new input system

This commit is contained in:
Jorijn van der Graaf 2026-05-12 00:24:48 +02:00
commit ac2eb7fb0a
31 changed files with 3292 additions and 781 deletions

View file

@ -56,6 +56,7 @@ module;
module Crafter.Graphics:Window_impl;
import :Window;
import :Device;
import :Gamepad;
import :VulkanTransition;
import :DescriptorHeapVulkan;
import :RenderPass;
@ -110,131 +111,15 @@ int create_shm_file(off_t size) {
#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
case VK_SHIFT: return CrafterKeys::LeftShift;
case VK_LSHIFT: return CrafterKeys::LeftShift;
case VK_RSHIFT: return CrafterKeys::RightShift;
case VK_CONTROL: return CrafterKeys::LeftCtrl;
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));
}
// 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
@ -266,28 +151,36 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
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.
CrafterKeys crafterKey = vk_to_crafter_key(wParam);
KeyCode code = KeyCodeFromLParam(lParam);
bool isRepeat = (lParam & (1 << 30)) != 0;
if (isRepeat) {
window->onKeyHold[(uint8_t)crafterKey].Invoke();
window->onAnyKeyHold.Invoke(crafterKey);
window->onRawKeyHold.Invoke(code);
} else {
window->heldkeys[(uint8_t)crafterKey] = true;
window->onKeyDown[(uint8_t)crafterKey].Invoke();
window->onAnyKeyDown.Invoke(crafterKey);
window->heldKeys.insert(code);
window->onRawKeyDown.Invoke(code);
}
break;
}
case WM_KEYUP:
case WM_SYSKEYUP: {
CrafterKeys crafterKey = vk_to_crafter_key(wParam);
window->heldkeys[(uint8_t)crafterKey] = false;
window->onKeyUp[(uint8_t)crafterKey].Invoke();
window->onAnyKeyUp.Invoke(crafterKey);
KeyCode code = KeyCodeFromLParam(lParam);
window->heldKeys.erase(code);
window->onRawKeyUp.Invoke(code);
break;
}
@ -510,6 +403,93 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
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();
// CreateSwapchain leaves new swapchain images in VK_IMAGE_LAYOUT_UNDEFINED.
// Render() barriers from PRESENT_SRC_KHR, so transition them now to
// match. Mirrors the StartInit logic, on a one-shot command buffer.
{
VkCommandBufferAllocateInfo cba{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = Device::commandPool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
VkCommandBuffer cmd = VK_NULL_HANDLE;
Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cba, &cmd));
VkCommandBufferBeginInfo cbi{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
};
Device::CheckVkResult(vkBeginCommandBuffer(cmd, &cbi));
VkImageSubresourceRange range{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
};
for (std::uint32_t i = 0; i < numFrames; i++) {
image_layout_transition(cmd,
images[i],
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
range);
}
Device::CheckVkResult(vkEndCommandBuffer(cmd));
VkSubmitInfo si{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.commandBufferCount = 1,
.pCommandBuffers = &cmd,
};
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE));
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
}
}
void Window::SetTitle(const std::string_view title) {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
xdg_toplevel_set_title(xdgToplevel, title.data());
@ -630,6 +610,7 @@ void Window::SetDefaultCursor() {
void Window::StartSync() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
while (open && wl_display_dispatch(Device::display) != -1) {
Gamepad::Tick();
onBeforeUpdate.Invoke();
}
#endif
@ -640,6 +621,7 @@ void Window::StartSync() {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Gamepad::Tick();
onBeforeUpdate.Invoke();
if(updating) {
Update();
@ -697,8 +679,23 @@ void Window::Update() {
}
void Window::Render() {
// Acquire the next image from the swap chain
Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, &currentBuffer));
// Acquire the next image from the swap chain. If the surface has
// changed size out from under us (compositor/Win32 resize delivered
// between Render calls), recreate and retry once.
{
VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
semaphores.presentComplete, (VkFence)nullptr, &currentBuffer);
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
RecreateSwapchainAndImages();
onResize.Invoke();
acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
semaphores.presentComplete, (VkFence)nullptr, &currentBuffer);
}
if (acquire != VK_SUBOPTIMAL_KHR) {
Device::CheckVkResult(acquire);
}
}
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
@ -822,8 +819,13 @@ void Window::Render() {
}
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
if(result == VK_SUBOPTIMAL_KHR) {
CreateSwapchain();
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);
}
@ -1067,8 +1069,13 @@ void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t
}
void Window::xdg_toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*){
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*) {
@ -1083,9 +1090,20 @@ void Window::xdg_surface_handle_configure(void* data, xdg_surface* xdg_surface,
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.
// 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);
}