Crafter.Graphics/interfaces/Crafter.Graphics-Window.cppm
Jorijn van der Graaf 234fcce099
fix(window): size the swapchain for its present mode, and key frames in flight by frame instead of image
Window
- CreateSwapchain asked for exactly numFrames (3) images whatever the
  surface said, read them into a three-element array, and keyed the
  command buffers, the fences and the descriptor-heap copy holding "its"
  swapchain image by the acquired image index. A swapchain of another
  size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw, and a
  surface whose minimum is above three got a create info below it
  (#160). Mesa's MAILBOX needs four images: Turnip on phoc, llvmpipe on
  any Wayland compositor.
- The present mode is picked first, then the swapchain is sized by
  SwapchainCapabilities: the surface's counts for that mode where the
  instance has surface_maintenance1. With swapchain_maintenance1 the
  swapchain names its mode (VkSwapchainPresentModesCreateInfoKHR), which
  is what makes the mode's own count the one that applies, at creation and
  to the forward progress of a UINT64_MAX acquire; without it the stricter
  of the mode's and the surface's counts for all modes. It asks for
  numFrames or that minimum where more, never past the maximum
  (SwapchainMinImageCount). Turnip's FIFO keeps its three images and
  MAILBOX gets four. The FIFO fallback and presentModeUnfit are gone.
- images, imageViewInfos (was imageViews: they are create-infos), the made
  imageViews that descriptor sets take, imageInitialised and the
  render-finished semaphores are per image, as many as the driver made.
  The views are made once per swapchain. The semaphores only grow, since
  a present of the old swapchain may still wait on one.
- Frames in flight stay numFrames, keyed by currentFrame, frameCounter %
  numFrames: the command buffer, the fence, the acquire semaphore, the
  heap copy (or set), RenderingElement3D::tlases, the frameIdx a pass is
  recorded with. currentImage is the acquired image. currentBuffer is
  gone, so code that used it stops compiling instead of picking the wrong
  one; RenderPass::Record's frameIdx keeps its name and is now the frame
  slot, as RenderPass.cppm says.
- The frame's fence is waited before the acquire. The acquire semaphores
  need no spare then, and a frame waits for exactly the one numFrames
  before it, which the deletion queue and the staging rings count on and
  fences keyed by image did not promise once images come back out of
  order. The swapchain slots are written before the fence is reset.
- From the acquire to the present the window is recording: a Resize or
  SetPresentMode from a listener (a VSync key ticked in onUpdate) takes
  effect after the present, through RebuildSwapchain, together with a
  present that found the swapchain out of date. Rebuilding mid-frame used
  to destroy the image the frame was drawing into; with images sized per
  swapchain it would also index past the new one's end.

DescriptorHeapVulkan
- Frames and images pair up differently from frame to frame, so no heap
  copy can keep one swapchain image. AddSwapchainSlot(slot, window) lists
  an image slot as that window's swapchain image, and Window::Render
  writes the image a frame acquired into its window's listed slots in that
  frame's copy, or its set, before onUpdate, skipping a slot that holds it
  already. A slot is one window's, so two windows can share a heap without
  writing each other's slots under each other's frames in flight. A slot
  listed during a frame its window records is written at once. A slot past
  the heap's image slots, an empty ImageSlot's 0xFFFF among them, is
  refused. Freeing a slot unlists it, as does initialising the heap in
  either binding model. WriteImageSet, the one use of which this
  replaces, is gone, and with it the per-frame dimension of setImageViews.

Device
- VK_KHR_surface_maintenance1 (or EXT) and VK_KHR_get_surface_capabilities2
  are enabled where the loader has them (surfaceMaintenance1Supported,
  vkGetPhysicalDeviceSurfaceCapabilities2KHR), and the matching
  swapchain_maintenance1 with its feature where the device has it
  (swapchainMaintenance1Supported).
- DistinctGpus groups the physical devices by GPU as CRAFTER_GRAPHICS_DEVICE
  numbers them, for ChooseGpu and tests.

UI
- UIRenderer lists its output slot instead of writing every copy itself,
  and again on every resize; the resize listener is gone on both
  backends. The headers' frameIdx is currentFrame.

Examples
- VulkanTriangle, Sponza, SponzaBench and RTVolume list their output slot
  and write only the per-frame TLASes. They wrote imageViews[f] into copy
  f once, which a resize left pointing at destroyed images.

Tests
- SwapchainImageCount: SwapchainMinImageCount on the numbers drivers
  report; every offered mode keeps its mode and gets what the window asks
  for; every frame presents the colour it drew, read back a texel per
  frame; a switch and a resize asked for during a frame wait for its
  present; a slot listed during a frame holds its image at once; two
  windows on one heap draw their own colours. The frame checks run again
  in child processes, through CRAFTER_GRAPHICS_DEVICE, on a GPU whose
  swapchains have more images than frames (this one, else llvmpipe), in
  each binding model the parent didn't cover; a child skips only for a GPU
  that can't run the library, and checks it runs the model it was asked
  for.
- FrameLoopSync checks the frame slot follows the frame counter and counts
  the swapchain's own images. HeapASRead, HeapDynamicIndexHit and
  RTPassPushData list their swapchain slot and drop their resize rewrites.

Docs
- README, AGENTS.md and AGENTS.consumer.md: frames and images, the
  swapchain slots, rebuilds held to the present, the new optional groups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:14:18 +01:00

528 lines
No EOL
28 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/input-event-codes.h>
#include <xkbcommon/xkbcommon.h>
#include "../lib/xdg-shell-client-protocol.h"
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#include <string.h>
#include <linux/input.h>
#include <sys/mman.h>
#include <wayland-cursor.h>
#include <errno.h>
#include <fcntl.h>
#include <print>
#include <wayland-client.h>
#include <wayland-client-protocol.h>
#endif
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include <windows.h>
#endif
export module Crafter.Graphics:Window;
import std;
import :Types;
import :Keys;
import Crafter.Event;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import :WebGPU;
import :DescriptorHeapWebGPU;
#endif
export namespace Crafter {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
struct Semaphores {
// Swap chain image presentation
VkSemaphore presentComplete;
// Command buffer submission and execution
VkSemaphore renderComplete;
};
struct RenderPass;
struct DescriptorHeapVulkan;
#else
struct RenderPass;
struct DescriptorHeapWebGPU;
#endif
// How a window hands finished frames to the display. Fifo waits for the
// display's refresh; it is the default and the one mode every surface
// has. Mailbox replaces a frame still waiting to be shown, so presenting
// never waits, which suits a window that updates on its own clock
// (Window::updateInterval). Immediate shows each frame at once and may
// tear. A mode the surface lacks falls back to Fifo. The mode decides how
// many images the swapchain has (Mesa's Mailbox needs at least four),
// which the frame loop takes as they come. In the browser the page
// presents the canvas and the mode has no effect.
enum class PresentMode {
Fifo,
Mailbox,
Immediate,
};
struct Window {
FrameTime currentFrameTime;
std::uint32_t width;
std::uint32_t height;
std::chrono::time_point<std::chrono::high_resolution_clock> lastFrameBegin;
Event<void> onClose;
Event<void> onBeforeUpdate;
Event<FrameTime> onUpdate;
// Fires when the swapchain has been recreated, for a new size or a
// new present mode. width/height already reflect the new size, and
// the old images have been destroyed: anything that holds one of
// them, or is sized after the window, is made again here. The heap
// slots that name the swapchain image follow by themselves (see
// DescriptorHeapVulkan::AddSwapchainSlot).
Event<void> onResize;
bool open = true;
bool updating = false;
// Zero (the default): Update runs when the compositor's frame
// callback says the window is about to show a new frame, so it
// follows the display and stops while the window is hidden or
// minimized. Non-zero: Update runs on this clock instead, visible or
// not, for an app whose output goes somewhere besides the screen.
// Pair it with PresentMode::Mailbox so a present never waits on the
// display. Set it before StartUpdate. Native only: in the browser,
// requestAnimationFrame drives Update.
std::chrono::nanoseconds updateInterval{0};
// The mode SetPresentMode last asked for.
PresentMode presentMode = PresentMode::Fifo;
// Currently-pressed raw key codes. The runtime stores raw platform
// codes only (Win32 PS/2 scancode + extended bit; Wayland kernel
// keycode). Cross-platform default bindings use `Key(CrafterKeys::X)`
// from :Keys to obtain the right code at compile time.
std::unordered_set<KeyCode> heldKeys;
Event<KeyCode> onRawKeyDown;
Event<KeyCode> onRawKeyHold;
Event<KeyCode> onRawKeyUp;
Event<const std::string_view> onTextInput;
Event<void> onMouseRightClick;
Event<void> onMouseLeftClick;
Event<void> onMouseRightHold;
Event<void> onMouseLeftHold;
Event<void> onMouseRightRelease;
Event<void> onMouseLeftRelease;
Event<void> onMouseMove;
Event<void> onMouseEnter;
Event<void> onMouseLeave;
// Mouse wheel, in whole detents per event, sign packed into the
// uint32 payload (reinterpret as int32 to read it). Positive =
// wheel down / toward the user — the DOM deltaY sign. Every
// backend normalizes to ±1 per notch: dom-env.js divides
// WheelEvent deltas by the per-deltaMode detent size, Wayland
// divides the axis value by libinput's 15-units-per-detent,
// Win32 divides by WHEEL_DELTA. Sub-detent motion (smooth-scroll
// touchpads, free-spinning wheels) is accumulated until a whole
// detent is reached — by PointerScroll natively, by dom-env.js in
// the browser — and dropped when the pointer leaves.
Event<std::uint32_t> onMouseScroll;
// Touch, from the seat's wl_touch. The first finger down also drives
// currentMousePos and the left-button events until it lifts, so a
// pointer-only application works on a touchscreen as it is.
Event<TouchPoint> onTouchDown;
Event<TouchPoint> onTouchMove;
Event<TouchPoint> onTouchUp;
Event<void> onTouchCancel;
std::int32_t primaryTouchId = -1;
// Text input (zwp_text_input_v3): the input method's composing text
// (an empty string clears it) and its deletions around the cursor;
// the text it commits arrives as onTextInput, like a key's.
Event<const std::string_view> onPreedit;
Event<TextDelete> onTextInputDelete;
Vector<float, 2> currentMousePos;
Vector<float, 2> lastMousePos;
Vector<float, 2> mouseDelta;
bool mouseLeftHeld = false;
bool mouseRightHeld = false;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// The pointer is over the window: set before onMouseEnter fires,
// cleared before onMouseLeave does.
bool mouseInside = false;
// The part of a wheel detent PointerScroll has not reported yet.
double scrollRemainder = 0.0;
// The backends report the pointer through these, so that enter,
// move, leave and scroll mean the same on each. PointerEnter puts
// the entry position in currentMousePos before onMouseEnter fires,
// and an enter is a move too: the backend follows it with
// PointerMove. PointerLeave drops the scroll remainder, and fires
// only after an enter. Each fires its event last, since a listener
// may destroy the window, so a backend that makes a second call
// first checks that the window still exists.
void PointerEnter(Vector<float, 2> position);
void PointerMove(Vector<float, 2> position);
void PointerLeave();
// Adds wheel motion in detents, fractions included, and fires
// onMouseScroll for each whole detent it adds up to.
void PointerScroll(double detents);
#endif
Window() = default;
Window(std::uint32_t width, std::uint32_t height);
Window(std::uint32_t width, std::uint32_t height, const std::string_view title);
Window(Window&) = delete;
Window(Window&&) = delete;
Window& operator=(const Window&) = delete;
// DOM mode keeps a process-global pointer to the live Window so
// the JS bridge can deliver document/window events back. If the
// Window is destroyed (e.g. it was stack-allocated in main and
// main returned), the global must be cleared so subsequent
// browser callbacks become no-ops instead of dereferencing
// freed memory. Native builds release what the constructor made:
// after a device wait, the command buffers, semaphores and fences,
// the swapchain and the Vulkan surface, then (Wayland) the cursor,
// viewport, fractional scale, decoration, xdg toplevel, xdg surface
// and wl_surface, and the window leaves Device::windows and the
// focus pointers, or (Win32) the HWND, detached from this Window
// first so that no message still meant for it arrives here — so a
// window can be closed and another created, which a resident app
// does on every hide and show.
~Window();
// Enter the platform event loop and never return. In DOM mode this
// hands the loop to requestAnimationFrame and _Exit(0)s so the wasm
// instance survives without running static destructors. No caller
// code after this point ever runs, on any target.
void StartSync();
// As StartSync, but does NOT start the animation-frame loop in DOM
// mode — for event-driven pages that only touch the DOM and never
// draw. A rAF tick running forever to do nothing costs battery in
// every open tab. Still never returns.
//
// On native this is identical to StartSync: the event loop is what
// keeps a window alive there, so there is no cheaper option. Call it
// unconditionally and get the browser saving without an #ifdef.
void StayAlive();
void StartUpdate();
void StopUpdate();
// Makes the swapchain again with the new mode and fires onResize, as
// a resize does, so call it outside StartInit/FinishInit. Called while
// a frame is being recorded (from onUpdate, a pass, an input action
// ticked there), the new swapchain is made once that frame has been
// presented: the frame still draws into an image of the old one. The
// environment variable CRAFTER_PRESENT_IMMEDIATE overrides the mode,
// for benchmarks.
void SetPresentMode(PresentMode mode);
void SetTitle(const std::string_view title);
// The xdg app id, which the compositor matches to a .desktop file
// for the icon, the task switcher and activation.
void SetAppId(const std::string_view id);
// Tells the compositor's input method that a text field has focus
// (an on-screen keyboard opens) and where its caret is, in physical
// pixels, or that none has. Remembered: it is applied again whenever
// the keyboard focus comes back to this window.
void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal);
// Like SetPresentMode, a resize asked for while a frame is being
// recorded takes effect once that frame has been presented, width and
// height included, which the rest of the frame still draws at.
void Resize(std::uint32_t width, std::uint32_t height);
void Render();
void Update();
// Replace the system cursor with a custom image. `pixels` is
// `width*height*4` bytes in R8G8B8A8 memory order (matching
// stb_image's STBI_rgb_alpha output) with straight (non-premultiplied)
// alpha — the conversion to the compositor's expected format is
// handled internally. The hotspot is in image-pixel coordinates.
// Re-callable at any time.
void SetCursorImage(std::uint16_t width, std::uint16_t height,
std::uint16_t hotspotX, std::uint16_t hotspotY,
const std::uint8_t* pixels);
// Restore the default system cursor (releases any previously-uploaded
// cursor pixel buffer).
void SetDefaultCursor();
#ifdef CRAFTER_TIMING
std::chrono::nanoseconds totalUpdate;
std::vector<std::pair<const EventListener<FrameTime>*, std::chrono::nanoseconds>> updateTimings;
std::chrono::nanoseconds totalRender;
std::chrono::nanoseconds vblank;
std::chrono::nanoseconds totalFrame;
std::chrono::time_point<std::chrono::high_resolution_clock> frameEnd;
// Fixed-size ring buffer of the most recent frame times. LogTiming does
// order-independent sum/avg/min/max, so head position is irrelevant to
// the reported stats; this avoids the per-frame memmove a vector::erase
// at the front would incur once full.
static constexpr std::size_t frameTimeCapacity = 100;
std::array<std::chrono::nanoseconds, frameTimeCapacity> frameTimes{};
std::size_t frameTimesHead = 0;
std::size_t frameTimesCount = 0;
void LogTiming();
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
HWND hwnd = nullptr;
HBITMAP cursorBitmap = nullptr;
HCURSOR cursorHandle = nullptr;
std::uint16_t cursorSizeX = 0;
std::uint16_t cursorSizeY = 0;
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
// Wakes the event loop when `fd` becomes readable and calls
// `onReadable` from it, which must consume what made the descriptor
// readable, or the loop wakes again at once. For a socket, or an
// eventfd another thread signals, so new data can drive an Update
// right away (call Update from the callback) instead of waiting for
// the next one. StartSync's loop serves the descriptors of every
// window. Remove a descriptor before closing it.
void AddPollFd(int fd, std::function<void()> onReadable);
void RemovePollFd(int fd);
struct PollFd {
int fd;
std::function<void()> onReadable;
};
std::vector<PollFd> pollFds;
float scale = 1.0f;
bool configured = false;
// Pending size from the most recent xdg_toplevel.configure, in
// surface-local (logical DP) units. Applied on xdg_surface.configure
// via Resize(); 0 means "compositor has no preference, keep current".
std::int32_t pendingLogicalWidth = 0;
std::int32_t pendingLogicalHeight = 0;
xdg_toplevel* xdgToplevel = nullptr;
zxdg_toplevel_decoration_v1* decoration = nullptr;
wp_viewport* wpViewport = nullptr;
wl_surface* surface = nullptr;
xdg_surface* xdgSurface = nullptr;
wl_callback* cb = nullptr;
wl_surface* cursorSurface = nullptr;
wl_buffer* cursorWlBuffer = nullptr;
std::uint32_t cursorBufferOldSize = 0;
// mmap'd view of the SHM cursor buffer — the user-supplied pixels
// are written here in BGRA8888 order. Lifetime matches cursorWlBuffer.
std::uint8_t* cursorMmap_ = nullptr;
std::uint16_t cursorHotspotX_ = 0;
std::uint16_t cursorHotspotY_ = 0;
// Most recent serial from a wl_pointer.enter on this window's surface.
// Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor`
// mid-session (the hotspot only updates when set_cursor is recalled).
std::uint32_t lastPointerSerial_ = 0;
bool textInputActive_ = false;
bool textInputEnabled_ = false; // enable sent and not taken back since
std::int32_t textInputRect_[4] = { 0, 0, 0, 0 };
TextInputPurpose textInputPurpose_ = TextInputPurpose::Normal;
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);
static void xdg_toplevel_handle_close(void* data, xdg_toplevel*);
static void xdg_surface_handle_configure(void* data, xdg_surface* xdg_surface, std::uint32_t serial);
static void xdg_toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*);
constexpr static xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_configure,
.close = xdg_toplevel_handle_close,
};
constexpr static wl_callback_listener wl_callback_listener = {
.done = wl_surface_frame_done,
};
constexpr static xdg_surface_listener xdg_surface_listener = {
.configure = xdg_surface_handle_configure,
};
constexpr static wp_fractional_scale_v1_listener wp_fractional_scale_v1_listener = {
.preferred_scale = xdg_surface_handle_preferred_scale,
};
wp_fractional_scale_v1* wp_scale = nullptr;
#endif
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
VkCommandBuffer StartInit();
void FinishInit();
VkCommandBuffer GetCmd();
void EndCmd(VkCommandBuffer cmd);
// Makes the swapchain, after the old one's images have stopped being
// used: every caller has waited for the queue (or has no frames yet).
void CreateSwapchain();
// The capabilities a swapchain of `mode` on `surface` is sized by. With
// surface_maintenance1 they are the surface's for that mode, which may
// differ from its answer for all modes (Mesa on Wayland: FIFO 3, all
// modes 4, because MAILBOX needs 4). Those are the ones that apply when
// the swapchain is made naming its mode, which `modePassedAlong` says
// (swapchain_maintenance1's VkSwapchainPresentModesCreateInfoKHR).
// Without that the counts for all modes bind as well, so the minimum
// and maximum are the stricter of the two. Asks for the mode last, as
// the validation layer checks minImageCount against the surface's
// last answer. Static so a test sizes swapchains the way the window
// does.
static VkSurfaceCapabilitiesKHR SwapchainCapabilities(VkPhysicalDevice gpu, VkSurfaceKHR surface,
VkPresentModeKHR mode, bool modePassedAlong);
// The minImageCount CreateSwapchain asks for, from those capabilities:
// numFrames, or the surface's minimum where that is more, and never
// more than its maximum (0 means no maximum). The driver may still
// make more. Static so a test can feed it the numbers drivers report.
static std::uint32_t SwapchainMinImageCount(const VkSurfaceCapabilitiesKHR& capabilities);
// Calls CreateSwapchain(). The new images start in UNDEFINED, and
// Render() transitions each on its first acquire (imageInitialised).
// Does NOT fire onResize — callers do that.
void RecreateSwapchainAndImages();
// What Resize and SetPresentMode do once no frame is being recorded:
// waits for the queue, frees the deletion queue, makes the swapchain
// again at width x height and fires onResize.
void RebuildSwapchain();
// Saves the frame the next Render() draws to a PNG at `path`, for
// visual regression tests and screenshots: that Render copies the
// swapchain image into a readback buffer before presenting it, waits
// for the frame, and writes the file. The copy can't come later: a
// presented image belongs to the presentation engine until it is
// acquired again.
void CaptureNextFrame(std::filesystem::path path);
std::optional<std::filesystem::path> pendingCapture;
// ── Frames and images ───────────────────────────────────────────
// The frame loop keeps numFrames frames in flight, and that number is
// its own: the swapchain has however many images the driver made for
// the present mode, which may be fewer or more (Mesa's Mailbox has at
// least four; issue #160). Two indices follow from that, set by
// Render() before onUpdate:
//
// currentFrame this frame's slot, frameCounter % numFrames. It
// picks everything that lives per frame in flight:
// the command buffer, the fence and acquire
// semaphore, the descriptor heap copy (or set) the
// frame binds, RenderingElement3D::tlases, the
// frameIdx a RenderPass is recorded with.
// currentImage the swapchain image the frame acquired and draws
// into: images[currentImage].
//
// The two go together differently from frame to frame, so a heap copy
// can't hold "its" swapchain image: Render() writes the acquired one
// into the frame's copy, at the slots DescriptorHeapVulkan::
// AddSwapchainSlot lists for this window, before onUpdate.
static constexpr std::uint8_t numFrames = 3;
std::uint32_t currentFrame = 0;
std::uint32_t currentImage = 0;
// True from a frame's acquire to its present: images[currentImage]
// and the frame's heap copy are this frame's, and a swapchain rebuild
// (Resize, SetPresentMode) waits for the present. What is waiting:
// a new present mode, or a present that found the swapchain out of
// date; and the last size a resize asked for, 0 when none did.
bool recording = false;
bool swapchainRebuildPending = false;
std::uint32_t deferredWidth = 0;
std::uint32_t deferredHeight = 0;
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
// Bumped from a process-wide count whenever CreateSwapchain makes a
// swapchain, so it names one swapchain for good: a new swapchain's
// handle and images may reuse the old one's handle values. The heap
// keys what it wrote into each copy on it.
std::uint64_t swapchainGeneration = 0;
// What the swapchain images were created for: STORAGE | TRANSFER_SRC,
// plus TRANSFER_DST where the surface allows it, so a pass can blit or
// clear into the image instead of only writing it from a shader.
VkImageUsageFlags swapchainUsage = 0;
// The mode the swapchain was made with: presentMode, unless the
// surface lacks it or CRAFTER_PRESENT_IMMEDIATE is set.
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
VkFormat colorFormat;
VkColorSpaceKHR colorSpace;
// Supported composite-alpha mode for vulkanSurface. A surface property
// that does not change across swapchain recreation, so it is selected
// once at construction rather than re-derived on every CreateSwapchain
// (i.e. on every resize configure).
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// One entry per swapchain image, as many as vkGetSwapchainImagesKHR
// returned; remade with the swapchain. imageViewInfos are the views a
// heap descriptor takes (VkImageDescriptorInfoEXT::pView); imageViews
// the same views made, which a descriptor set takes, owned by the
// window and destroyed with its swapchain.
std::vector<VkImage> images;
std::vector<VkImageViewCreateInfo> imageViewInfos;
std::vector<VkImageView> imageViews;
// Tracks whether each swapchain image has been rendered (and thus
// left in PRESENT_SRC_KHR) at least once. Freshly created swapchain
// images start in VK_IMAGE_LAYOUT_UNDEFINED, so the first per-frame
// barrier must transition from UNDEFINED, not PRESENT_SRC_KHR.
// Reset in CreateSwapchain(). A presentable image may only be touched
// after it has been acquired, so this initial transition happens
// lazily in Render() (post-acquire) rather than up front.
std::vector<bool> imageInitialised;
std::thread thread;
// Per frame in flight.
VkCommandBuffer drawCmdBuffers[numFrames] = {};
// Per-frame Vulkan info structs whose contents are almost entirely
// invariant. Initialised once in the constructor; Render() patches only
// the few fields that change each frame (the two barriers' image and
// the acquire barrier's oldLayout, and — for the multi-frame-in-flight
// sync below — submitInfo/presentInfo's wait/signal semaphores plus
// submitInfo's command buffer, which follow currentFrame/currentImage).
// presentInfo.pImageIndices and pSwapchains point at the
// currentImage/swapChain members, so they track value changes
// (including swapchain recreation) automatically.
VkSubmitInfo submitInfo;
VkCommandBufferBeginInfo cmdBufInfo;
VkImageMemoryBarrier acquireBarrier;
VkImageMemoryBarrier presentBarrier;
VkPresentInfoKHR presentInfo;
// ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
// Render() waits for its frame slot's fence before it acquires, so
// the command buffer, the heap copy and the acquire semaphore of that
// slot are free again, whichever image the acquire then returns.
// Signaled by the queue submit, waited by vkQueuePresentKHR. Keyed by
// the acquired IMAGE index: the presentation engine keeps this
// semaphore in use until the image is re-acquired, so a per-image
// semaphore is the only safe key. Keying it per-CPU-frame trips
// VUID-vkQueueSubmit-pSignalSemaphores-00067.
// https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html
// Grown by CreateSwapchain to the image count and never shrunk: a
// present of the old swapchain may still wait on one, and nothing
// reports when that wait is done, so they live as long as the window.
std::vector<VkSemaphore> renderFinishedSemaphores;
// Signaled by the queue submit, waited + reset before
// drawCmdBuffers[frame] and that frame's descriptor-heap copy are
// re-recorded. Keyed by frame slot. Created signaled so the first
// wait on each slot passes through instead of deadlocking.
std::array<VkFence, numFrames> waitFences{};
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
// by frame slot: the slot's fence is waited before the acquire, and
// that fence follows the submit that waited on the semaphore, so the
// semaphore has no pending operation when it is reused
// (VUID-vkAcquireNextImageKHR-semaphore-01779).
std::array<VkSemaphore, numFrames> imageAcquiredSemaphores{};
// Free-running count of Render() calls; currentFrame is this modulo
// numFrames.
std::uint64_t frameCounter = 0;
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
std::vector<RenderPass*> passes;
DescriptorHeapVulkan* descriptorHeap = nullptr;
std::optional<std::array<float, 4>> clearColor;
#else
// DOM mode: the page IS the window. WebGPU device and canvas are
// owned JS-side (see additional/dom-webgpu.js); this struct just
// holds the per-Window state Crafter::Window users expect:
// a list of render passes and a pointer to the descriptor heap.
static constexpr std::uint8_t numFrames = 1;
std::uint32_t currentFrame = 0;
std::vector<RenderPass*> passes;
DescriptorHeapWebGPU* descriptorHeap = nullptr;
std::optional<std::array<float, 4>> clearColor;
// DOM-mode StartInit/FinishInit are no-ops returning an opaque
// command-buffer marker so cross-platform user code (HelloUI's
// `auto init = window.StartInit();`) compiles unchanged.
WebGPUCommandEncoderRef StartInit();
void FinishInit();
#endif
};
}