Crafter.Graphics/interfaces/Crafter.Graphics-Window.cppm

361 lines
17 KiB
Text
Raw Permalink Normal View History

2025-05-07 19:21:51 +02:00
/*
Crafter®.Graphics
2026-03-09 20:10:19 +01:00
Copyright (C) 2026 Catcrafts®
2025-11-22 20:58:42 +01:00
catcrafts.net
2025-05-07 19:21:51 +02:00
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
2025-11-22 20:58:42 +01:00
License version 3.0 as published by the Free Software Foundation;
2025-05-07 19:21:51 +02:00
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
2025-11-17 00:44:45 +01:00
module;
2026-03-09 20:10:19 +01:00
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
2025-11-22 20:58:42 +01:00
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/input-event-codes.h>
2025-11-17 00:44:45 +01:00
#include <xkbcommon/xkbcommon.h>
#include "../lib/xdg-shell-client-protocol.h"
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
2025-12-29 18:56:06 +01:00
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
2025-11-22 20:58:42 +01:00
#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
2026-05-18 02:07:48 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-02 23:53:13 +01:00
#include "vulkan/vulkan.h"
2026-05-18 02:07:48 +02:00
#endif
2026-04-02 16:52:10 +02:00
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include <windows.h>
#endif
2025-11-17 00:44:45 +01:00
2025-04-16 00:43:33 +02:00
export module Crafter.Graphics:Window;
2025-11-16 15:32:11 +01:00
import std;
2025-11-23 04:04:53 +01:00
import :Types;
2026-05-12 00:24:48 +02:00
import :Keys;
2025-11-23 04:04:53 +01:00
import Crafter.Event;
2026-05-18 04:58:52 +02:00
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import :WebGPU;
import :DescriptorHeapWebGPU;
#endif
2025-04-16 00:43:33 +02:00
export namespace Crafter {
2026-05-18 02:07:48 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-09 20:10:19 +01:00
struct Semaphores {
// Swap chain image presentation
VkSemaphore presentComplete;
// Command buffer submission and execution
VkSemaphore renderComplete;
};
2026-05-01 23:35:37 +02:00
struct RenderPass;
2026-04-05 22:53:59 +02:00
struct DescriptorHeapVulkan;
2026-05-18 04:58:52 +02:00
#else
struct RenderPass;
struct DescriptorHeapWebGPU;
2026-05-18 02:07:48 +02:00
#endif
2026-03-09 20:10:19 +01:00
struct Window {
2026-01-30 00:09:37 +01:00
FrameTime currentFrameTime;
2026-03-09 20:10:19 +01:00
std::uint32_t width;
std::uint32_t height;
2025-11-25 18:52:32 +01:00
std::chrono::time_point<std::chrono::high_resolution_clock> lastFrameBegin;
2025-11-23 04:04:53 +01:00
Event<void> onClose;
Event<void> onBeforeUpdate;
2025-11-24 03:38:20 +01:00
Event<FrameTime> onUpdate;
2026-05-12 00:24:48 +02:00
// 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;
2025-11-23 04:04:53 +01:00
bool open = true;
bool updating = false;
2026-05-12 00:24:48 +02:00
// 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;
2026-04-16 23:03:24 +02:00
Event<const std::string_view> onTextInput;
2026-03-09 20:10:19 +01:00
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 backend-side
// until a whole detent is reached.
2025-12-30 23:28:38 +01:00
Event<std::uint32_t> onMouseScroll;
2026-03-09 20:10:19 +01:00
Vector<float, 2> currentMousePos;
Vector<float, 2> lastMousePos;
Vector<float, 2> mouseDelta;
2025-11-23 04:04:53 +01:00
bool mouseLeftHeld = false;
bool mouseRightHeld = false;
2025-11-22 20:58:42 +01:00
2026-03-12 01:07:46 +01:00
Window() = default;
2026-03-09 20:10:19 +01:00
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;
2026-05-18 02:07:48 +02:00
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
// 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 have nothing to clean up at the
// Window level (Vulkan resources teardown is the user's job).
~Window();
#endif
2025-11-22 20:58:42 +01:00
2026-03-09 20:10:19 +01:00
void StartSync();
void StartUpdate();
void StopUpdate();
void SetTitle(const std::string_view title);
void Resize(std::uint32_t width, std::uint32_t height);
void Render();
void Update();
2026-05-02 21:08:20 +02:00
// 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();
2025-11-22 20:58:42 +01:00
2026-03-09 20:10:19 +01:00
#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;
2026-03-09 20:10:19 +01:00
void LogTiming();
#endif
2026-04-02 16:52:10 +02:00
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
HBITMAP cursorBitmap = nullptr;
HCURSOR cursorHandle = nullptr;
std::uint16_t cursorSizeX = 0;
std::uint16_t cursorSizeY = 0;
#endif
2026-03-09 20:10:19 +01:00
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
2026-05-01 23:35:37 +02:00
float scale = 1.0f;
2025-11-23 04:04:53 +01:00
bool configured = false;
2026-05-12 00:24:48 +02:00
// 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;
2025-11-23 04:04:53 +01:00
xdg_toplevel* xdgToplevel = nullptr;
2025-12-29 18:56:06 +01:00
wp_viewport* wpViewport = nullptr;
2025-11-23 04:04:53 +01:00
wl_surface* surface = nullptr;
xdg_surface* xdgSurface = nullptr;
wl_callback* cb = nullptr;
2026-03-12 01:07:46 +01:00
wl_surface* cursorSurface = nullptr;
wl_buffer* cursorWlBuffer = nullptr;
std::uint32_t cursorBufferOldSize = 0;
2026-05-02 21:08:20 +02:00
// 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;
2026-03-09 20:10:19 +01:00
2025-12-29 18:56:06 +01:00
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
2026-03-09 20:10:19 +01:00
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);
2025-11-23 04:04:53 +01:00
static void xdg_toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*);
2026-03-09 20:10:19 +01:00
2025-11-23 04:04:53 +01:00
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 = {
2025-11-22 20:58:42 +01:00
.configure = xdg_surface_handle_configure,
};
2025-12-29 18:56:06 +01:00
constexpr static wp_fractional_scale_v1_listener wp_fractional_scale_v1_listener = {
.preferred_scale = xdg_surface_handle_preferred_scale,
};
2026-03-09 20:10:19 +01:00
inline static wp_fractional_scale_v1* wp_scale = nullptr;
#endif
2026-01-27 22:34:24 +01:00
2026-05-18 02:07:48 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-09 20:10:19 +01:00
VkCommandBuffer StartInit();
void FinishInit();
VkCommandBuffer GetCmd();
void EndCmd(VkCommandBuffer cmd);
2026-01-27 22:34:24 +01:00
void CreateSwapchain();
2026-05-12 00:24:48 +02:00
// 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();
2026-05-01 23:35:37 +02:00
// Save the current swapchain image (state after Render() returns) to
// a PNG file. Allocates a one-shot staging buffer + command buffer,
// copies image-to-buffer, waits idle, then writes PNG via stb. Useful
// for visual regression tests and screenshotting from headless code.
void SaveFrame(const std::filesystem::path& path);
2026-03-09 20:10:19 +01:00
static constexpr std::uint8_t numFrames = 3;
2026-01-27 22:34:24 +01:00
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
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;
2026-02-03 21:03:11 +01:00
VkImage images[numFrames];
2026-04-05 22:53:59 +02:00
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{};
2026-01-27 22:34:24 +01:00
std::thread thread;
2026-02-03 21:03:11 +01:00
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;
feat(window): multi-frame-in-flight frame pacing (#40) The renderer was effectively single-buffered despite allocating triple-buffered infrastructure: Render() ended with an unconditional vkQueueWaitIdle, and a single (presentComplete, renderComplete) semaphore pair with zero fences was shared for the Window's lifetime. Rework the pacing model so up to numFrames frames overlap: - Per-swapchain-image VkFence, signaled by the submit and waited+reset before that image's command buffer / descriptor-heap slot is re-recorded. Keyed by acquired image index (drawCmdBuffers, the heap slots, and the swapchain images are all image-indexed — see WriteSwapchainDescriptors). Created signaled so first use passes. - Per-image render-finished (present) semaphore: the presentation engine holds it until the image is re-acquired, so per-image is the only safe key (per-CPU-frame trips VUID-vkQueueSubmit-pSignalSemaphores-00067). - Per-CPU-frame acquire semaphores (image index unknown until acquire returns), sized numFrames+1: in-flight depth is bounded by the per-image fences, so numFrames+1 distinct acquire semaphores guarantee the reused one has no pending op (VUID-vkAcquireNextImageKHR-01779). - Drop the steady-state vkQueueWaitIdle; keep it on resize / OUT_OF_DATE / teardown. Add tests/FrameLoopSync: drives the real frame loop against a live Wayland compositor for 60 frames (>> in-flight slots) and asserts the CPU frame counter advanced, the swapchain rotated across multiple images, and the validation layer stayed silent — the load-bearing check, since the old single-pair design only avoided being an active race because the wait-idle masked it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:37:11 +00:00
// ── 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;
2026-03-09 20:10:19 +01:00
std::uint32_t currentBuffer = 0;
2026-01-27 22:34:24 +01:00
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
2026-05-01 23:35:37 +02:00
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;
2026-05-01 23:35:37 +02:00
std::optional<std::array<float, 4>> clearColor;
2026-05-18 02:07:48 +02:00
#else
2026-05-18 04:58:52 +02:00
// 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.
2026-05-18 02:07:48 +02:00
static constexpr std::uint8_t numFrames = 1;
2026-05-18 04:58:52 +02:00
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();
2026-05-18 02:07:48 +02:00
#endif
2026-01-27 22:34:24 +01:00
};
2025-04-16 00:43:33 +02:00
}