2025-11-25 02:21:06 +01:00
/*
Crafter ® . Graphics
2026-03-09 20:10:19 +01:00
Copyright ( C ) 2026 Catcrafts ®
2025-11-25 02:21:06 +01:00
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
*/
2026-03-09 20:10:19 +01:00
module ;
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <linux/input-event-codes.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 <cassert>
# include <linux/input.h>
# include <sys/mman.h>
# include <wayland-cursor.h>
# include <xkbcommon/xkbcommon.h>
# include <errno.h>
# include <fcntl.h>
# include <print>
# include <wayland-client.h>
# include <wayland-client-protocol.h>
# include <sys/stat.h>
# include <time.h>
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
# include <windows.h>
# include <cassert>
# endif
2026-05-18 02:07:48 +02:00
# ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-09 20:10:19 +01:00
# include "vulkan/vulkan.h"
2026-05-18 02:07:48 +02:00
# endif
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
# include "vulkan/vulkan_wayland.h"
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
# include "vulkan/vulkan_win32.h"
# endif
2026-05-18 02:07:48 +02:00
# ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-05-01 23:35:37 +02:00
# define STB_IMAGE_WRITE_IMPLEMENTATION
# include "../lib/stb_image_write.h"
2026-05-18 02:07:48 +02:00
# endif
2025-11-25 02:21:06 +01:00
module Crafter . Graphics : Window_impl ;
import : Window ;
2026-03-09 20:10:19 +01:00
import : Device ;
2026-05-12 00:24:48 +02:00
import : Gamepad ;
2026-05-18 02:07:48 +02:00
// The Vulkan-typed partitions exist as empty stubs in DOM builds (the
// build system scans `import :X` statements pre-preprocessor, so even
// guarded imports must resolve to a real partition). Their bodies are
// gated under !CRAFTER_GRAPHICS_WINDOW_DOM so DOM compiles see empty
// modules. Cheap.
2026-03-09 20:10:19 +01:00
import : VulkanTransition ;
2026-04-05 22:53:59 +02:00
import : DescriptorHeapVulkan ;
2026-05-01 23:35:37 +02:00
import : RenderPass ;
2025-11-25 02:21:06 +01:00
import std ;
using namespace Crafter ;
2026-05-18 02:07:48 +02:00
# ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void randname ( char * buf ) {
struct timespec ts ;
clock_gettime ( CLOCK_REALTIME , & ts ) ;
long r = ts . tv_nsec ;
for ( int i = 0 ; i < 6 ; + + i ) {
buf [ i ] = ' A ' + ( r & 15 ) + ( r & 16 ) * 2 ;
r > > = 5 ;
}
2025-11-25 18:52:32 +01:00
}
2026-03-09 20:10:19 +01:00
int anonymous_shm_open ( void ) {
char name [ ] = " /hello-wayland-XXXXXX " ;
int retries = 100 ;
do {
randname ( name + strlen ( name ) - 6 ) ;
- - retries ;
// shm_open guarantees that O_CLOEXEC is set
int fd = shm_open ( name , O_RDWR | O_CREAT | O_EXCL , 0600 ) ;
if ( fd > = 0 ) {
shm_unlink ( name ) ;
return fd ;
}
} while ( retries > 0 & & errno = = EEXIST ) ;
return - 1 ;
}
int create_shm_file ( off_t size ) {
int fd = anonymous_shm_open ( ) ;
if ( fd < 0 ) {
return fd ;
}
if ( ftruncate ( fd , size ) < 0 ) {
close ( fd ) ;
return - 1 ;
}
return fd ;
}
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
2026-05-12 00:24:48 +02:00
// Extract the layout-independent raw key code from a WM_KEY* lParam. Bits
// 16-23 hold the PS/2 set-1 scancode byte; bit 24 is the extended-key flag
// (the 0xE0-prefixed variants — RightCtrl, RightAlt, the cursor cluster,
// keypad Enter/Slash, the Windows keys). We pack the extended flag into bit
// 8 of the returned KeyCode so it round-trips with the compile-time
// `Key(CrafterKeys::...)` table in :Keys.
static inline KeyCode KeyCodeFromLParam ( LPARAM lParam ) {
return ( ( KeyCode ) ( ( lParam > > 16 ) & 0xFF ) )
| ( ( ( lParam > > 24 ) & 1u ) < < 8 ) ;
2026-03-09 20:10:19 +01:00
}
// Define a window class name
const char g_szClassName [ ] = " myWindowClass " ;
// Window procedure function that processes messages
LRESULT CALLBACK WndProc ( HWND hwnd , UINT msg , WPARAM wParam , LPARAM lParam ) {
Window * window = nullptr ;
if ( msg = = WM_NCCREATE )
{
CREATESTRUCT * pCreate = reinterpret_cast < CREATESTRUCT * > ( lParam ) ;
window = static_cast < Window * > ( pCreate - > lpCreateParams ) ;
SetWindowLongPtr ( hwnd , GWLP_USERDATA , reinterpret_cast < LONG_PTR > ( window ) ) ;
return TRUE ;
}
else
{
window = reinterpret_cast < Window * > (
GetWindowLongPtr ( hwnd , GWLP_USERDATA )
) ;
2025-12-29 18:56:06 +01:00
}
2026-03-09 20:10:19 +01:00
switch ( msg ) {
case WM_DESTROY : {
PostQuitMessage ( 0 ) ;
break ;
}
2026-05-12 00:24:48 +02:00
case WM_SIZE : {
// SIZE_MINIMIZED reports (0, 0) — Resize() short-circuits, so
// we just propagate the values directly. WM_SIZE fires
// synchronously during a drag-resize loop; the StartSync
// pump runs WndProc between frames, so the swapchain is
// never touched mid-Render.
if ( window ) {
window - > Resize ( LOWORD ( lParam ) , HIWORD ( lParam ) ) ;
}
break ;
}
2026-04-16 23:03:24 +02:00
case WM_KEYDOWN :
case WM_SYSKEYDOWN : { // SYSKEYDOWN catches Alt combos, F10, etc.
2026-05-12 00:24:48 +02:00
KeyCode code = KeyCodeFromLParam ( lParam ) ;
2026-04-16 23:03:24 +02:00
bool isRepeat = ( lParam & ( 1 < < 30 ) ) ! = 0 ;
if ( isRepeat ) {
2026-05-12 00:24:48 +02:00
window - > onRawKeyHold . Invoke ( code ) ;
2026-04-16 23:03:24 +02:00
} else {
2026-05-12 00:24:48 +02:00
window - > heldKeys . insert ( code ) ;
window - > onRawKeyDown . Invoke ( code ) ;
2026-03-09 20:10:19 +01:00
}
break ;
}
2026-04-16 23:03:24 +02:00
case WM_KEYUP :
case WM_SYSKEYUP : {
2026-05-12 00:24:48 +02:00
KeyCode code = KeyCodeFromLParam ( lParam ) ;
window - > heldKeys . erase ( code ) ;
window - > onRawKeyUp . Invoke ( code ) ;
2026-03-09 20:10:19 +01:00
break ;
}
2026-04-16 23:03:24 +02:00
case WM_CHAR : {
// wParam is a UTF-16 code unit. May be a surrogate — buffer until we have a pair.
wchar_t wc = ( wchar_t ) wParam ;
// Filter control characters (backspace=0x08, tab=0x09, enter=0x0D, escape=0x1B, etc.)
if ( wc < 0x20 | | wc = = 0x7f ) break ;
// Handle UTF-16 surrogate pairs (characters outside the BMP, e.g. emoji).
static wchar_t highSurrogate = 0 ;
wchar_t utf16 [ 2 ] ;
int utf16Len ;
if ( wc > = 0xD800 & & wc < = 0xDBFF ) {
// High surrogate — stash it and wait for the low surrogate.
highSurrogate = wc ;
break ;
} else if ( wc > = 0xDC00 & & wc < = 0xDFFF ) {
// Low surrogate — pair with the stashed high surrogate.
if ( highSurrogate = = 0 ) break ; // orphaned low surrogate, ignore
utf16 [ 0 ] = highSurrogate ;
utf16 [ 1 ] = wc ;
utf16Len = 2 ;
highSurrogate = 0 ;
} else {
utf16 [ 0 ] = wc ;
utf16Len = 1 ;
}
// Convert UTF-16 to UTF-8.
char utf8 [ 8 ] ;
int n = WideCharToMultiByte ( CP_UTF8 , 0 , utf16 , utf16Len , utf8 , sizeof ( utf8 ) , nullptr , nullptr ) ;
if ( n > 0 ) {
window - > onTextInput . Invoke ( std : : string ( utf8 , n ) ) ;
}
2026-03-09 20:10:19 +01:00
break ;
}
case WM_LBUTTONDOWN : {
window - > mouseLeftHeld = true ;
window - > onMouseLeftClick . Invoke ( ) ;
break ;
}
case WM_LBUTTONUP : {
window - > mouseLeftHeld = false ;
window - > onMouseLeftRelease . Invoke ( ) ;
break ;
}
case WM_RBUTTONDOWN : {
window - > mouseRightHeld = true ;
window - > onMouseRightClick . Invoke ( ) ;
break ;
}
case WM_RBUTTONUP : {
window - > mouseRightHeld = false ;
window - > onMouseRightRelease . Invoke ( ) ;
break ;
}
2026-04-02 16:52:10 +02:00
case WM_SETCURSOR : {
if ( LOWORD ( lParam ) = = HTCLIENT & & window - > cursorHandle ) {
SetCursor ( window - > cursorHandle ) ;
return TRUE ;
}
break ;
}
2026-03-09 20:10:19 +01:00
default : return DefWindowProc ( hwnd , msg , wParam , lParam ) ;
}
return 0 ;
2025-11-25 18:52:32 +01:00
}
2026-03-09 20:10:19 +01:00
# endif
Window : : Window ( std : : uint32_t width , std : : uint32_t height , const std : : string_view title ) : Window ( width , height ) {
SetTitle ( title ) ;
2025-11-25 18:52:32 +01:00
}
2026-03-09 20:10:19 +01:00
Window : : Window ( std : : uint32_t width , std : : uint32_t height ) : width ( width ) , height ( height ) {
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
Device : : windows . push_back ( this ) ;
surface = wl_compositor_create_surface ( Device : : compositor ) ;
xdgSurface = xdg_wm_base_get_xdg_surface ( Device : : xdgWmBase , surface ) ;
xdgToplevel = xdg_surface_get_toplevel ( xdgSurface ) ;
xdg_surface_add_listener ( xdgSurface , & xdg_surface_listener , this ) ;
xdg_toplevel_add_listener ( xdgToplevel , & xdg_toplevel_listener , this ) ;
wl_surface_commit ( surface ) ;
wp_scale = wp_fractional_scale_manager_v1_get_fractional_scale ( Device : : fractionalScaleManager , surface ) ;
wp_fractional_scale_v1_add_listener ( wp_scale , & wp_fractional_scale_v1_listener , this ) ;
while ( wl_display_dispatch ( Device : : display ) ! = - 1 & & ! configured ) { }
wl_surface_commit ( surface ) ;
zxdg_toplevel_decoration_v1 * decoration = zxdg_decoration_manager_v1_get_toplevel_decoration ( Device : : manager , xdgToplevel ) ;
zxdg_toplevel_decoration_v1_set_mode ( decoration , ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE ) ;
wpViewport = wp_viewporter_get_viewport ( Device : : wpViewporter , surface ) ;
wp_viewport_set_destination ( wpViewport , std : : ceil ( width / scale ) , std : : ceil ( height / scale ) ) ;
wl_surface_commit ( surface ) ;
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Initialize the window class
WNDCLASS wc = { 0 } ;
wc . lpfnWndProc = WndProc ; // Set window procedure
wc . hInstance = GetModuleHandle ( NULL ) ; // Get instance handle
wc . lpszClassName = g_szClassName ;
wc . hCursor = LoadCursor ( NULL , IDC_ARROW ) ;
if ( ! RegisterClass ( & wc ) ) {
MessageBox ( NULL , " Window Class Registration Failed! " , " Error " , MB_ICONERROR ) ;
}
RECT rc = { 0 , 0 , static_cast < LONG > ( width ) , static_cast < LONG > ( height ) } ;
AdjustWindowRect ( & rc , WS_OVERLAPPEDWINDOW , FALSE ) ;
HWND hwnd = CreateWindowEx (
0 ,
g_szClassName ,
" " ,
WS_OVERLAPPEDWINDOW ,
CW_USEDEFAULT , CW_USEDEFAULT ,
rc . right - rc . left ,
rc . bottom - rc . top ,
NULL , NULL , wc . hInstance , this
) ;
if ( hwnd = = NULL ) {
MessageBox ( NULL , " Window Creation Failed! " , " Error " , MB_ICONERROR ) ;
}
// Show the window
ShowWindow ( hwnd , SW_SHOWNORMAL ) ;
UpdateWindow ( hwnd ) ;
MSG msg ;
while ( PeekMessage ( & msg , NULL , 0 , 0 , PM_REMOVE ) ) {
TranslateMessage ( & msg ) ;
DispatchMessage ( & msg ) ;
}
VkWin32SurfaceCreateInfoKHR createInfo = { } ;
createInfo . sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR ;
createInfo . hinstance = wc . hInstance ;
createInfo . hwnd = hwnd ;
Device : : CheckVkResult ( vkCreateWin32SurfaceKHR ( Device : : instance , & createInfo , NULL , & vulkanSurface ) ) ;
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
VkWaylandSurfaceCreateInfoKHR createInfo = { } ;
createInfo . sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR ;
createInfo . display = Device : : display ;
createInfo . surface = surface ;
Device : : CheckVkResult ( vkCreateWaylandSurfaceKHR ( Device : : instance , & createInfo , NULL , & vulkanSurface ) ) ;
# endif
// Get list of supported surface formats
std : : uint32_t formatCount ;
Device : : CheckVkResult ( vkGetPhysicalDeviceSurfaceFormatsKHR ( Device : : physDevice , vulkanSurface , & formatCount , NULL ) ) ;
assert ( formatCount > 0 ) ;
std : : vector < VkSurfaceFormatKHR > surfaceFormats ( formatCount ) ;
Device : : CheckVkResult ( vkGetPhysicalDeviceSurfaceFormatsKHR ( Device : : physDevice , vulkanSurface , & formatCount , surfaceFormats . data ( ) ) ) ;
// We want to get a format that best suits our needs, so we try to get one from a set of preferred formats
// Initialize the format to the first one returned by the implementation in case we can't find one of the preffered formats
VkSurfaceFormatKHR selectedFormat = surfaceFormats [ 0 ] ;
std : : vector < VkFormat > preferredImageFormats = {
VK_FORMAT_R8G8B8A8_UNORM ,
VK_FORMAT_B8G8R8A8_UNORM
} ;
for ( auto & availableFormat : surfaceFormats ) {
if ( std : : find ( preferredImageFormats . begin ( ) , preferredImageFormats . end ( ) , availableFormat . format ) ! = preferredImageFormats . end ( ) ) {
selectedFormat = availableFormat ;
break ;
}
}
colorFormat = selectedFormat . format ;
colorSpace = selectedFormat . colorSpace ;
CreateSwapchain ( ) ;
VkCommandBufferAllocateInfo cmdBufAllocateInfo { } ;
cmdBufAllocateInfo . sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO ;
cmdBufAllocateInfo . commandPool = Device : : commandPool ;
cmdBufAllocateInfo . level = VK_COMMAND_BUFFER_LEVEL_PRIMARY ;
cmdBufAllocateInfo . commandBufferCount = numFrames ;
Device : : CheckVkResult ( vkAllocateCommandBuffers ( Device : : device , & cmdBufAllocateInfo , drawCmdBuffers ) ) ;
VkSemaphoreCreateInfo semaphoreCreateInfo { } ;
semaphoreCreateInfo . sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO ;
Device : : CheckVkResult ( vkCreateSemaphore ( Device : : device , & semaphoreCreateInfo , nullptr , & semaphores . presentComplete ) ) ;
Device : : CheckVkResult ( vkCreateSemaphore ( Device : : device , & semaphoreCreateInfo , nullptr , & semaphores . renderComplete ) ) ;
// Set up submit info structure
// Semaphores will stay the same during application lifetime
// Command buffer submission info is set by each example
submitInfo . sType = VK_STRUCTURE_TYPE_SUBMIT_INFO ;
submitInfo . pWaitDstStageMask = & submitPipelineStages ;
submitInfo . waitSemaphoreCount = 1 ;
submitInfo . pWaitSemaphores = & semaphores . presentComplete ;
submitInfo . signalSemaphoreCount = 1 ;
submitInfo . pSignalSemaphores = & semaphores . renderComplete ;
submitInfo . pNext = VK_NULL_HANDLE ;
2026-03-10 03:05:10 +01:00
lastMousePos = { 0 , 0 } ;
mouseDelta = { 0 , 0 } ;
currentMousePos = { 0 , 0 } ;
2025-11-25 18:52:32 +01:00
}
2025-11-25 19:43:40 +01:00
2026-05-12 00:24:48 +02:00
void Window : : Resize ( std : : uint32_t newWidth , std : : uint32_t newHeight ) {
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
// sometimes echoes the current size in a configure.
if ( newWidth = = 0 | | newHeight = = 0 ) return ;
if ( newWidth = = width & & newHeight = = height ) return ;
// Win32 fires WM_SIZE synchronously inside CreateWindowEx (before the
// constructor's CreateSwapchain). Defer the first resize to that
// CreateSwapchain call instead of trying to recreate a non-existent
// swapchain.
if ( swapChain = = VK_NULL_HANDLE ) {
width = newWidth ;
height = newHeight ;
return ;
}
width = newWidth ;
height = newHeight ;
// Caller (configure handler / WM_SIZE) runs between frames, but be
// defensive: ensure no in-flight commands reference the old swapchain.
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if ( wpViewport ) {
wp_viewport_set_destination ( wpViewport ,
static_cast < int > ( std : : ceil ( width / scale ) ) ,
static_cast < int > ( std : : ceil ( height / scale ) ) ) ;
}
# endif
RecreateSwapchainAndImages ( ) ;
onResize . Invoke ( ) ;
}
void Window : : RecreateSwapchainAndImages ( ) {
CreateSwapchain ( ) ;
// CreateSwapchain leaves new swapchain images in VK_IMAGE_LAYOUT_UNDEFINED.
// Render() barriers from PRESENT_SRC_KHR, so transition them now to
// match. Mirrors the StartInit logic, on a one-shot command buffer.
{
VkCommandBufferAllocateInfo cba {
. sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO ,
. commandPool = Device : : commandPool ,
. level = VK_COMMAND_BUFFER_LEVEL_PRIMARY ,
. commandBufferCount = 1 ,
} ;
VkCommandBuffer cmd = VK_NULL_HANDLE ;
Device : : CheckVkResult ( vkAllocateCommandBuffers ( Device : : device , & cba , & cmd ) ) ;
VkCommandBufferBeginInfo cbi {
. sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO ,
. flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT ,
} ;
Device : : CheckVkResult ( vkBeginCommandBuffer ( cmd , & cbi ) ) ;
VkImageSubresourceRange range {
. aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ,
. baseMipLevel = 0 ,
. levelCount = VK_REMAINING_MIP_LEVELS ,
. baseArrayLayer = 0 ,
. layerCount = VK_REMAINING_ARRAY_LAYERS ,
} ;
for ( std : : uint32_t i = 0 ; i < numFrames ; i + + ) {
image_layout_transition ( cmd ,
images [ i ] ,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT ,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT ,
0 , 0 ,
VK_IMAGE_LAYOUT_UNDEFINED ,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
range ) ;
}
Device : : CheckVkResult ( vkEndCommandBuffer ( cmd ) ) ;
VkSubmitInfo si {
. sType = VK_STRUCTURE_TYPE_SUBMIT_INFO ,
. commandBufferCount = 1 ,
. pCommandBuffers = & cmd ,
} ;
Device : : CheckVkResult ( vkQueueSubmit ( Device : : queue , 1 , & si , VK_NULL_HANDLE ) ) ;
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
vkFreeCommandBuffers ( Device : : device , Device : : commandPool , 1 , & cmd ) ;
}
}
2026-03-09 20:10:19 +01:00
void Window : : SetTitle ( const std : : string_view title ) {
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
xdg_toplevel_set_title ( xdgToplevel , title . data ( ) ) ;
# endif
}
2026-05-02 21:08:20 +02:00
void Window : : SetCursorImage ( std : : uint16_t width , std : : uint16_t height ,
std : : uint16_t hotspotX , std : : uint16_t hotspotY ,
const std : : uint8_t * pixels ) {
2026-03-12 01:07:46 +01:00
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
2026-05-02 21:08:20 +02:00
if ( width = = 0 | | height = = 0 | | pixels = = nullptr ) {
SetDefaultCursor ( ) ;
return ;
}
if ( cursorSurface = = nullptr ) {
2026-03-12 01:07:46 +01:00
cursorSurface = wl_compositor_create_surface ( Device : : compositor ) ;
2026-05-02 21:08:20 +02:00
}
int stride = width * 4 ;
int size = stride * height ;
// Reuse the existing mmap+buffer if the size is unchanged; otherwise
// tear down and re-allocate.
if ( cursorWlBuffer ! = nullptr & &
cursorBufferOldSize = = static_cast < std : : uint32_t > ( size ) ) {
// size unchanged — keep the buffer and mmap.
2026-03-12 01:07:46 +01:00
} else {
2026-05-02 21:08:20 +02:00
if ( cursorMmap_ ) {
munmap ( cursorMmap_ , cursorBufferOldSize ) ;
cursorMmap_ = nullptr ;
}
if ( cursorWlBuffer ) {
wl_buffer_destroy ( cursorWlBuffer ) ;
cursorWlBuffer = nullptr ;
}
int fd = create_shm_file ( size ) ;
if ( fd < 0 ) {
throw std : : runtime_error ( std : : format (
" Window::SetCursorImage: shm allocation for {}B failed " , size ) ) ;
}
void * mapped = mmap ( nullptr , size , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0 ) ;
if ( mapped = = MAP_FAILED ) {
close ( fd ) ;
throw std : : runtime_error ( " Window::SetCursorImage: mmap failed " ) ;
}
cursorMmap_ = static_cast < std : : uint8_t * > ( mapped ) ;
wl_shm_pool * pool = wl_shm_create_pool ( Device : : shm , fd , size ) ;
cursorWlBuffer = wl_shm_pool_create_buffer (
pool , 0 , width , height , stride , WL_SHM_FORMAT_ARGB8888 ) ;
wl_shm_pool_destroy ( pool ) ;
close ( fd ) ;
cursorBufferOldSize = static_cast < std : : uint32_t > ( size ) ;
}
// Convert the user's straight-alpha RGBA8 pixels into the compositor's
// expected premultiplied BGRA8 (= ARGB8888 little-endian byte order).
for ( int i = 0 ; i < width * height ; + + i ) {
std : : uint8_t r = pixels [ i * 4 + 0 ] ;
std : : uint8_t g = pixels [ i * 4 + 1 ] ;
std : : uint8_t b = pixels [ i * 4 + 2 ] ;
std : : uint8_t a = pixels [ i * 4 + 3 ] ;
cursorMmap_ [ i * 4 + 0 ] = static_cast < std : : uint8_t > ( ( b * a ) / 255 ) ;
cursorMmap_ [ i * 4 + 1 ] = static_cast < std : : uint8_t > ( ( g * a ) / 255 ) ;
cursorMmap_ [ i * 4 + 2 ] = static_cast < std : : uint8_t > ( ( r * a ) / 255 ) ;
cursorMmap_ [ i * 4 + 3 ] = a ;
}
cursorHotspotX_ = hotspotX ;
cursorHotspotY_ = hotspotY ;
wl_surface_attach ( cursorSurface , cursorWlBuffer , 0 , 0 ) ;
wl_surface_damage ( cursorSurface , 0 , 0 , width , height ) ;
wl_surface_commit ( cursorSurface ) ;
// If the pointer is currently inside our window, re-apply the cursor
// so the new hotspot takes effect immediately. Otherwise the next
// pointer-enter event will pick it up.
if ( Device : : wlPointer & & Device : : focusedWindow = = this & & lastPointerSerial_ ) {
wl_pointer_set_cursor ( Device : : wlPointer , lastPointerSerial_ ,
cursorSurface , hotspotX , hotspotY ) ;
}
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Win32 cursor support is not implemented for the v2 Window.
( void ) width ; ( void ) height ; ( void ) hotspotX ; ( void ) hotspotY ; ( void ) pixels ;
# endif
}
void Window : : SetDefaultCursor ( ) {
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if ( cursorMmap_ ) {
munmap ( cursorMmap_ , cursorBufferOldSize ) ;
cursorMmap_ = nullptr ;
}
if ( cursorWlBuffer ) {
2026-03-12 01:07:46 +01:00
wl_buffer_destroy ( cursorWlBuffer ) ;
2026-05-02 21:08:20 +02:00
cursorWlBuffer = nullptr ;
2026-03-12 01:07:46 +01:00
}
2026-05-02 21:08:20 +02:00
if ( cursorSurface ) {
wl_surface_destroy ( cursorSurface ) ;
cursorSurface = nullptr ;
2026-04-02 16:52:10 +02:00
}
2026-05-02 21:08:20 +02:00
cursorBufferOldSize = 0 ;
cursorHotspotX_ = 0 ;
cursorHotspotY_ = 0 ;
// Tell the compositor to drop our cursor surface — passing nullptr
// makes it fall back to the system default.
if ( Device : : wlPointer & & Device : : focusedWindow = = this & & lastPointerSerial_ ) {
wl_pointer_set_cursor ( Device : : wlPointer , lastPointerSerial_ , nullptr , 0 , 0 ) ;
2026-04-02 16:52:10 +02:00
}
# endif
2026-03-12 01:07:46 +01:00
}
2026-03-09 20:10:19 +01:00
void Window : : StartSync ( ) {
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
while ( open & & wl_display_dispatch ( Device : : display ) ! = - 1 ) {
2026-05-12 00:24:48 +02:00
Gamepad : : Tick ( ) ;
2026-03-13 01:06:55 +01:00
onBeforeUpdate . Invoke ( ) ;
2026-03-09 20:10:19 +01:00
}
# endif
2026-03-10 02:47:28 +01:00
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
2026-03-09 20:10:19 +01:00
while ( open ) {
MSG msg ;
while ( PeekMessage ( & msg , NULL , 0 , 0 , PM_REMOVE ) ) {
TranslateMessage ( & msg ) ;
DispatchMessage ( & msg ) ;
}
2026-05-12 00:24:48 +02:00
Gamepad : : Tick ( ) ;
2026-03-13 01:06:55 +01:00
onBeforeUpdate . Invoke ( ) ;
2026-03-09 20:10:19 +01:00
if ( updating ) {
Update ( ) ;
}
}
# endif
}
void Window : : StartUpdate ( ) {
lastFrameBegin = std : : chrono : : high_resolution_clock : : now ( ) ;
updating = true ;
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
cb = wl_surface_frame ( surface ) ;
wl_callback_add_listener ( cb , & wl_callback_listener , this ) ;
# endif
}
void Window : : StopUpdate ( ) {
updating = false ;
}
2026-03-09 21:00:58 +01:00
std : : chrono : : time_point < std : : chrono : : high_resolution_clock > startTime ;
2026-03-09 20:10:19 +01:00
void Window : : Update ( ) {
2026-03-09 21:00:58 +01:00
startTime = std : : chrono : : high_resolution_clock : : now ( ) ;
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_TIMING
2026-03-09 21:00:58 +01:00
vblank = duration_cast < std : : chrono : : milliseconds > ( startTime - frameEnd ) ;
2026-03-09 20:10:19 +01:00
# endif
2026-03-09 21:50:24 +01:00
mouseDelta = { currentMousePos . x - lastMousePos . x , currentMousePos . y - lastMousePos . y } ;
2026-03-09 22:15:36 +01:00
currentFrameTime = { startTime , startTime - lastFrameBegin } ;
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_TIMING
auto renderStart = std : : chrono : : high_resolution_clock : : now ( ) ;
renderTimings . clear ( ) ;
# endif
Render ( ) ;
# ifdef CRAFTER_TIMING
auto renderEnd = std : : chrono : : high_resolution_clock : : now ( ) ;
totalRender = renderEnd - renderStart ;
# endif
2026-03-09 21:50:24 +01:00
lastMousePos = currentMousePos ;
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_TIMING
frameEnd = std : : chrono : : high_resolution_clock : : now ( ) ;
frameTimes . push_back ( totalUpdate + totalRender ) ;
// Keep only the last 100 frame times
if ( frameTimes . size ( ) > 100 ) {
frameTimes . erase ( frameTimes . begin ( ) ) ;
}
# endif
2026-03-09 21:00:58 +01:00
lastFrameBegin = startTime ;
2026-03-09 20:10:19 +01:00
}
void Window : : Render ( ) {
2026-05-12 00:24:48 +02:00
// Acquire the next image from the swap chain. If the surface has
// changed size out from under us (compositor/Win32 resize delivered
// between Render calls), recreate and retry once.
{
VkResult acquire = vkAcquireNextImageKHR ( Device : : device , swapChain , UINT64_MAX ,
semaphores . presentComplete , ( VkFence ) nullptr , & currentBuffer ) ;
if ( acquire = = VK_ERROR_OUT_OF_DATE_KHR ) {
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
RecreateSwapchainAndImages ( ) ;
onResize . Invoke ( ) ;
acquire = vkAcquireNextImageKHR ( Device : : device , swapChain , UINT64_MAX ,
semaphores . presentComplete , ( VkFence ) nullptr , & currentBuffer ) ;
}
if ( acquire ! = VK_SUBOPTIMAL_KHR ) {
Device : : CheckVkResult ( acquire ) ;
}
}
2026-03-09 20:10:19 +01:00
submitInfo . commandBufferCount = 1 ;
submitInfo . pCommandBuffers = & drawCmdBuffers [ currentBuffer ] ;
VkCommandBufferBeginInfo cmdBufInfo { } ;
cmdBufInfo . sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO ;
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( vkBeginCommandBuffer ( drawCmdBuffers [ currentBuffer ] , & cmdBufInfo ) ) ;
2026-03-09 20:10:19 +01:00
VkImageSubresourceRange range { } ;
range . aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ;
range . baseMipLevel = 0 ;
range . levelCount = VK_REMAINING_MIP_LEVELS ;
range . baseArrayLayer = 0 ;
range . layerCount = VK_REMAINING_ARRAY_LAYERS ;
VkImageMemoryBarrier image_memory_barrier {
. sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER ,
. srcAccessMask = 0 ,
2026-05-01 23:35:37 +02:00
. dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT ,
2026-03-09 20:10:19 +01:00
. oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
. newLayout = VK_IMAGE_LAYOUT_GENERAL ,
. srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. image = images [ currentBuffer ] ,
. subresourceRange = range
} ;
2026-05-01 23:35:37 +02:00
vkCmdPipelineBarrier ( drawCmdBuffers [ currentBuffer ] , VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT , VK_PIPELINE_STAGE_ALL_COMMANDS_BIT , 0 , 0 , nullptr , 0 , nullptr , 1 , & image_memory_barrier ) ;
2026-03-09 20:10:19 +01:00
2026-05-02 00:03:24 +02:00
// Synthesise key-repeat events before listeners run, so the focused
// widget's OnTextInput / OnKeyDown sees them in the same frame.
Device : : TickKeyRepeats ( ) ;
2026-05-05 23:49:29 +02:00
// Bind the descriptor heaps BEFORE the user's update event fires.
// Any compute work the update lambda records (e.g. physics dispatches)
// needs the heaps bound at execution time; recording order in the cmd
// buffer dictates GPU execution order, so the bind must come first.
// Pass-side dispatches still run with the same heaps bound — moving
// the bind earlier doesn't change anything for them.
2026-05-01 23:35:37 +02:00
if ( descriptorHeap ) {
VkBindHeapInfoEXT resourceHeapInfo = {
. sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT ,
. heapRange = {
. address = descriptorHeap - > resourceHeap [ currentBuffer ] . address ,
. size = static_cast < std : : uint32_t > ( descriptorHeap - > resourceHeap [ currentBuffer ] . size )
} ,
. reservedRangeOffset = ( descriptorHeap - > resourceHeap [ currentBuffer ] . size - Device : : descriptorHeapProperties . minResourceHeapReservedRange ) & ~ ( Device : : descriptorHeapProperties . imageDescriptorAlignment - 1 ) ,
. reservedRangeSize = Device : : descriptorHeapProperties . minResourceHeapReservedRange
} ;
Device : : vkCmdBindResourceHeapEXT ( drawCmdBuffers [ currentBuffer ] , & resourceHeapInfo ) ;
2026-03-09 20:10:19 +01:00
2026-05-01 23:35:37 +02:00
VkBindHeapInfoEXT samplerHeapInfo = {
. sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT ,
. heapRange = {
. address = descriptorHeap - > samplerHeap [ currentBuffer ] . address ,
. size = static_cast < std : : uint32_t > ( descriptorHeap - > samplerHeap [ currentBuffer ] . size )
} ,
. reservedRangeOffset = descriptorHeap - > samplerHeap [ currentBuffer ] . size - Device : : descriptorHeapProperties . minSamplerHeapReservedRange ,
. reservedRangeSize = Device : : descriptorHeapProperties . minSamplerHeapReservedRange
} ;
Device : : vkCmdBindSamplerHeapEXT ( drawCmdBuffers [ currentBuffer ] , & samplerHeapInfo ) ;
}
2026-04-05 22:53:59 +02:00
2026-05-05 23:49:29 +02:00
onUpdate . Invoke ( { startTime , startTime - lastFrameBegin } ) ;
# ifdef CRAFTER_TIMING
totalUpdate = std : : chrono : : nanoseconds ( 0 ) ;
updateTimings . clear ( ) ;
for ( const std : : pair < const EventListener < FrameTime > * , std : : chrono : : nanoseconds > & entry : onUpdate . listenerTimes ) {
updateTimings . push_back ( entry ) ;
totalUpdate + = entry . second ;
}
# endif
2026-05-01 23:35:37 +02:00
// Note: vkCmdClearColorImage is unavailable here — the swapchain is
// created with VK_IMAGE_USAGE_STORAGE_BIT only (no TRANSFER_DST_BIT).
// Passes that need a background should write one explicitly (UIScene
// exposes a `background()` setter for this purpose).
( void ) clearColor ;
for ( std : : size_t i = 0 ; i < passes . size ( ) ; + + i ) {
passes [ i ] - > Record ( drawCmdBuffers [ currentBuffer ] , currentBuffer , * this ) ;
if ( i + 1 < passes . size ( ) ) {
VkMemoryBarrier mb {
. sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER ,
. srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT ,
. dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT ,
} ;
vkCmdPipelineBarrier ( drawCmdBuffers [ currentBuffer ] , VK_PIPELINE_STAGE_ALL_COMMANDS_BIT , VK_PIPELINE_STAGE_ALL_COMMANDS_BIT , 0 , 1 , & mb , 0 , nullptr , 0 , nullptr ) ;
}
}
2026-03-09 20:10:19 +01:00
VkImageMemoryBarrier image_memory_barrier2 {
. sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER ,
. srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT ,
. dstAccessMask = 0 ,
. oldLayout = VK_IMAGE_LAYOUT_GENERAL ,
. newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
. srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. image = images [ currentBuffer ] ,
. subresourceRange = range
} ;
2026-05-01 23:35:37 +02:00
vkCmdPipelineBarrier ( drawCmdBuffers [ currentBuffer ] , VK_PIPELINE_STAGE_ALL_COMMANDS_BIT , VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT , 0 , 0 , nullptr , 0 , nullptr , 1 , & image_memory_barrier2 ) ;
2026-03-09 20:10:19 +01:00
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( vkEndCommandBuffer ( drawCmdBuffers [ currentBuffer ] ) ) ;
2026-03-09 20:10:19 +01:00
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( vkQueueSubmit ( Device : : queue , 1 , & submitInfo , VK_NULL_HANDLE ) ) ;
2026-03-09 20:10:19 +01:00
VkPresentInfoKHR presentInfo = { } ;
presentInfo . sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR ;
presentInfo . pNext = NULL ;
presentInfo . swapchainCount = 1 ;
presentInfo . pSwapchains = & swapChain ;
presentInfo . pImageIndices = & currentBuffer ;
// Check if a wait semaphore has been specified to wait for before presenting the image
if ( semaphores . renderComplete ! = VK_NULL_HANDLE )
{
presentInfo . pWaitSemaphores = & semaphores . renderComplete ;
presentInfo . waitSemaphoreCount = 1 ;
}
2026-03-09 21:14:53 +01:00
VkResult result = vkQueuePresentKHR ( Device : : queue , & presentInfo ) ;
2026-05-12 00:24:48 +02:00
if ( result = = VK_SUBOPTIMAL_KHR | | result = = VK_ERROR_OUT_OF_DATE_KHR ) {
// Surface size changed mid-present. Drain the queue, rebuild the
// swapchain, and let dependents (descriptors holding old image
// handles) re-bind via onResize before the next frame.
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
RecreateSwapchainAndImages ( ) ;
onResize . Invoke ( ) ;
2026-03-09 20:10:19 +01:00
} else {
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( result ) ;
2026-03-09 20:10:19 +01:00
}
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
2025-11-25 19:43:40 +01:00
}
2025-11-25 23:29:48 +01:00
# ifdef CRAFTER_TIMING
void Window : : LogTiming ( ) {
std : : cout < < std : : format ( " Update: {} " , duration_cast < std : : chrono : : milliseconds > ( totalUpdate ) ) < < std : : endl ;
for ( const std : : pair < const EventListener < FrameTime > * , std : : chrono : : nanoseconds > & entry : updateTimings ) {
std : : cout < < std : : format ( " \t {} {} " , reinterpret_cast < const void * > ( entry . first ) , duration_cast < std : : chrono : : microseconds > ( entry . second ) ) < < std : : endl ;
}
std : : cout < < std : : format ( " Render: {} " , duration_cast < std : : chrono : : milliseconds > ( totalRender ) ) < < std : : endl ;
2026-03-09 20:10:19 +01:00
for ( const std : : tuple < const RenderingElement * , std : : uint32_t , std : : uint32_t , std : : chrono : : nanoseconds > & entry : renderer . renderTimings ) {
2025-11-25 23:29:48 +01:00
std : : cout < < std : : format ( " \t {} {}x{} {} " , reinterpret_cast < const void * > ( std : : get < 0 > ( entry ) ) , std : : get < 1 > ( entry ) , std : : get < 2 > ( entry ) , duration_cast < std : : chrono : : microseconds > ( std : : get < 3 > ( entry ) ) ) < < std : : endl ;
}
2025-11-25 23:36:43 +01:00
std : : cout < < std : : format ( " Total: {} " , duration_cast < std : : chrono : : milliseconds > ( totalUpdate + totalRender ) ) < < std : : endl ;
2025-11-25 23:29:48 +01:00
std : : cout < < std : : format ( " Vblank: {} " , duration_cast < std : : chrono : : milliseconds > ( vblank ) ) < < std : : endl ;
2025-11-25 23:36:43 +01:00
// Add 100-frame average and min-max timing info
if ( ! frameTimes . empty ( ) ) {
// Calculate average
std : : chrono : : nanoseconds sum ( 0 ) ;
for ( const auto & frameTime : frameTimes ) {
sum + = frameTime ;
}
auto average = sum / frameTimes . size ( ) ;
// Find min and max
auto min = frameTimes . front ( ) ;
auto max = frameTimes . front ( ) ;
for ( const auto & frameTime : frameTimes ) {
if ( frameTime < min ) min = frameTime ;
if ( frameTime > max ) max = frameTime ;
}
std : : cout < < std : : format ( " Last 100 Frame Times - Avg: {}, Min: {}, Max: {} " ,
duration_cast < std : : chrono : : milliseconds > ( average ) ,
duration_cast < std : : chrono : : milliseconds > ( min ) ,
duration_cast < std : : chrono : : milliseconds > ( max ) ) < < std : : endl ;
}
2025-11-25 23:29:48 +01:00
}
2025-11-26 18:48:58 +01:00
# endif
2026-03-09 20:10:19 +01:00
void Window : : CreateSwapchain ( )
{
// Store the current swap chain handle so we can use it later on to ease up recreation
VkSwapchainKHR oldSwapchain = swapChain ;
2025-11-26 18:48:58 +01:00
2026-03-09 20:10:19 +01:00
// Get physical device surface properties and formats
VkSurfaceCapabilitiesKHR surfCaps ;
Device : : CheckVkResult ( vkGetPhysicalDeviceSurfaceCapabilitiesKHR ( Device : : physDevice , vulkanSurface , & surfCaps ) ) ;
2025-11-26 18:48:58 +01:00
2026-03-09 20:10:19 +01:00
VkExtent2D swapchainExtent = { } ;
// If width (and height) equals the special value 0xFFFFFFFF, the size of the surface will be set by the swapchain
if ( surfCaps . currentExtent . width = = ( uint32_t ) - 1 )
{
// If the surface size is undefined, the size is set to the size of the images requested
swapchainExtent . width = width ;
swapchainExtent . height = height ;
}
else
{
// If the surface size is defined, the swap chain size must match
swapchainExtent = surfCaps . currentExtent ;
width = surfCaps . currentExtent . width ;
height = surfCaps . currentExtent . height ;
}
2025-11-26 20:15:25 +01:00
2025-11-26 23:36:08 +01:00
2026-03-09 20:10:19 +01:00
// Select a present mode for the swapchain
uint32_t presentModeCount ;
Device : : CheckVkResult ( vkGetPhysicalDeviceSurfacePresentModesKHR ( Device : : physDevice , vulkanSurface , & presentModeCount , NULL ) ) ;
assert ( presentModeCount > 0 ) ;
2025-11-26 23:36:08 +01:00
2026-03-09 20:10:19 +01:00
std : : vector < VkPresentModeKHR > presentModes ( presentModeCount ) ;
Device : : CheckVkResult ( vkGetPhysicalDeviceSurfacePresentModesKHR ( Device : : physDevice , vulkanSurface , & presentModeCount , presentModes . data ( ) ) ) ;
2025-11-26 23:36:08 +01:00
2026-03-09 20:10:19 +01:00
// The VK_PRESENT_MODE_FIFO_KHR mode must always be present as per spec
// This mode waits for the vertical blank ("v-sync")
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR ;
// Find the transformation of the surface
VkSurfaceTransformFlagsKHR preTransform ;
if ( surfCaps . supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR )
{
// We prefer a non-rotated transform
preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ;
}
else
{
preTransform = surfCaps . currentTransform ;
}
// Find a supported composite alpha format (not all devices support alpha opaque)
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR ;
// Simply select the first composite alpha format available
std : : vector < VkCompositeAlphaFlagBitsKHR > compositeAlphaFlags = {
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR ,
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR ,
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR ,
VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR ,
} ;
for ( auto & compositeAlphaFlag : compositeAlphaFlags ) {
if ( surfCaps . supportedCompositeAlpha & compositeAlphaFlag ) {
compositeAlpha = compositeAlphaFlag ;
break ;
} ;
}
VkSwapchainCreateInfoKHR swapchainCI = { } ;
swapchainCI . sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR ;
swapchainCI . surface = vulkanSurface ;
swapchainCI . minImageCount = numFrames ;
swapchainCI . imageFormat = colorFormat ;
swapchainCI . imageColorSpace = colorSpace ;
swapchainCI . imageExtent = { swapchainExtent . width , swapchainExtent . height } ;
2026-05-01 23:35:37 +02:00
swapchainCI . imageUsage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT ;
2026-03-09 20:10:19 +01:00
swapchainCI . preTransform = ( VkSurfaceTransformFlagBitsKHR ) preTransform ;
swapchainCI . imageArrayLayers = 1 ;
swapchainCI . imageSharingMode = VK_SHARING_MODE_EXCLUSIVE ;
swapchainCI . queueFamilyIndexCount = 0 ;
swapchainCI . presentMode = swapchainPresentMode ;
// Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images
swapchainCI . oldSwapchain = oldSwapchain ;
// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area
swapchainCI . clipped = VK_TRUE ;
swapchainCI . compositeAlpha = compositeAlpha ;
Device : : CheckVkResult ( vkCreateSwapchainKHR ( Device : : device , & swapchainCI , nullptr , & swapChain ) ) ;
// If an existing swap chain is re-created, destroy the old swap chain and the ressources owned by the application (image views, images are owned by the swap chain)
if ( oldSwapchain ! = VK_NULL_HANDLE ) {
vkDestroySwapchainKHR ( Device : : device , oldSwapchain , nullptr ) ;
}
uint32_t imageCount { 0 } ;
Device : : CheckVkResult ( vkGetSwapchainImagesKHR ( Device : : device , swapChain , & imageCount , nullptr ) ) ;
// Get the swap chain images
Device : : CheckVkResult ( vkGetSwapchainImagesKHR ( Device : : device , swapChain , & imageCount , images ) ) ;
2026-04-05 22:53:59 +02:00
for ( std : : uint8_t i = 0 ; i < numFrames ; i + + ) {
imageViews [ i ] = {
. sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO ,
. flags = 0 ,
. image = images [ i ] ,
. viewType = VK_IMAGE_VIEW_TYPE_2D ,
. format = colorFormat ,
. components = {
VK_COMPONENT_SWIZZLE_R ,
VK_COMPONENT_SWIZZLE_G ,
VK_COMPONENT_SWIZZLE_B ,
VK_COMPONENT_SWIZZLE_A
} ,
. subresourceRange = {
. aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ,
. baseMipLevel = 0 ,
. levelCount = 1 ,
. baseArrayLayer = 0 ,
. layerCount = 1 ,
} ,
} ;
2026-03-09 20:10:19 +01:00
}
}
VkCommandBuffer Window : : StartInit ( ) {
VkCommandBufferBeginInfo cmdBufInfo { } ;
cmdBufInfo . sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO ;
Device : : CheckVkResult ( vkBeginCommandBuffer ( drawCmdBuffers [ currentBuffer ] , & cmdBufInfo ) ) ;
VkImageSubresourceRange range { } ;
range . aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ;
range . baseMipLevel = 0 ;
range . levelCount = VK_REMAINING_MIP_LEVELS ;
range . baseArrayLayer = 0 ;
range . layerCount = VK_REMAINING_ARRAY_LAYERS ;
for ( std : : uint32_t i = 0 ; i < numFrames ; i + + ) {
image_layout_transition ( drawCmdBuffers [ currentBuffer ] ,
images [ i ] ,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT ,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT ,
0 ,
0 ,
VK_IMAGE_LAYOUT_UNDEFINED ,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
range
) ;
}
return drawCmdBuffers [ currentBuffer ] ;
}
void Window : : FinishInit ( ) {
VkSubmitInfo submitInfo { } ;
submitInfo . sType = VK_STRUCTURE_TYPE_SUBMIT_INFO ;
submitInfo . commandBufferCount = 1 ;
submitInfo . pCommandBuffers = & drawCmdBuffers [ currentBuffer ] ;
Device : : CheckVkResult ( vkEndCommandBuffer ( drawCmdBuffers [ currentBuffer ] ) ) ;
Device : : CheckVkResult ( vkQueueSubmit ( Device : : queue , 1 , & submitInfo , VK_NULL_HANDLE ) ) ;
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
}
2026-03-13 01:06:55 +01:00
VkCommandBuffer Window : : GetCmd ( ) {
VkCommandBufferBeginInfo cmdBufInfo { } ;
cmdBufInfo . sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO ;
Device : : CheckVkResult ( vkBeginCommandBuffer ( drawCmdBuffers [ currentBuffer ] , & cmdBufInfo ) ) ;
VkImageSubresourceRange range { } ;
range . aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ;
range . baseMipLevel = 0 ;
range . levelCount = VK_REMAINING_MIP_LEVELS ;
range . baseArrayLayer = 0 ;
range . layerCount = VK_REMAINING_ARRAY_LAYERS ;
return drawCmdBuffers [ currentBuffer ] ;
}
void Window : : EndCmd ( VkCommandBuffer cmd ) {
VkSubmitInfo submitInfo { } ;
submitInfo . sType = VK_STRUCTURE_TYPE_SUBMIT_INFO ;
submitInfo . commandBufferCount = 1 ;
submitInfo . pCommandBuffers = & drawCmdBuffers [ currentBuffer ] ;
Device : : CheckVkResult ( vkEndCommandBuffer ( drawCmdBuffers [ currentBuffer ] ) ) ;
Device : : CheckVkResult ( vkQueueSubmit ( Device : : queue , 1 , & submitInfo , VK_NULL_HANDLE ) ) ;
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
}
2026-03-09 20:10:19 +01:00
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void Window : : wl_surface_frame_done ( void * data , struct wl_callback * cb , uint32_t time ) {
wl_callback_destroy ( cb ) ;
Window * window = reinterpret_cast < Window * > ( data ) ;
if ( window - > updating ) {
cb = wl_surface_frame ( window - > surface ) ;
wl_callback_add_listener ( cb , & Window : : wl_callback_listener , window ) ;
2026-03-09 21:43:25 +01:00
window - > Update ( ) ;
} else {
cb = nullptr ;
2026-03-09 20:10:19 +01:00
}
}
2026-05-12 00:24:48 +02:00
void Window : : xdg_toplevel_configure ( void * data , xdg_toplevel * , std : : int32_t width , std : : int32_t height , wl_array * ) {
// xdg-shell batches state — width/height are pending until the matching
// xdg_surface.configure arrives. Width/height are in surface-local
// (logical DP) units; (0, 0) means "compositor has no preference".
Window * window = reinterpret_cast < Window * > ( data ) ;
window - > pendingLogicalWidth = width ;
window - > pendingLogicalHeight = height ;
2026-03-09 20:10:19 +01:00
}
void Window : : xdg_toplevel_handle_close ( void * data , xdg_toplevel * ) {
Window * window = reinterpret_cast < Window * > ( data ) ;
window - > onClose . Invoke ( ) ;
window - > open = false ;
}
void Window : : xdg_surface_handle_configure ( void * data , xdg_surface * xdg_surface , std : : uint32_t serial ) {
Window * window = reinterpret_cast < Window * > ( data ) ;
// The compositor configures our surface, acknowledge the configure event
xdg_surface_ack_configure ( xdg_surface , serial ) ;
if ( window - > configured ) {
2026-05-12 00:24:48 +02:00
// Subsequent configure: if the toplevel asked for a new size
// (non-zero, different from current), drive the resize end-to-end.
// (0, 0) means "compositor has no preference, keep current size".
// The swapchain may not exist yet on the very first frame between
// the constructor's wait loop and CreateSwapchain — the Resize
// guard against equal sizes already covers that path.
if ( window - > pendingLogicalWidth > 0 & & window - > pendingLogicalHeight > 0 & &
window - > swapChain ! = VK_NULL_HANDLE ) {
std : : uint32_t newWidth = static_cast < std : : uint32_t > (
std : : ceil ( window - > pendingLogicalWidth * window - > scale ) ) ;
std : : uint32_t newHeight = static_cast < std : : uint32_t > (
std : : ceil ( window - > pendingLogicalHeight * window - > scale ) ) ;
window - > Resize ( newWidth , newHeight ) ;
}
2026-03-09 20:10:19 +01:00
wl_surface_commit ( window - > surface ) ;
}
window - > configured = true ;
}
void Window : : xdg_surface_handle_preferred_scale ( void * data , wp_fractional_scale_v1 * , std : : uint32_t scale ) {
Window * window = reinterpret_cast < Window * > ( data ) ;
window - > scale = scale / 120.0f ;
}
2026-05-01 23:35:37 +02:00
# endif
void Window : : SaveFrame ( const std : : filesystem : : path & path ) {
// Staging buffer big enough for one RGBA frame.
VkDeviceSize bufSize = static_cast < VkDeviceSize > ( width ) * height * 4 ;
VkBufferCreateInfo bci {
. sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO ,
. size = bufSize ,
. usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT ,
. sharingMode = VK_SHARING_MODE_EXCLUSIVE ,
} ;
VkBuffer stagingBuf = VK_NULL_HANDLE ;
Device : : CheckVkResult ( vkCreateBuffer ( Device : : device , & bci , nullptr , & stagingBuf ) ) ;
VkMemoryRequirements memReqs ;
vkGetBufferMemoryRequirements ( Device : : device , stagingBuf , & memReqs ) ;
VkMemoryAllocateInfo mai {
. sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO ,
. allocationSize = memReqs . size ,
. memoryTypeIndex = Device : : GetMemoryType ( memReqs . memoryTypeBits ,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ) ,
} ;
VkDeviceMemory stagingMem = VK_NULL_HANDLE ;
Device : : CheckVkResult ( vkAllocateMemory ( Device : : device , & mai , nullptr , & stagingMem ) ) ;
Device : : CheckVkResult ( vkBindBufferMemory ( Device : : device , stagingBuf , stagingMem , 0 ) ) ;
// One-shot command buffer so we don't trash the per-frame ones.
VkCommandBufferAllocateInfo cba {
. sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO ,
. commandPool = Device : : commandPool ,
. level = VK_COMMAND_BUFFER_LEVEL_PRIMARY ,
. commandBufferCount = 1 ,
} ;
VkCommandBuffer cmd = VK_NULL_HANDLE ;
Device : : CheckVkResult ( vkAllocateCommandBuffers ( Device : : device , & cba , & cmd ) ) ;
VkCommandBufferBeginInfo cbi {
. sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO ,
. flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT ,
} ;
Device : : CheckVkResult ( vkBeginCommandBuffer ( cmd , & cbi ) ) ;
VkImageSubresourceRange range {
. aspectMask = VK_IMAGE_ASPECT_COLOR_BIT ,
. baseMipLevel = 0 ,
. levelCount = 1 ,
. baseArrayLayer = 0 ,
. layerCount = 1 ,
} ;
// Render() leaves the image in PRESENT_SRC_KHR.
VkImageMemoryBarrier toSrc {
. sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER ,
. srcAccessMask = 0 ,
. dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT ,
. oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
. newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ,
. srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. image = images [ currentBuffer ] ,
. subresourceRange = range ,
} ;
vkCmdPipelineBarrier ( cmd ,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT , VK_PIPELINE_STAGE_TRANSFER_BIT ,
0 , 0 , nullptr , 0 , nullptr , 1 , & toSrc ) ;
VkBufferImageCopy region {
. bufferOffset = 0 ,
. bufferRowLength = 0 ,
. bufferImageHeight = 0 ,
. imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT , 0 , 0 , 1 } ,
. imageOffset = { 0 , 0 , 0 } ,
. imageExtent = { width , height , 1 } ,
} ;
vkCmdCopyImageToBuffer ( cmd , images [ currentBuffer ] ,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL , stagingBuf , 1 , & region ) ;
VkImageMemoryBarrier back {
. sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER ,
. srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT ,
. dstAccessMask = 0 ,
. oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ,
. newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR ,
. srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED ,
. image = images [ currentBuffer ] ,
. subresourceRange = range ,
} ;
vkCmdPipelineBarrier ( cmd ,
VK_PIPELINE_STAGE_TRANSFER_BIT , VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT ,
0 , 0 , nullptr , 0 , nullptr , 1 , & back ) ;
Device : : CheckVkResult ( vkEndCommandBuffer ( cmd ) ) ;
VkSubmitInfo si {
. sType = VK_STRUCTURE_TYPE_SUBMIT_INFO ,
. commandBufferCount = 1 ,
. pCommandBuffers = & cmd ,
} ;
Device : : CheckVkResult ( vkQueueSubmit ( Device : : queue , 1 , & si , VK_NULL_HANDLE ) ) ;
Device : : CheckVkResult ( vkQueueWaitIdle ( Device : : queue ) ) ;
// Read back, swizzle BGRA → RGBA if needed, write PNG.
void * mapped = nullptr ;
Device : : CheckVkResult ( vkMapMemory ( Device : : device , stagingMem , 0 , VK_WHOLE_SIZE , 0 , & mapped ) ) ;
const std : : uint8_t * src = static_cast < const std : : uint8_t * > ( mapped ) ;
std : : vector < std : : uint8_t > rgba ( static_cast < std : : size_t > ( width ) * height * 4 ) ;
bool bgr = ( colorFormat = = VK_FORMAT_B8G8R8A8_UNORM ) ;
for ( std : : uint32_t i = 0 ; i < width * height ; + + i ) {
if ( bgr ) {
rgba [ i * 4 + 0 ] = src [ i * 4 + 2 ] ;
rgba [ i * 4 + 1 ] = src [ i * 4 + 1 ] ;
rgba [ i * 4 + 2 ] = src [ i * 4 + 0 ] ;
rgba [ i * 4 + 3 ] = src [ i * 4 + 3 ] ;
} else {
rgba [ i * 4 + 0 ] = src [ i * 4 + 0 ] ;
rgba [ i * 4 + 1 ] = src [ i * 4 + 1 ] ;
rgba [ i * 4 + 2 ] = src [ i * 4 + 2 ] ;
rgba [ i * 4 + 3 ] = src [ i * 4 + 3 ] ;
}
}
vkUnmapMemory ( Device : : device , stagingMem ) ;
stbi_write_png ( path . string ( ) . c_str ( ) , static_cast < int > ( width ) , static_cast < int > ( height ) ,
4 , rgba . data ( ) , static_cast < int > ( width ) * 4 ) ;
vkFreeCommandBuffers ( Device : : device , Device : : commandPool , 1 , & cmd ) ;
vkDestroyBuffer ( Device : : device , stagingBuf , nullptr ) ;
vkFreeMemory ( Device : : device , stagingMem , nullptr ) ;
2026-05-18 02:07:48 +02:00
}
# endif // !CRAFTER_GRAPHICS_WINDOW_DOM
// ──────────────────────────────────────────────────────────────────────
// DOM backend
// ──────────────────────────────────────────────────────────────────────
//
// In DOM mode the "window" IS the browser page; there is no separate
// surface to create, no swapchain to manage, no GPU pipeline to wait on.
// All Window does here is:
// - mirror requested title onto document.title
// - register itself with the JS bridge so DOM-level events route into
// its event objects
// - hand the frame loop to requestAnimationFrame; `Update` runs on
// each rAF tick when `updating` is true
//
// The C exports (__crafterDom_*) below are how the JS bridge reaches
// back into the live Window instance. We keep a process-global pointer
// for V1 — only one Window per page — and lookups are O(1).
# ifdef CRAFTER_GRAPHICS_WINDOW_DOM
// The JS runtime initializes itself before main() runs, so there's
// nothing to do here. Defined as a no-op so user code calling
// `Device::Initialize()` links the same way it does on native.
void Device : : Initialize ( ) { }
namespace {
Window * g_domWindow = nullptr ;
}
namespace Crafter : : DomEnv {
__attribute__ ( ( import_module ( " env " ) , import_name ( " domAttachWindow " ) ) )
void domAttachWindow ( std : : int32_t handle ) ;
__attribute__ ( ( import_module ( " env " ) , import_name ( " domSetTitle " ) ) )
void domSetTitle ( const char * title , std : : int32_t titleLen ) ;
__attribute__ ( ( import_module ( " env " ) , import_name ( " domGetInnerWidth " ) ) )
std : : int32_t domGetInnerWidth ( ) ;
__attribute__ ( ( import_module ( " env " ) , import_name ( " domGetInnerHeight " ) ) )
std : : int32_t domGetInnerHeight ( ) ;
__attribute__ ( ( import_module ( " env " ) , import_name ( " domStartFrameLoop " ) ) )
void domStartFrameLoop ( ) ;
__attribute__ ( ( import_module ( " env " ) , import_name ( " domStopFrameLoop " ) ) )
void domStopFrameLoop ( ) ;
}
// Compile-time string hash matching what dom-env.js sends through. The
// JS bridge marshals `KeyboardEvent.code` as a UTF-8 string; we hash it
// to a 32-bit KeyCode here so the same value compares equal against the
// table in :Keys (DOM branch). FNV-1a, deterministic, no allocation.
namespace {
constexpr KeyCode HashKeyCode ( const char * p , std : : size_t n ) {
std : : uint32_t h = 2166136261u ;
for ( std : : size_t i = 0 ; i < n ; + + i ) {
h ^ = static_cast < std : : uint8_t > ( p [ i ] ) ;
h * = 16777619u ;
}
return h ;
}
}
Window : : Window ( std : : uint32_t w , std : : uint32_t h , const std : : string_view title )
: Window ( w , h ) {
SetTitle ( title ) ;
}
Window : : Window ( std : : uint32_t w , std : : uint32_t h ) : width ( w ) , height ( h ) {
if ( g_domWindow ! = nullptr ) {
// Only one Window per page in V1. Subsequent constructions are
// a programming error — log loudly and clobber the previous
// pointer so the new Window's events at least fire.
// (stderr isn't reachable via `import std;` on wasi-sdk yet; just log
// to cout. The browser console pipes both to the same place.)
std : : println ( " Crafter::Window: only one DOM Window per page; "
" overwriting the previous instance. " ) ;
}
g_domWindow = this ;
// Use the browser-reported viewport size as the initial dimensions
// unless the caller asked for something specific. Browser owns the
// real size; w/h passed in are advisory.
if ( w = = 0 | | h = = 0 ) {
width = static_cast < std : : uint32_t > ( Crafter : : DomEnv : : domGetInnerWidth ( ) ) ;
height = static_cast < std : : uint32_t > ( Crafter : : DomEnv : : domGetInnerHeight ( ) ) ;
}
// The handle passed to attach is just a non-zero token the JS side
// includes back in every dispatcher call. We don't use it on the
// C++ side (g_domWindow is the lookup) but it has to be non-zero so
// the JS bridge treats the window as "attached".
Crafter : : DomEnv : : domAttachWindow ( 1 ) ;
lastMousePos = { 0 , 0 } ;
currentMousePos = { 0 , 0 } ;
mouseDelta = { 0 , 0 } ;
}
Window : : ~ Window ( ) {
// Clear the global pointer iff it still references us — defensive
// against a stack-allocated Window in main() that goes out of scope
// while rAF / DOM event callbacks are still queued. After this, the
// JS-side dispatchers (__crafterDom_*) early-return harmlessly. A
// shrill warning to the console flags the (almost certainly
// unintended) lifetime mistake so the user notices before everything
// mysteriously stops working.
if ( g_domWindow = = this ) {
g_domWindow = nullptr ;
std : : println ( " Crafter::Window: destroyed while DOM mode is active. "
" Browser events will no-op until a new Window is constructed. "
" Did you forget to put the Window in `static` / `new`d storage? " ) ;
}
}
void Window : : SetTitle ( const std : : string_view title ) {
Crafter : : DomEnv : : domSetTitle ( title . data ( ) , static_cast < std : : int32_t > ( title . size ( ) ) ) ;
}
void Window : : Resize ( std : : uint32_t newWidth , std : : uint32_t newHeight ) {
if ( newWidth = = 0 | | newHeight = = 0 ) return ;
if ( newWidth = = width & & newHeight = = height ) return ;
width = newWidth ;
height = newHeight ;
onResize . Invoke ( ) ;
}
void Window : : SetCursorImage ( std : : uint16_t /*cw*/ , std : : uint16_t /*ch*/ ,
std : : uint16_t /*hx*/ , std : : uint16_t /*hy*/ ,
const std : : uint8_t * /*pixels*/ ) {
// V1: not wired. The natural impl is to base64-encode an inline PNG
// and assign it via document.body.style.cursor = `url(data:...) hx hy, auto`.
// Left for a follow-up so the first DOM build can ship without an
// inline PNG encoder.
}
void Window : : SetDefaultCursor ( ) {
// Mirror SetCursorImage stub. Future impl: clear body.style.cursor.
}
void Window : : StartSync ( ) {
// Hand the loop to rAF. Returns immediately; the wasm `_start`
// (main) finishes, and the runtime keeps the module alive while
// the JS-side rAF chain ticks `__crafterDom_frame`.
Crafter : : DomEnv : : domStartFrameLoop ( ) ;
}
void Window : : StartUpdate ( ) {
lastFrameBegin = std : : chrono : : high_resolution_clock : : now ( ) ;
updating = true ;
}
void Window : : StopUpdate ( ) {
updating = false ;
Crafter : : DomEnv : : domStopFrameLoop ( ) ;
}
void Window : : Update ( ) {
auto now = std : : chrono : : high_resolution_clock : : now ( ) ;
mouseDelta = { currentMousePos . x - lastMousePos . x ,
currentMousePos . y - lastMousePos . y } ;
currentFrameTime = { now , now - lastFrameBegin } ;
onUpdate . Invoke ( currentFrameTime ) ;
lastMousePos = currentMousePos ;
lastFrameBegin = now ;
}
void Window : : Render ( ) {
// V1: no rendering in DOM mode. Kept as a callable no-op so
// existing cross-platform code paths (e.g. main loops calling
// window.Render() before window.StartSync()) compile. V2 will
// hang the WebGPU command-submit here.
}
// ─── C exports the JS bridge calls back into ──────────────────────────
extern " C " {
__attribute__ ( ( export_name ( " __crafterDom_frame " ) ) )
void __crafterDom_frame ( std : : int32_t /*handle*/ ) {
if ( ! g_domWindow ) return ;
Gamepad : : Tick ( ) ;
g_domWindow - > onBeforeUpdate . Invoke ( ) ;
if ( g_domWindow - > updating ) {
g_domWindow - > Update ( ) ;
}
}
__attribute__ ( ( export_name ( " __crafterDom_mouseMove " ) ) )
void __crafterDom_mouseMove ( std : : int32_t /*handle*/ , double x , double y ) {
if ( ! g_domWindow ) return ;
g_domWindow - > currentMousePos = { static_cast < float > ( x ) , static_cast < float > ( y ) } ;
g_domWindow - > onMouseMove . Invoke ( ) ;
}
__attribute__ ( ( export_name ( " __crafterDom_mouseDown " ) ) )
void __crafterDom_mouseDown ( std : : int32_t /*handle*/ , std : : int32_t button ) {
if ( ! g_domWindow ) return ;
// MouseEvent.button: 0=left, 1=middle, 2=right
if ( button = = 0 ) {
g_domWindow - > mouseLeftHeld = true ;
g_domWindow - > onMouseLeftClick . Invoke ( ) ;
} else if ( button = = 2 ) {
g_domWindow - > mouseRightHeld = true ;
g_domWindow - > onMouseRightClick . Invoke ( ) ;
}
}
__attribute__ ( ( export_name ( " __crafterDom_mouseUp " ) ) )
void __crafterDom_mouseUp ( std : : int32_t /*handle*/ , std : : int32_t button ) {
if ( ! g_domWindow ) return ;
if ( button = = 0 ) {
g_domWindow - > mouseLeftHeld = false ;
g_domWindow - > onMouseLeftRelease . Invoke ( ) ;
} else if ( button = = 2 ) {
g_domWindow - > mouseRightHeld = false ;
g_domWindow - > onMouseRightRelease . Invoke ( ) ;
}
}
__attribute__ ( ( export_name ( " __crafterDom_wheel " ) ) )
void __crafterDom_wheel ( std : : int32_t /*handle*/ , double deltaY ) {
if ( ! g_domWindow ) return ;
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
g_domWindow - > onMouseScroll . Invoke ( static_cast < std : : uint32_t > ( static_cast < std : : int32_t > ( deltaY ) ) ) ;
}
__attribute__ ( ( export_name ( " __crafterDom_keyDown " ) ) )
void __crafterDom_keyDown ( std : : int32_t /*handle*/ ,
const char * codePtr , std : : int32_t codeLen ,
const char * keyPtr , std : : int32_t keyLen ,
bool repeat ) {
if ( ! g_domWindow ) return ;
KeyCode code = HashKeyCode ( codePtr , static_cast < std : : size_t > ( codeLen ) ) ;
if ( repeat ) {
g_domWindow - > onRawKeyHold . Invoke ( code ) ;
} else {
g_domWindow - > heldKeys . insert ( code ) ;
g_domWindow - > onRawKeyDown . Invoke ( code ) ;
}
// KeyboardEvent.key is the printable form. Forward as UTF-8
// text input for non-control keys so onTextInput drives input
// fields the same way it does on Win32 / Wayland.
if ( keyLen = = 1 & & static_cast < unsigned char > ( keyPtr [ 0 ] ) > = 0x20
& & static_cast < unsigned char > ( keyPtr [ 0 ] ) ! = 0x7F ) {
g_domWindow - > onTextInput . Invoke ( std : : string_view ( keyPtr , static_cast < std : : size_t > ( keyLen ) ) ) ;
} else if ( keyLen > 1 ) {
// Multi-byte UTF-8 (non-ASCII printable). Forward as-is —
// dom-env.js always sends valid UTF-8.
g_domWindow - > onTextInput . Invoke ( std : : string_view ( keyPtr , static_cast < std : : size_t > ( keyLen ) ) ) ;
}
}
__attribute__ ( ( export_name ( " __crafterDom_keyUp " ) ) )
void __crafterDom_keyUp ( std : : int32_t /*handle*/ , const char * codePtr , std : : int32_t codeLen ) {
if ( ! g_domWindow ) return ;
KeyCode code = HashKeyCode ( codePtr , static_cast < std : : size_t > ( codeLen ) ) ;
g_domWindow - > heldKeys . erase ( code ) ;
g_domWindow - > onRawKeyUp . Invoke ( code ) ;
}
__attribute__ ( ( export_name ( " __crafterDom_resize " ) ) )
void __crafterDom_resize ( std : : int32_t /*handle*/ , std : : int32_t newW , std : : int32_t newH ) {
if ( ! g_domWindow ) return ;
g_domWindow - > Resize ( static_cast < std : : uint32_t > ( newW ) ,
static_cast < std : : uint32_t > ( newH ) ) ;
}
__attribute__ ( ( export_name ( " __crafterDom_close " ) ) )
void __crafterDom_close ( std : : int32_t /*handle*/ ) {
if ( ! g_domWindow ) return ;
g_domWindow - > open = false ;
g_domWindow - > onClose . Invoke ( ) ;
}
}
# endif // CRAFTER_GRAPHICS_WINDOW_DOM