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
# include "vulkan/vulkan.h"
# ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
# include "vulkan/vulkan_wayland.h"
# endif
# ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
# include "vulkan/vulkan_win32.h"
# endif
2026-05-01 23:35:37 +02:00
# define STB_IMAGE_WRITE_IMPLEMENTATION
# include "../lib/stb_image_write.h"
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 ;
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-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
CrafterKeys vk_to_crafter_key ( WPARAM vk )
{
switch ( vk )
{
// Alphabet
case ' A ' : return CrafterKeys : : A ;
case ' B ' : return CrafterKeys : : B ;
case ' C ' : return CrafterKeys : : C ;
case ' D ' : return CrafterKeys : : D ;
case ' E ' : return CrafterKeys : : E ;
case ' F ' : return CrafterKeys : : F ;
case ' G ' : return CrafterKeys : : G ;
case ' H ' : return CrafterKeys : : H ;
case ' I ' : return CrafterKeys : : I ;
case ' J ' : return CrafterKeys : : J ;
case ' K ' : return CrafterKeys : : K ;
case ' L ' : return CrafterKeys : : L ;
case ' M ' : return CrafterKeys : : M ;
case ' N ' : return CrafterKeys : : N ;
case ' O ' : return CrafterKeys : : O ;
case ' P ' : return CrafterKeys : : P ;
case ' Q ' : return CrafterKeys : : Q ;
case ' R ' : return CrafterKeys : : R ;
case ' S ' : return CrafterKeys : : S ;
case ' T ' : return CrafterKeys : : T ;
case ' U ' : return CrafterKeys : : U ;
case ' V ' : return CrafterKeys : : V ;
case ' W ' : return CrafterKeys : : W ;
case ' X ' : return CrafterKeys : : X ;
case ' Y ' : return CrafterKeys : : Y ;
case ' Z ' : return CrafterKeys : : Z ;
// Numbers
case ' 0 ' : return CrafterKeys : : _0 ;
case ' 1 ' : return CrafterKeys : : _1 ;
case ' 2 ' : return CrafterKeys : : _2 ;
case ' 3 ' : return CrafterKeys : : _3 ;
case ' 4 ' : return CrafterKeys : : _4 ;
case ' 5 ' : return CrafterKeys : : _5 ;
case ' 6 ' : return CrafterKeys : : _6 ;
case ' 7 ' : return CrafterKeys : : _7 ;
case ' 8 ' : return CrafterKeys : : _8 ;
case ' 9 ' : return CrafterKeys : : _9 ;
// Function keys
case VK_F1 : return CrafterKeys : : F1 ;
case VK_F2 : return CrafterKeys : : F2 ;
case VK_F3 : return CrafterKeys : : F3 ;
case VK_F4 : return CrafterKeys : : F4 ;
case VK_F5 : return CrafterKeys : : F5 ;
case VK_F6 : return CrafterKeys : : F6 ;
case VK_F7 : return CrafterKeys : : F7 ;
case VK_F8 : return CrafterKeys : : F8 ;
case VK_F9 : return CrafterKeys : : F9 ;
case VK_F10 : return CrafterKeys : : F10 ;
case VK_F11 : return CrafterKeys : : F11 ;
case VK_F12 : return CrafterKeys : : F12 ;
// Control keys
case VK_ESCAPE : return CrafterKeys : : Escape ;
case VK_TAB : return CrafterKeys : : Tab ;
case VK_RETURN : return CrafterKeys : : Enter ;
case VK_SPACE : return CrafterKeys : : Space ;
case VK_BACK : return CrafterKeys : : Backspace ;
case VK_DELETE : return CrafterKeys : : Delete ;
case VK_INSERT : return CrafterKeys : : Insert ;
case VK_HOME : return CrafterKeys : : Home ;
case VK_END : return CrafterKeys : : End ;
case VK_PRIOR : return CrafterKeys : : PageUp ;
case VK_NEXT : return CrafterKeys : : PageDown ;
case VK_CAPITAL : return CrafterKeys : : CapsLock ;
case VK_NUMLOCK : return CrafterKeys : : NumLock ;
case VK_SCROLL : return CrafterKeys : : ScrollLock ;
// Modifiers
2026-03-09 22:34:14 -05:00
case VK_SHIFT : return CrafterKeys : : LeftShift ;
2026-03-09 20:10:19 +01:00
case VK_LSHIFT : return CrafterKeys : : LeftShift ;
2026-03-09 22:34:14 -05:00
case VK_RSHIFT : return CrafterKeys : : RightShift ;
case VK_CONTROL : return CrafterKeys : : LeftCtrl ;
2026-03-09 20:10:19 +01:00
case VK_LCONTROL : return CrafterKeys : : LeftCtrl ;
case VK_RCONTROL : return CrafterKeys : : RightCtrl ;
case VK_LMENU : return CrafterKeys : : LeftAlt ;
case VK_RMENU : return CrafterKeys : : RightAlt ;
case VK_LWIN : return CrafterKeys : : LeftSuper ;
case VK_RWIN : return CrafterKeys : : RightSuper ;
// Arrows
case VK_UP : return CrafterKeys : : Up ;
case VK_DOWN : return CrafterKeys : : Down ;
case VK_LEFT : return CrafterKeys : : Left ;
case VK_RIGHT : return CrafterKeys : : Right ;
// Keypad
case VK_NUMPAD0 : return CrafterKeys : : keypad_0 ;
case VK_NUMPAD1 : return CrafterKeys : : keypad_1 ;
case VK_NUMPAD2 : return CrafterKeys : : keypad_2 ;
case VK_NUMPAD3 : return CrafterKeys : : keypad_3 ;
case VK_NUMPAD4 : return CrafterKeys : : keypad_4 ;
case VK_NUMPAD5 : return CrafterKeys : : keypad_5 ;
case VK_NUMPAD6 : return CrafterKeys : : keypad_6 ;
case VK_NUMPAD7 : return CrafterKeys : : keypad_7 ;
case VK_NUMPAD8 : return CrafterKeys : : keypad_8 ;
case VK_NUMPAD9 : return CrafterKeys : : keypad_9 ;
case VK_SEPARATOR : return CrafterKeys : : keypad_enter ;
case VK_ADD : return CrafterKeys : : keypad_plus ;
case VK_SUBTRACT : return CrafterKeys : : keypad_minus ;
case VK_MULTIPLY : return CrafterKeys : : keypad_multiply ;
case VK_DIVIDE : return CrafterKeys : : keypad_divide ;
case VK_DECIMAL : return CrafterKeys : : keypad_decimal ;
// Punctuation
case VK_OEM_3 : return CrafterKeys : : grave ; // `
case VK_OEM_MINUS : return CrafterKeys : : minus ; // -
case VK_OEM_PLUS : return CrafterKeys : : equal ; // =
case VK_OEM_4 : return CrafterKeys : : bracket_left ; // [
case VK_OEM_6 : return CrafterKeys : : bracket_right ; // ]
case VK_OEM_5 : return CrafterKeys : : backslash ; //
case VK_OEM_1 : return CrafterKeys : : semicolon ; // ;
case VK_OEM_7 : return CrafterKeys : : quote ; // '
case VK_OEM_COMMA : return CrafterKeys : : comma ; // ,
case VK_OEM_PERIOD : return CrafterKeys : : period ; // .
case VK_OEM_2 : return CrafterKeys : : slash ; // /
default : throw std : : runtime_error ( std : : format ( " Unkown VK {} " , vk ) ) ;
}
}
// 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-04-16 23:03:24 +02:00
case WM_KEYDOWN :
case WM_SYSKEYDOWN : { // SYSKEYDOWN catches Alt combos, F10, etc.
CrafterKeys crafterKey = vk_to_crafter_key ( wParam ) ;
bool isRepeat = ( lParam & ( 1 < < 30 ) ) ! = 0 ;
if ( isRepeat ) {
window - > onKeyHold [ ( uint8_t ) crafterKey ] . Invoke ( ) ;
window - > onAnyKeyHold . Invoke ( crafterKey ) ;
} else {
window - > heldkeys [ ( uint8_t ) crafterKey ] = true ;
window - > onKeyDown [ ( uint8_t ) crafterKey ] . Invoke ( ) ;
window - > onAnyKeyDown . Invoke ( crafterKey ) ;
2026-03-09 20:10:19 +01:00
}
break ;
}
2026-04-16 23:03:24 +02:00
case WM_KEYUP :
case WM_SYSKEYUP : {
2026-03-09 20:10:19 +01:00
CrafterKeys crafterKey = vk_to_crafter_key ( wParam ) ;
2026-04-16 23:03:24 +02:00
window - > heldkeys [ ( uint8_t ) crafterKey ] = false ;
window - > onKeyUp [ ( uint8_t ) crafterKey ] . Invoke ( ) ;
2026-03-09 20:10:19 +01:00
window - > onAnyKeyUp . Invoke ( crafterKey ) ;
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-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-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-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-03-09 21:16:14 +01:00
// Acquire the next image from the swap chain
2026-03-09 21:14:53 +01:00
Device : : CheckVkResult ( vkAcquireNextImageKHR ( Device : : device , swapChain , UINT64_MAX , semaphores . presentComplete , ( VkFence ) nullptr , & currentBuffer ) ) ;
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-03-09 21:16:14 +01: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
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-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-03-09 20:10:19 +01:00
if ( result = = VK_SUBOPTIMAL_KHR ) {
CreateSwapchain ( ) ;
} 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
}
}
void Window : : xdg_toplevel_configure ( void * , xdg_toplevel * , std : : int32_t , std : : int32_t , wl_array * ) {
}
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 ) {
// If this isn't the first configure event we've received, we already
// have a buffer attached, so no need to do anything. Commit the
// surface to apply the configure acknowledgement.
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 ) ;
}