/* 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 #include #include #include #include #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 #include #include #include #include #include #include #include #include #endif #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #include #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 lastFrameBegin; Event onClose; Event onBeforeUpdate; Event 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 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 heldKeys; Event onRawKeyDown; Event onRawKeyHold; Event onRawKeyUp; Event onTextInput; Event onMouseRightClick; Event onMouseLeftClick; Event onMouseRightHold; Event onMouseLeftHold; Event onMouseRightRelease; Event onMouseLeftRelease; Event onMouseMove; Event onMouseEnter; Event onMouseLeave; Event onMouseScroll; Vector currentMousePos; Vector lastMousePos; Vector 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::chrono::nanoseconds>> updateTimings; std::chrono::nanoseconds totalRender; std::chrono::nanoseconds vblank; std::chrono::nanoseconds totalFrame; std::chrono::time_point frameEnd; std::vector 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]; std::thread thread; VkCommandBuffer drawCmdBuffers[numFrames]; VkSubmitInfo submitInfo; Semaphores semaphores; std::uint32_t currentBuffer = 0; VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; std::vector passes; DescriptorHeapVulkan* descriptorHeap = nullptr; std::optional> 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 passes; DescriptorHeapWebGPU* descriptorHeap = nullptr; std::optional> 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 }; }