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;
|
2026-03-13 01:06:55 +01:00
|
|
|
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;
|
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;
|
|
|
|
|
std::vector<std::chrono::nanoseconds> frameTimes;
|
|
|
|
|
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();
|
2026-03-13 01:06:55 +01:00
|
|
|
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;
|
2026-02-03 21:03:11 +01:00
|
|
|
VkImage images[numFrames];
|
2026-04-05 22:53:59 +02:00
|
|
|
VkImageViewCreateInfo imageViews[numFrames];
|
2026-01-27 22:34:24 +01:00
|
|
|
std::thread thread;
|
2026-02-03 21:03:11 +01:00
|
|
|
VkCommandBuffer drawCmdBuffers[numFrames];
|
2026-01-27 22:34:24 +01:00
|
|
|
VkSubmitInfo submitInfo;
|
|
|
|
|
Semaphores semaphores;
|
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;
|
|
|
|
|
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
|
|
|
}
|