Window::Render() rebuilt cmdBufInfo, the subresource range, both VkImageMemoryBarrier structs, and presentInfo on the stack every frame even though only the barriers' image (and the acquire barrier's oldLayout) ever change. Hoist them to members initialised once in the constructor; Render() now patches only the two varying fields. presentInfo.pImageIndices/pSwapchains point at the currentBuffer and swapChain members, so they track value changes (including swapchain recreation) automatically. The render-complete wait semaphore is fixed for the window's lifetime, so the per-frame `renderComplete != NULL` check — set once, never cleared — was dead and is removed; the semaphore is wired into presentInfo in the constructor instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
296 lines
No EOL
13 KiB
C++
296 lines
No EOL
13 KiB
C++
/*
|
|
Crafter®.Graphics
|
|
Copyright (C) 2026 Catcrafts®
|
|
catcrafts.net
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License version 3.0 as published by the Free Software Foundation;
|
|
|
|
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
|
|
*/
|
|
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 <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
|
|
|
|
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;
|
|
// 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 backend-side
|
|
// until a whole detent is reached.
|
|
Event<std::uint32_t> onMouseScroll;
|
|
Vector<float, 2> currentMousePos;
|
|
Vector<float, 2> lastMousePos;
|
|
Vector<float, 2> mouseDelta;
|
|
bool mouseLeftHeld = false;
|
|
bool mouseRightHeld = false;
|
|
|
|
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;
|
|
#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
|
|
|
|
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();
|
|
// 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;
|
|
std::vector<std::chrono::nanoseconds> frameTimes;
|
|
void LogTiming();
|
|
#endif
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
HBITMAP cursorBitmap = nullptr;
|
|
HCURSOR cursorHandle = nullptr;
|
|
std::uint16_t cursorSizeX = 0;
|
|
std::uint16_t cursorSizeY = 0;
|
|
#endif
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
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;
|
|
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;
|
|
|
|
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,
|
|
};
|
|
inline static 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();
|
|
|
|
// 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);
|
|
static constexpr std::uint8_t numFrames = 3;
|
|
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
|
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
|
VkFormat colorFormat;
|
|
VkColorSpaceKHR colorSpace;
|
|
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];
|
|
VkSubmitInfo submitInfo;
|
|
// 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). presentInfo.pImageIndices and
|
|
// pSwapchains point at the currentBuffer/swapChain members, so they
|
|
// track value changes (including swapchain recreation) automatically.
|
|
VkCommandBufferBeginInfo cmdBufInfo;
|
|
VkImageMemoryBarrier acquireBarrier;
|
|
VkImageMemoryBarrier presentBarrier;
|
|
VkPresentInfoKHR presentInfo;
|
|
Semaphores semaphores;
|
|
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
|
|
};
|
|
} |