DescriptorHeapVulkan - The buffer region started at images * imageDescriptorSize / bufferDescriptorSize, rounded down. A descriptor_heap array indexes the heap from byte 0 in strides of its descriptor size, so buffer slot 0 has to sit on a whole buffer descriptor past the images. Where the image descriptors were not a whole number of buffer descriptors, the first buffer descriptors overwrote the last image descriptors. Where they were less than one, the start was bumped to descriptor 1, but the heap was still sized as if the buffers began right after the images, so the last buffer descriptors went into the implementation's reserved range. Every descriptor size is a power of two, so both cases need image descriptors smaller than buffer descriptors, which NVIDIA (32 and 16 bytes) and llvmpipe (64 and 16) do not have. The start now rounds up and the heap is sized from it (#163). - The reserved range sat at a multiple of imageDescriptorAlignment only, where vkCmdBindResourceHeapEXT also wants bufferDescriptorAlignment (VUID-vkCmdBindResourceHeapEXT-pBindInfo-11435 and -11436). It now starts on a multiple of the larger. The buffer descriptors need no more than the rounding: each alignment is at most its descriptor size, so a whole number of descriptors is aligned. - bufferStartElement is 16 bits and took the division unchecked, so an image region past 65535 buffer descriptors (16384 images on llvmpipe) wrapped the buffer region around onto the images. Slot counts whose buffer indices do not fit 16 bits, 0xFFFF being the empty slot, now throw, naming the counts. A heap without buffer slots has no buffer region, which starts at 0 as in descriptor-set mode, so image-only heaps have no index to fit. - On a driver that reserves nothing (llvmpipe), a heap without sampler slots was a zero-size buffer, which every Initialize(1, 1, 0) made, and Window::Render bound its address each frame. Each heap now holds at least one descriptor, as each binding does in descriptor-set mode. That grows NVIDIA's sampler heap by one descriptor when no sampler slots are asked for; the resource heaps of the slot counts in the tree keep their layouts byte for byte. - ComputeLayout holds the arithmetic, pure over the descriptor-heap properties, and Initialize goes through it. GetBufferOffset and GetBufferOffsetElement, which had their own copies of the old arithmetic and never had a caller, are gone. - Bind(cmd, frame) binds a frame's heaps with the reserved ranges Initialize stored, or in descriptor-set mode the frame's descriptor set. Window::Render and the RTVolume example worked the reserved-range offset out from the heap's size with the image-alignment mask, and call Bind now. The window's per-heap cache of bind structs goes with it: keyed on the heap pointer, it kept binding the old buffers after a re-Initialize that reallocated them. The heap still ends with its reserved range, so code that derives the offset from the size keeps getting the right one. Tests - DescriptorHeapLayout: the issue's two layouts, the reserved range where buffer descriptors are aligned more strictly than image ones, every legal combination of descriptor sizes and alignments up to 256 bytes, no heap empty, the NVIDIA and llvmpipe layouts, the 16-bit limit and image-only heaps. Pure CPU, no device. Against the old arithmetic it fails 12 of its 18 checks. Docs - README.md's heap bullet and AGENTS.consumer.md say how to bind the heap outside the frame loop, and the consumer notes that Initialize throws when buffer indices would pass 16 bits. AGENTS.md lists ComputeLayout among the functions the pure-logic tests drive. Closes #163. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
465 lines
No EOL
23 KiB
C++
465 lines
No EOL
23 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, as does one whose
|
|
// swapchain the driver makes with more images than the frame loop's
|
|
// Window::numFrames (Mesa's Mailbox has at least four). 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.
|
|
// width/height already reflect the new size. Passes that hold
|
|
// descriptors referring to window.imageViews[] must re-write them
|
|
// here (the old VkImage handles have been destroyed).
|
|
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. 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);
|
|
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);
|
|
void CreateSwapchain();
|
|
// Tear-and-rebuild helper used by Resize() and the OUT_OF_DATE
|
|
// recovery in Render(). Calls CreateSwapchain() and re-issues the
|
|
// initial PRESENT_SRC_KHR layout transition for the new images so
|
|
// Render()'s barriers (which assume oldLayout = PRESENT_SRC_KHR)
|
|
// stay valid. Does NOT fire onResize — callers do that.
|
|
void RecreateSwapchainAndImages();
|
|
|
|
// 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;
|
|
static constexpr std::uint8_t numFrames = 3;
|
|
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
|
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
|
// 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;
|
|
// The mode presentMode asks for made a swapchain with more images than
|
|
// numFrames once, so swapchains are FIFO until SetPresentMode is
|
|
// called again, rather than being made twice at every resize.
|
|
bool presentModeUnfit = false;
|
|
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;
|
|
VkImage images[numFrames];
|
|
VkImageViewCreateInfo imageViews[numFrames];
|
|
// 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::array<bool, numFrames> imageInitialised{};
|
|
std::thread thread;
|
|
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 currentBuffer/frameCounter).
|
|
// presentInfo.pImageIndices and pSwapchains point at the
|
|
// currentBuffer/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) ──────────────
|
|
// The renderer keeps up to numFrames frames in flight, so a single
|
|
// semaphore pair + a per-frame wait-idle is no longer enough. Note
|
|
// that drawCmdBuffers, the per-frame descriptor heap slots, and the
|
|
// swapchain images are ALL keyed by the acquired image index
|
|
// (currentBuffer) — WriteSwapchainDescriptors bakes heap slot i to
|
|
// write imageViews[i], so the index that selects a command buffer /
|
|
// heap slot must equal the acquired image index. Everything below
|
|
// follows from that.
|
|
|
|
// 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
|
|
std::array<VkSemaphore, numFrames> renderFinishedSemaphores{};
|
|
|
|
// Signaled by the queue submit, waited + reset before
|
|
// drawCmdBuffers[image] and that image's descriptor-heap slot are
|
|
// re-recorded. Keyed by image index. Created signaled so the first
|
|
// wait on each image passes through instead of deadlocking.
|
|
std::array<VkFence, numFrames> waitFences{};
|
|
|
|
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
|
|
// by a free-running CPU frame counter, because the image index isn't
|
|
// known until acquire returns. Sized numFrames+1: at most numFrames
|
|
// frames are ever in flight (each image's command buffer is gated by
|
|
// its own fence, and there are numFrames images), so numFrames+1
|
|
// distinct acquire semaphores guarantees the one being reused has no
|
|
// pending operation — required by
|
|
// VUID-vkAcquireNextImageKHR-semaphore-01779.
|
|
static constexpr std::uint8_t numAcquireSemaphores = numFrames + 1;
|
|
std::array<VkSemaphore, numAcquireSemaphores> imageAcquiredSemaphores{};
|
|
|
|
// Free-running count of Render() calls; drives the acquire-semaphore
|
|
// slot (frameCounter % numAcquireSemaphores).
|
|
std::uint64_t frameCounter = 0;
|
|
std::uint32_t currentBuffer = 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 currentBuffer = 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
|
|
};
|
|
} |