Win32
- WM_MOUSEMOVE is handled again: c9ebd44 put the WM_CHAR case where it
was, so currentMousePos stopped following the mouse and onMouseMove
never fired. Positions are read signed (GET_X_LPARAM), since under
capture they run past the client area.
- onMouseEnter and onMouseLeave. The first move over the client area
after a leave is the enter, and arms the TME_LEAVE whose WM_MOUSELEAVE
is the leave. An enter counts only once that is armed, a leave only
after an enter, and a move past the client area is a move only.
- A press captures the mouse, so a drag that leaves the window still
sends its moves and its release, as Wayland's implicit grab does.
Losing the capture releases what is held, and a button-up without its
press fires nothing.
- WM_CLOSE fires onClose and clears open, as xdg_toplevel.close does,
instead of destroying the HWND under the swapchain. WM_DESTROY no
longer posts WM_QUIT, which nothing read.
- ~Window detaches the HWND and destroys it. It stayed on screen, and its
messages went to the freed Window. WndProc hands messages without a
Window to DefWindowProc, and looks the Window up again after an event
that more work follows, in case a listener destroyed it.
- The constructor's message pump leaves input queued for StartSync: the
WM_MOUSEMOVE sent when a window appears under the cursor made an
onMouseEnter nobody could hear yet. The mouse state is reset before
the platform code, no longer after it.
- A window after the first finds its class registered, which showed an
error box.
- The wheel remainder is the window's, no longer a function static that
every window shared, and a leave drops it.
Window
- PointerEnter, PointerMove, PointerLeave and PointerScroll hold the rules
both native backends report the pointer by. An enter puts the entry
position in currentMousePos before onMouseEnter fires, and is followed
by a move, on Wayland too now. scrollRemainder replaces
Device::scrollDetentRemainder, and mouseInside is shared.
Tests
- PointerEnterLeave: enter, move and leave through the wl_pointer
callbacks.
- PointerWin32: posts a real HWND the messages above and pumps them as
StartSync does. Built for x86_64-w64-mingw32 it runs under Wine, and is
skipped where Wine is not on PATH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
477 lines
No EOL
24 KiB
C++
477 lines
No EOL
24 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;
|
|
// Cached per-frame heap-bind structs. The heap address/size per slot
|
|
// are stable once a heap is assigned; only currentBuffer varies frame
|
|
// to frame, so the structs (and the reservedRangeOffset arithmetic)
|
|
// are computed once per heap rather than every frame in Render().
|
|
// Keyed on the heap pointer: whenever descriptorHeap differs from
|
|
// cachedDescriptorHeap the cache is rebuilt. This invalidates on any
|
|
// heap (re)assignment — heaps never resize today, so a stable pointer
|
|
// means stable ranges. Not tied to onResize (the heap is independent
|
|
// of swapchain size).
|
|
VkBindHeapInfoEXT resourceHeapInfos[numFrames];
|
|
VkBindHeapInfoEXT samplerHeapInfos[numFrames];
|
|
DescriptorHeapVulkan* cachedDescriptorHeap = 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
|
|
};
|
|
} |