Window::Render() rebuilt cmdBufInfo, the subresource range, both VkImageMemoryBarrier structs, and presentInfo on the stack every frame even though only the barriers' image (and the acquire barrier's oldLayout) ever change. Hoist them to members initialised once in the constructor; Render() now patches only the two varying fields. presentInfo.pImageIndices/pSwapchains point at the currentBuffer and swapChain members, so they track value changes (including swapchain recreation) automatically. The render-complete wait semaphore is fixed for the window's lifetime, so the per-frame `renderComplete != NULL` check — set once, never cleared — was dead and is removed; the semaphore is wired into presentInfo in the constructor instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1560 lines
No EOL
61 KiB
C++
1560 lines
No EOL
61 KiB
C++
/*
|
|
Crafter®.Graphics
|
|
Copyright (C) 2026 Catcrafts®
|
|
catcrafts.net
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License version 3.0 as published by the Free Software Foundation;
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
module;
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <linux/input-event-codes.h>
|
|
#include "../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
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
#include "vulkan/vulkan.h"
|
|
#endif
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
#include "vulkan/vulkan_wayland.h"
|
|
#endif
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
#include "vulkan/vulkan_win32.h"
|
|
#endif
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#include "../lib/stb_image_write.h"
|
|
#endif
|
|
module Crafter.Graphics:Window_impl;
|
|
import :Window;
|
|
import :Device;
|
|
import :Gamepad;
|
|
// 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.
|
|
import :VulkanTransition;
|
|
import :DescriptorHeapVulkan;
|
|
import :RenderPass;
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
import :WebGPU;
|
|
import :DescriptorHeapWebGPU;
|
|
#endif
|
|
import std;
|
|
|
|
using namespace Crafter;
|
|
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
#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;
|
|
}
|
|
}
|
|
|
|
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
|
|
// 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);
|
|
}
|
|
|
|
// 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)
|
|
);
|
|
}
|
|
|
|
switch (msg) {
|
|
case WM_DESTROY:{
|
|
PostQuitMessage(0);
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
case WM_KEYDOWN:
|
|
case WM_SYSKEYDOWN: { // SYSKEYDOWN catches Alt combos, F10, etc.
|
|
KeyCode code = KeyCodeFromLParam(lParam);
|
|
bool isRepeat = (lParam & (1 << 30)) != 0;
|
|
|
|
if (isRepeat) {
|
|
window->onRawKeyHold.Invoke(code);
|
|
} else {
|
|
window->heldKeys.insert(code);
|
|
window->onRawKeyDown.Invoke(code);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_KEYUP:
|
|
case WM_SYSKEYUP: {
|
|
KeyCode code = KeyCodeFromLParam(lParam);
|
|
window->heldKeys.erase(code);
|
|
window->onRawKeyUp.Invoke(code);
|
|
break;
|
|
}
|
|
|
|
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));
|
|
}
|
|
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;
|
|
}
|
|
|
|
case WM_MOUSEWHEEL: {
|
|
// High word of wParam is the signed wheel movement, WHEEL_DELTA
|
|
// (120) per detent, positive = wheel away from the user (scroll
|
|
// up). Window::onMouseScroll speaks whole detents with +1 =
|
|
// wheel down (the DOM deltaY / Wayland axis sign), so negate.
|
|
// Free-spinning wheels report sub-WHEEL_DELTA steps — accumulate
|
|
// so they add up instead of each truncating to zero.
|
|
static int wheelRemainder = 0;
|
|
wheelRemainder += -static_cast<int>(GET_WHEEL_DELTA_WPARAM(wParam));
|
|
int detents = wheelRemainder / WHEEL_DELTA; // trunc toward zero
|
|
if (detents != 0) {
|
|
wheelRemainder -= detents * WHEEL_DELTA;
|
|
// Event payload is uint32 — preserve sign via two's complement.
|
|
window->onMouseScroll.Invoke(
|
|
static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_SETCURSOR: {
|
|
if (LOWORD(lParam) == HTCLIENT && window->cursorHandle) {
|
|
SetCursor(window->cursorHandle);
|
|
return TRUE;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: return DefWindowProc(hwnd, msg, wParam, lParam);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
Window::Window(std::uint32_t width, std::uint32_t height, const std::string_view title) : Window(width, height) {
|
|
SetTitle(title);
|
|
}
|
|
|
|
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;
|
|
|
|
// Per-frame info structs: everything that never varies between frames is
|
|
// set here once. Render() only patches the barriers' image/oldLayout.
|
|
cmdBufInfo = {};
|
|
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
|
|
const VkImageSubresourceRange range {
|
|
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
|
.baseMipLevel = 0,
|
|
.levelCount = VK_REMAINING_MIP_LEVELS,
|
|
.baseArrayLayer = 0,
|
|
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
|
};
|
|
|
|
// Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image
|
|
// and oldLayout are patched per frame (oldLayout depends on first use).
|
|
acquireBarrier = {
|
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
|
.srcAccessMask = 0,
|
|
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.image = VK_NULL_HANDLE,
|
|
.subresourceRange = range,
|
|
};
|
|
|
|
// Transition back to PRESENT_SRC_KHR after the passes. Only image varies.
|
|
presentBarrier = {
|
|
.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 = VK_NULL_HANDLE,
|
|
.subresourceRange = range,
|
|
};
|
|
|
|
// pImageIndices/pSwapchains point at members, so they follow value changes
|
|
// (currentBuffer each frame, swapChain across recreation) on their own.
|
|
// The render-complete wait semaphore is fixed for the window's lifetime.
|
|
presentInfo = {};
|
|
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
|
presentInfo.pNext = NULL;
|
|
presentInfo.swapchainCount = 1;
|
|
presentInfo.pSwapchains = &swapChain;
|
|
presentInfo.pImageIndices = ¤tBuffer;
|
|
presentInfo.waitSemaphoreCount = 1;
|
|
presentInfo.pWaitSemaphores = &semaphores.renderComplete;
|
|
|
|
lastMousePos = {0,0};
|
|
mouseDelta = {0,0};
|
|
currentMousePos = {0,0};
|
|
}
|
|
|
|
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 leaves the new swapchain images in
|
|
// VK_IMAGE_LAYOUT_UNDEFINED and resets imageInitialised. The images are
|
|
// NOT transitioned here: a presentable image may only be touched after
|
|
// vkAcquireNextImageKHR returns it, so Render() performs each image's
|
|
// first layout transition lazily (from UNDEFINED) once it has been
|
|
// acquired. Pre-transitioning unacquired images is a validation error.
|
|
CreateSwapchain();
|
|
}
|
|
|
|
void Window::SetTitle(const std::string_view title) {
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
xdg_toplevel_set_title(xdgToplevel, title.data());
|
|
#endif
|
|
}
|
|
|
|
void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
|
|
std::uint16_t hotspotX, std::uint16_t hotspotY,
|
|
const std::uint8_t* pixels) {
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
if (width == 0 || height == 0 || pixels == nullptr) {
|
|
SetDefaultCursor();
|
|
return;
|
|
}
|
|
|
|
if (cursorSurface == nullptr) {
|
|
cursorSurface = wl_compositor_create_surface(Device::compositor);
|
|
}
|
|
|
|
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.
|
|
} else {
|
|
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) {
|
|
wl_buffer_destroy(cursorWlBuffer);
|
|
cursorWlBuffer = nullptr;
|
|
}
|
|
if (cursorSurface) {
|
|
wl_surface_destroy(cursorSurface);
|
|
cursorSurface = nullptr;
|
|
}
|
|
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);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Window::StartSync() {
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
while (open && wl_display_dispatch(Device::display) != -1) {
|
|
Gamepad::Tick();
|
|
onBeforeUpdate.Invoke();
|
|
}
|
|
#endif
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
while(open) {
|
|
MSG msg;
|
|
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
Gamepad::Tick();
|
|
onBeforeUpdate.Invoke();
|
|
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;
|
|
}
|
|
|
|
std::chrono::time_point<std::chrono::high_resolution_clock> startTime;
|
|
|
|
void Window::Update() {
|
|
startTime = std::chrono::high_resolution_clock::now();
|
|
#ifdef CRAFTER_TIMING
|
|
vblank = duration_cast<std::chrono::milliseconds>(startTime - frameEnd);
|
|
#endif
|
|
|
|
mouseDelta = {currentMousePos.x-lastMousePos.x, currentMousePos.y-lastMousePos.y};
|
|
currentFrameTime = {startTime, startTime-lastFrameBegin};
|
|
#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
|
|
|
|
lastMousePos = currentMousePos;
|
|
|
|
#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
|
|
lastFrameBegin = startTime;
|
|
}
|
|
|
|
void Window::Render() {
|
|
// 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, ¤tBuffer);
|
|
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, ¤tBuffer);
|
|
}
|
|
if (acquire != VK_SUBOPTIMAL_KHR) {
|
|
Device::CheckVkResult(acquire);
|
|
}
|
|
}
|
|
submitInfo.commandBufferCount = 1;
|
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
|
|
|
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
|
|
|
// On an image's first use after (re)creating the swapchain it is still in
|
|
// VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
|
|
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
|
|
// error, so pick based on whether this image has been rendered before.
|
|
// The image was just acquired above, so touching it here is legal.
|
|
const bool firstUse = !imageInitialised[currentBuffer];
|
|
imageInitialised[currentBuffer] = true;
|
|
|
|
acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
|
|
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
acquireBarrier.image = images[currentBuffer];
|
|
|
|
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
|
|
|
|
// Synthesise key-repeat events before listeners run, so the focused
|
|
// widget's OnTextInput / OnKeyDown sees them in the same frame.
|
|
Device::TickKeyRepeats();
|
|
|
|
// 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.
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
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
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
presentBarrier.image = images[currentBuffer];
|
|
|
|
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
|
|
|
|
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
|
|
|
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
|
|
|
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
|
|
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();
|
|
} else {
|
|
Device::CheckVkResult(result);
|
|
}
|
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
}
|
|
|
|
#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;
|
|
for (const std::tuple<const RenderingElement*, std::uint32_t, std::uint32_t, std::chrono::nanoseconds>& entry : renderer.renderTimings) {
|
|
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;
|
|
}
|
|
std::cout << std::format("Total: {}", duration_cast<std::chrono::milliseconds>(totalUpdate+totalRender)) << std::endl;
|
|
std::cout << std::format("Vblank: {}", duration_cast<std::chrono::milliseconds>(vblank)) << std::endl;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void Window::CreateSwapchain()
|
|
{
|
|
// Store the current swap chain handle so we can use it later on to ease up recreation
|
|
VkSwapchainKHR oldSwapchain = swapChain;
|
|
|
|
// Get physical device surface properties and formats
|
|
VkSurfaceCapabilitiesKHR surfCaps;
|
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps));
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// Select a present mode for the swapchain
|
|
uint32_t presentModeCount;
|
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, NULL));
|
|
assert(presentModeCount > 0);
|
|
|
|
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
|
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, presentModes.data()));
|
|
|
|
// 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 };
|
|
swapchainCI.imageUsage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
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));
|
|
|
|
// Brand-new swapchain images are in VK_IMAGE_LAYOUT_UNDEFINED; none have
|
|
// been rendered/presented yet. Render() consults this to pick the correct
|
|
// oldLayout for each image's first post-acquire transition.
|
|
imageInitialised.fill(false);
|
|
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
VkCommandBuffer Window::StartInit() {
|
|
VkCommandBufferBeginInfo cmdBufInfo {};
|
|
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
|
|
|
// The swapchain images are deliberately NOT transitioned here. They are
|
|
// presentable images and may only be touched after vkAcquireNextImageKHR
|
|
// returns them; transitioning an unacquired presentable image is a
|
|
// validation error. Render() instead transitions each image from
|
|
// VK_IMAGE_LAYOUT_UNDEFINED on its first acquired use (see
|
|
// imageInitialised). This command buffer is for the caller's one-time
|
|
// setup work (uploads, acceleration-structure builds, etc.).
|
|
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));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
#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);
|
|
window->Update();
|
|
} else {
|
|
cb = nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
// 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);
|
|
}
|
|
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;
|
|
}
|
|
|
|
#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, ®ion);
|
|
|
|
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);
|
|
}
|
|
#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) {
|
|
std::println("Crafter::Window: only one DOM Window per page; "
|
|
"overwriting the previous instance.");
|
|
}
|
|
g_domWindow = this;
|
|
|
|
// Browser owns the real surface size. The width/height passed in are
|
|
// advisory only — useful as a native-side hint, ignored on DOM. We
|
|
// always sync to innerWidth/innerHeight so:
|
|
// - window.width/.height match the canvas's CSS pixel size,
|
|
// - MouseEvent.clientX/.clientY (CSS pixels) compare correctly
|
|
// against any layout done with window.width/.height,
|
|
// - the dispatch group count from window.width/8 covers the
|
|
// canvas exactly.
|
|
(void)w; (void)h;
|
|
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, then exit the wasm via _Exit so wasi-libc
|
|
// skips __wasm_call_dtors. If we let main return normally, _start
|
|
// calls __wasm_call_dtors → static destructors fire (including
|
|
// Window's own), then __wasi_proc_exit → wasm trap. Subsequent rAF
|
|
// calls into the wasm would then trap too, killing rendering.
|
|
// _Exit jumps straight to __wasi_proc_exit, which our runtime.js
|
|
// catches via a thrown sentinel so the instance stays alive while
|
|
// every static-allocated object (Window, UIRenderer, GPU buffers,
|
|
// event listeners) remains untouched. Callers' code after
|
|
// StartSync() never runs — match that contract on native too.
|
|
Crafter::DomEnv::domStartFrameLoop();
|
|
std::_Exit(0);
|
|
}
|
|
|
|
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() {
|
|
if (!open) return;
|
|
Crafter::WebGPU::wgpuFrameBegin();
|
|
for (RenderPass* p : passes) {
|
|
if (p) p->Record(/*cmd*/ 0u, currentBuffer, *this);
|
|
}
|
|
Crafter::WebGPU::wgpuFrameEnd();
|
|
}
|
|
|
|
WebGPUCommandEncoderRef Window::StartInit() {
|
|
// DOM init: no command buffer needed — texture / buffer creation goes
|
|
// through synchronous wgpu* imports. Return 0 as a placeholder; the
|
|
// value is opaque to user code (auto-typed in HelloUI).
|
|
Crafter::WebGPU::wgpuInit();
|
|
return 0;
|
|
}
|
|
|
|
void Window::FinishInit() {
|
|
// Nothing to submit in DOM mode; all init writes are queued at call
|
|
// time via queue.writeBuffer / writeTexture.
|
|
}
|
|
|
|
// ─── 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();
|
|
g_domWindow->Render();
|
|
}
|
|
}
|
|
|
|
__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 detents) {
|
|
if (!g_domWindow) return;
|
|
// dom-env.js has already normalized WheelEvent deltas to whole
|
|
// detents (+1 = wheel down) — see __windowListeners.wheel.
|
|
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
|
|
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
|
|
}
|
|
|
|
__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 for character keys, BUT
|
|
// it's also the named identifier for non-character keys —
|
|
// "Backspace", "Shift", "ArrowLeft", "Enter", … Those are
|
|
// multi-byte ASCII strings, and a naive `keyLen > 1` check
|
|
// forwards them straight into input fields as literal text.
|
|
//
|
|
// The reliable discriminator: multi-byte UTF-8 has the high bit
|
|
// set on its first byte (lead byte ≥ 0xC0; continuation bytes
|
|
// ≥ 0x80). Named keys are pure ASCII letters → first byte < 0x80.
|
|
// So forward when:
|
|
// • keyLen == 1 and the byte is a printable ASCII char, OR
|
|
// • keyLen > 1 and the first byte is ≥ 0x80 (true UTF-8).
|
|
// The onRawKeyDown event above already carries the key code for
|
|
// control-key consumers (InputField_OnKey handles Backspace/etc
|
|
// through that channel).
|
|
const auto first = static_cast<unsigned char>(keyPtr[0]);
|
|
if (keyLen == 1 && first >= 0x20 && first != 0x7F) {
|
|
g_domWindow->onTextInput.Invoke(std::string_view(keyPtr, static_cast<std::size_t>(keyLen)));
|
|
} else if (keyLen > 1 && first >= 0x80) {
|
|
// Real 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
|