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

@ -0,0 +1,174 @@
/*
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 as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
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 <wayland-client.h>
#include <wayland-client-protocol.h>
#include <unistd.h>
#include <errno.h>
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#endif
module Crafter.Graphics;
import std;
using namespace Crafter;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
namespace {
// Heap-allocated state attached to each wl_data_source via its
// user_data slot. The text must outlive the source (the compositor
// can call `send` minutes after we set the selection, every time a
// remote app pastes), so we own it here and free it from
// `OnSourceCancelled` — which is the only callback the compositor
// guarantees will fire when the source is no longer needed (when
// the selection is replaced, or on app exit).
struct Held {
std::string text;
};
void OnSourceTarget(void*, wl_data_source*, const char*) {}
void OnSourceSend(void* data, wl_data_source*,
const char* /*mime_type*/, std::int32_t fd) {
// We only ever advertise text MIME types, so any negotiated
// type maps to the same UTF-8 buffer. `send` may fire multiple
// times across the lifetime of one selection (each paste is a
// fresh fd), so we must not consume `text` here.
Held* h = static_cast<Held*>(data);
const char* p = h->text.data();
std::size_t rem = h->text.size();
while (rem > 0) {
const ssize_t w = ::write(fd, p, rem);
if (w < 0) {
if (errno == EINTR) continue;
break; // pipe closed; remote gave up reading
}
if (w == 0) break;
p += w;
rem -= static_cast<std::size_t>(w);
}
::close(fd);
}
void OnSourceCancelled(void* data, wl_data_source* source) {
// Selection was replaced (someone else copied) or the app is
// exiting. Either way, this is our hook to release the held
// text and the source itself.
delete static_cast<Held*>(data);
wl_data_source_destroy(source);
}
// Drag-and-drop only callbacks. We never start a DnD action, so
// these can't fire — but the listener struct's size is fixed at
// the v3 shape, and wayland-client uses the struct's slots
// directly. Stubs are required.
void OnSourceDndDropPerformed(void*, wl_data_source*) {}
void OnSourceDndFinished(void*, wl_data_source*) {}
void OnSourceAction(void*, wl_data_source*, std::uint32_t) {}
constexpr wl_data_source_listener kSourceListener = {
.target = OnSourceTarget,
.send = OnSourceSend,
.cancelled = OnSourceCancelled,
.dnd_drop_performed = OnSourceDndDropPerformed,
.dnd_finished = OnSourceDndFinished,
.action = OnSourceAction,
};
}
#endif
bool Crafter::Clipboard::SetText(std::string_view text) {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
// Compositor doesn't expose wl_data_device_manager (rare; some
// headless / minimal compositors). Caller can fall back.
return false;
}
// wl_data_device.set_selection requires a serial from a recent
// input event. We track the most recent pointer-enter serial on
// each window — fine for "user clicked Copy" interactions, since
// the click itself produced the serial. Without one, the
// compositor would silently reject the request.
std::uint32_t serial = 0;
if (Device::focusedWindow != nullptr) {
serial = Device::focusedWindow->lastPointerSerial_;
}
if (serial == 0) return false;
auto* held = new Held{ std::string(text) };
wl_data_source* source =
wl_data_device_manager_create_data_source(Device::dataDeviceManager);
if (source == nullptr) {
delete held;
return false;
}
wl_data_source_add_listener(source, &kSourceListener, held);
// Advertise the four common text MIME types so legacy + modern
// pasters both find a match. Wayland clients prefer the first one
// they recognise, in order.
wl_data_source_offer(source, "text/plain;charset=utf-8");
wl_data_source_offer(source, "text/plain");
wl_data_source_offer(source, "UTF8_STRING");
wl_data_source_offer(source, "TEXT");
wl_data_device_set_selection(Device::dataDevice, source, serial);
// Push the request so the compositor sees it before the next event
// loop iteration — otherwise a quick read in another app might
// miss the selection update.
wl_display_flush(Device::display);
return true;
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
// CF_UNICODETEXT round-trip. Convert UTF-8 → UTF-16, allocate a
// moveable HGLOBAL, hand it off to the OS. The OS frees the global
// when the next clipboard owner replaces the data, so our caller
// doesn't need to keep `text` alive — same lifetime contract as
// the Wayland path.
if (!OpenClipboard(nullptr)) return false;
EmptyClipboard();
const int wlen = MultiByteToWideChar(CP_UTF8, 0,
text.data(), static_cast<int>(text.size()), nullptr, 0);
if (wlen < 0) { CloseClipboard(); return false; }
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE,
(static_cast<std::size_t>(wlen) + 1) * sizeof(wchar_t));
if (h == nullptr) { CloseClipboard(); return false; }
wchar_t* dst = static_cast<wchar_t*>(GlobalLock(h));
if (dst == nullptr) {
GlobalFree(h);
CloseClipboard();
return false;
}
MultiByteToWideChar(CP_UTF8, 0,
text.data(), static_cast<int>(text.size()), dst, wlen);
dst[wlen] = L'\0';
GlobalUnlock(h);
const bool ok = SetClipboardData(CF_UNICODETEXT, h) != nullptr;
if (!ok) GlobalFree(h); // OS only takes ownership on success
CloseClipboard();
return ok;
#else
(void)text;
return false;
#endif
}

View file

@ -176,158 +176,6 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
constexpr CrafterKeys keysym_to_crafter_key(xkb_keysym_t sym)
{
switch (sym)
{
// Alphabet
case XKB_KEY_a: return CrafterKeys::A;
case XKB_KEY_b: return CrafterKeys::B;
case XKB_KEY_c: return CrafterKeys::C;
case XKB_KEY_d: return CrafterKeys::D;
case XKB_KEY_e: return CrafterKeys::E;
case XKB_KEY_f: return CrafterKeys::F;
case XKB_KEY_g: return CrafterKeys::G;
case XKB_KEY_h: return CrafterKeys::H;
case XKB_KEY_i: return CrafterKeys::I;
case XKB_KEY_j: return CrafterKeys::J;
case XKB_KEY_k: return CrafterKeys::K;
case XKB_KEY_l: return CrafterKeys::L;
case XKB_KEY_m: return CrafterKeys::M;
case XKB_KEY_n: return CrafterKeys::N;
case XKB_KEY_o: return CrafterKeys::O;
case XKB_KEY_p: return CrafterKeys::P;
case XKB_KEY_q: return CrafterKeys::Q;
case XKB_KEY_r: return CrafterKeys::R;
case XKB_KEY_s: return CrafterKeys::S;
case XKB_KEY_t: return CrafterKeys::T;
case XKB_KEY_u: return CrafterKeys::U;
case XKB_KEY_v: return CrafterKeys::V;
case XKB_KEY_w: return CrafterKeys::W;
case XKB_KEY_x: return CrafterKeys::X;
case XKB_KEY_y: return CrafterKeys::Y;
case XKB_KEY_z: return CrafterKeys::Z;
case XKB_KEY_A: return CrafterKeys::A;
case XKB_KEY_B: return CrafterKeys::B;
case XKB_KEY_C: return CrafterKeys::C;
case XKB_KEY_D: return CrafterKeys::D;
case XKB_KEY_E: return CrafterKeys::E;
case XKB_KEY_F: return CrafterKeys::F;
case XKB_KEY_G: return CrafterKeys::G;
case XKB_KEY_H: return CrafterKeys::H;
case XKB_KEY_I: return CrafterKeys::I;
case XKB_KEY_J: return CrafterKeys::J;
case XKB_KEY_K: return CrafterKeys::K;
case XKB_KEY_L: return CrafterKeys::L;
case XKB_KEY_M: return CrafterKeys::M;
case XKB_KEY_N: return CrafterKeys::N;
case XKB_KEY_O: return CrafterKeys::O;
case XKB_KEY_P: return CrafterKeys::P;
case XKB_KEY_Q: return CrafterKeys::Q;
case XKB_KEY_R: return CrafterKeys::R;
case XKB_KEY_S: return CrafterKeys::S;
case XKB_KEY_T: return CrafterKeys::T;
case XKB_KEY_U: return CrafterKeys::U;
case XKB_KEY_V: return CrafterKeys::V;
case XKB_KEY_W: return CrafterKeys::W;
case XKB_KEY_X: return CrafterKeys::X;
case XKB_KEY_Y: return CrafterKeys::Y;
case XKB_KEY_Z: return CrafterKeys::Z;
// Numbers
case XKB_KEY_0: return CrafterKeys::_0;
case XKB_KEY_1: return CrafterKeys::_1;
case XKB_KEY_2: return CrafterKeys::_2;
case XKB_KEY_3: return CrafterKeys::_3;
case XKB_KEY_4: return CrafterKeys::_4;
case XKB_KEY_5: return CrafterKeys::_5;
case XKB_KEY_6: return CrafterKeys::_6;
case XKB_KEY_7: return CrafterKeys::_7;
case XKB_KEY_8: return CrafterKeys::_8;
case XKB_KEY_9: return CrafterKeys::_9;
// Function keys
case XKB_KEY_F1: return CrafterKeys::F1;
case XKB_KEY_F2: return CrafterKeys::F2;
case XKB_KEY_F3: return CrafterKeys::F3;
case XKB_KEY_F4: return CrafterKeys::F4;
case XKB_KEY_F5: return CrafterKeys::F5;
case XKB_KEY_F6: return CrafterKeys::F6;
case XKB_KEY_F7: return CrafterKeys::F7;
case XKB_KEY_F8: return CrafterKeys::F8;
case XKB_KEY_F9: return CrafterKeys::F9;
case XKB_KEY_F10: return CrafterKeys::F10;
case XKB_KEY_F11: return CrafterKeys::F11;
case XKB_KEY_F12: return CrafterKeys::F12;
// Control keys
case XKB_KEY_Escape: return CrafterKeys::Escape;
case XKB_KEY_Tab: return CrafterKeys::Tab;
case XKB_KEY_Return: return CrafterKeys::Enter;
case XKB_KEY_space: return CrafterKeys::Space;
case XKB_KEY_BackSpace: return CrafterKeys::Backspace;
case XKB_KEY_Delete: return CrafterKeys::Delete;
case XKB_KEY_Insert: return CrafterKeys::Insert;
case XKB_KEY_Home: return CrafterKeys::Home;
case XKB_KEY_End: return CrafterKeys::End;
case XKB_KEY_Page_Up: return CrafterKeys::PageUp;
case XKB_KEY_Page_Down: return CrafterKeys::PageDown;
case XKB_KEY_Caps_Lock: return CrafterKeys::CapsLock;
case XKB_KEY_Num_Lock: return CrafterKeys::NumLock;
case XKB_KEY_Scroll_Lock:return CrafterKeys::ScrollLock;
// Modifiers
case XKB_KEY_Shift_L: return CrafterKeys::LeftShift;
case XKB_KEY_Shift_R: return CrafterKeys::RightShift;
case XKB_KEY_Control_L: return CrafterKeys::LeftCtrl;
case XKB_KEY_Control_R: return CrafterKeys::RightCtrl;
case XKB_KEY_Alt_L: return CrafterKeys::LeftAlt;
case XKB_KEY_Alt_R: return CrafterKeys::RightAlt;
case XKB_KEY_Super_L: return CrafterKeys::LeftSuper;
case XKB_KEY_Super_R: return CrafterKeys::RightSuper;
// Arrows
case XKB_KEY_Up: return CrafterKeys::Up;
case XKB_KEY_Down: return CrafterKeys::Down;
case XKB_KEY_Left: return CrafterKeys::Left;
case XKB_KEY_Right: return CrafterKeys::Right;
// Keypad
case XKB_KEY_KP_0: return CrafterKeys::keypad_0;
case XKB_KEY_KP_1: return CrafterKeys::keypad_1;
case XKB_KEY_KP_2: return CrafterKeys::keypad_2;
case XKB_KEY_KP_3: return CrafterKeys::keypad_3;
case XKB_KEY_KP_4: return CrafterKeys::keypad_4;
case XKB_KEY_KP_5: return CrafterKeys::keypad_5;
case XKB_KEY_KP_6: return CrafterKeys::keypad_6;
case XKB_KEY_KP_7: return CrafterKeys::keypad_7;
case XKB_KEY_KP_8: return CrafterKeys::keypad_8;
case XKB_KEY_KP_9: return CrafterKeys::keypad_9;
case XKB_KEY_KP_Enter: return CrafterKeys::keypad_enter;
case XKB_KEY_KP_Add: return CrafterKeys::keypad_plus;
case XKB_KEY_KP_Subtract: return CrafterKeys::keypad_minus;
case XKB_KEY_KP_Multiply: return CrafterKeys::keypad_multiply;
case XKB_KEY_KP_Divide: return CrafterKeys::keypad_divide;
case XKB_KEY_KP_Decimal: return CrafterKeys::keypad_decimal;
// Punctuation
case XKB_KEY_grave: return CrafterKeys::grave;
case XKB_KEY_minus: return CrafterKeys::minus;
case XKB_KEY_equal: return CrafterKeys::equal;
case XKB_KEY_bracketleft: return CrafterKeys::bracket_left;
case XKB_KEY_bracketright:return CrafterKeys::bracket_right;
case XKB_KEY_backslash: return CrafterKeys::backslash;
case XKB_KEY_semicolon: return CrafterKeys::semicolon;
case XKB_KEY_apostrophe: return CrafterKeys::quote;
case XKB_KEY_comma: return CrafterKeys::comma;
case XKB_KEY_period: return CrafterKeys::period;
case XKB_KEY_slash: return CrafterKeys::slash;
default: return CrafterKeys::CrafterKeysMax;
}
}
void Device::xdg_wm_base_handle_ping(void* data, xdg_wm_base* xdg_wm_base, std::uint32_t serial) {
xdg_wm_base_pong(xdg_wm_base, serial);
}
@ -336,8 +184,15 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
if (strcmp(interface, wl_shm_interface.name) == 0) {
shm = reinterpret_cast<wl_shm*>(wl_registry_bind(registry, name, &wl_shm_interface, 1));
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
wl_seat* seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
// Assign to Device::seat (not a fresh local) so SetClipboardText
// and any other code that needs the seat post-init can find it.
seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
wl_seat_add_listener(seat, &seat_listener, nullptr);
// If the manager came in first, the data device couldn't be
// created yet — do it now that we have the seat.
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
} else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) {
compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
@ -349,6 +204,16 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
wpViewporter = reinterpret_cast<wp_viewporter*>(wl_registry_bind(registry, name, &wp_viewporter_interface, 1));
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
fractionalScaleManager = reinterpret_cast<wp_fractional_scale_manager_v1*>(wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1));
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
// v3 gives us the full set of data_source events (target / send /
// cancelled / dnd_*). Universally supported by the compositors
// we target — fall back path is the per-source listener simply
// not getting the v3-only callbacks.
dataDeviceManager = reinterpret_cast<wl_data_device_manager*>(
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3));
if (seat != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
}
}
@ -441,23 +306,24 @@ void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial,
}
void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) {
xkb_keycode_t keycode = key + 8;
xkb_keysym_t keysym = xkb_state_key_get_one_sym(xkb_state, keycode);
CrafterKeys crafterKey = keysym_to_crafter_key(keysym);
// `key` is the kernel input-event-code (KEY_*). That is exactly what
// :Keys returns for Wayland builds, so we store it verbatim with no
// translation. The +8 X11 offset is only needed for the XKB layer,
// which we still consult to produce UTF-8 text.
KeyCode code = key;
xkb_keycode_t xkbKeycode = key + 8;
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (focusedWindow->heldkeys[(std::uint8_t)crafterKey]) {
focusedWindow->onKeyHold[(std::uint8_t)crafterKey].Invoke();
focusedWindow->onAnyKeyHold.Invoke(crafterKey);
if (focusedWindow->heldKeys.contains(code)) {
focusedWindow->onRawKeyHold.Invoke(code);
} else {
focusedWindow->heldkeys[(std::uint8_t)crafterKey] = true;
focusedWindow->onKeyDown[(std::uint8_t)crafterKey].Invoke();
focusedWindow->onAnyKeyDown.Invoke(crafterKey);
focusedWindow->heldKeys.insert(code);
focusedWindow->onRawKeyDown.Invoke(code);
}
std::string buf;
buf.resize(16);
int n = xkb_state_key_get_utf8(xkb_state, keycode, buf.data(), 16);
int n = xkb_state_key_get_utf8(xkb_state, xkbKeycode, buf.data(), 16);
std::string utf8;
if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) {
buf.resize(n);
@ -468,19 +334,18 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui
// Replace the active repeat with this key — most recent press wins,
// matching xkbcommon's typical behaviour and most desktop apps.
keyRepeat.active = (keyRepeat.rate > 0);
keyRepeat.key = crafterKey;
keyRepeat.key = code;
keyRepeat.utf8 = std::move(utf8);
keyRepeat.pressTime = std::chrono::steady_clock::now();
keyRepeat.lastFireTime = keyRepeat.pressTime;
} else {
focusedWindow->heldkeys[(std::uint8_t)crafterKey] = false;
focusedWindow->onKeyUp[(std::uint8_t)crafterKey].Invoke();
focusedWindow->onAnyKeyUp.Invoke(crafterKey);
focusedWindow->heldKeys.erase(code);
focusedWindow->onRawKeyUp.Invoke(code);
// If the released key was the one repeating, stop. Otherwise leave
// the existing repeat alone (user pressed/released a modifier
// mid-repeat etc.).
if (keyRepeat.active && keyRepeat.key == crafterKey) {
if (keyRepeat.active && keyRepeat.key == code) {
keyRepeat.active = false;
keyRepeat.utf8.clear();
}
@ -513,10 +378,8 @@ void Device::TickKeyRepeats() {
// Catch up — emit one event per missed period so a paused frame doesn't
// make the repeat permanently lag behind.
while (now - keyRepeat.lastFireTime >= period) {
focusedWindow->onKeyDown[(std::uint8_t)keyRepeat.key].Invoke();
focusedWindow->onAnyKeyDown.Invoke(keyRepeat.key);
focusedWindow->onKeyHold[(std::uint8_t)keyRepeat.key].Invoke();
focusedWindow->onAnyKeyHold.Invoke(keyRepeat.key);
focusedWindow->onRawKeyDown.Invoke(keyRepeat.key);
focusedWindow->onRawKeyHold.Invoke(keyRepeat.key);
if (!keyRepeat.utf8.empty()) {
focusedWindow->onTextInput.Invoke(keyRepeat.utf8);
}
@ -673,11 +536,40 @@ void Device::Initialize() {
}
}
// Enumerate available device extensions so we can opt into
// VK_EXT_memory_decompression when the driver advertises it. Drivers
// without it (AMD, Intel as of early 2026) get the CPU-decode fallback.
{
std::uint32_t extCount = 0;
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr);
std::vector<VkExtensionProperties> exts(extCount);
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
for (const VkExtensionProperties& e : exts) {
if (std::strcmp(e.extensionName, VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME) == 0) {
memoryDecompressionSupported = true;
break;
}
}
}
// Properties query: chain memory-decompression props only when supported,
// otherwise sType validation flags it as an unrecognized struct on
// drivers that don't expose the extension.
if (memoryDecompressionSupported) {
memoryDecompressionProperties.pNext = const_cast<void*>(rayTracingProperties.pNext);
rayTracingProperties.pNext = &memoryDecompressionProperties;
}
VkPhysicalDeviceProperties2 properties2 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
.pNext = &rayTracingProperties
};
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
if (memoryDecompressionSupported &&
(memoryDecompressionProperties.decompressionMethods & VK_MEMORY_DECOMPRESSION_METHOD_GDEFLATE_1_0_BIT_EXT) == 0) {
memoryDecompressionSupported = false;
}
uint32_t queueFamilyCount;
vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, NULL);
@ -724,9 +616,26 @@ void Device::Initialize() {
.shaderUntypedPointers = VK_TRUE,
};
// Enables synchronization2 sentinels (VkMemoryBarrier2, VK_PIPELINE_STAGE_2_*,
// VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens.
VkPhysicalDeviceVulkan13Features features13 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.pNext = &untypedPointersFeatures,
.synchronization2 = VK_TRUE,
};
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
.pNext = &features13,
.memoryDecompression = VK_TRUE,
};
void* postDecompressChain = memoryDecompressionSupported
? static_cast<void*>(&memoryDecompressionFeatures)
: static_cast<void*>(&features13);
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
.pNext = &untypedPointersFeatures,
.pNext = postDecompressChain,
.descriptorHeap = VK_TRUE,
};
@ -787,12 +696,22 @@ void Device::Initialize() {
}
};
// Build the enabled-extension list dynamically so we can append the
// optional VK_EXT_memory_decompression entry only when the driver
// advertises it.
std::vector<const char*> enabledDeviceExtensions(
std::begin(deviceExtensionNames),
std::end(deviceExtensionNames));
if (memoryDecompressionSupported) {
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
}
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
deviceCreateInfo.enabledExtensionCount = sizeof(deviceExtensionNames) / sizeof(const char*);
deviceCreateInfo.ppEnabledExtensionNames = deviceExtensionNames;
deviceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
deviceCreateInfo.pNext = &physical_features2;
uint32_t deviceLayerCount;
@ -846,6 +765,19 @@ void Device::Initialize() {
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
if (memoryDecompressionSupported) {
// vkGetDeviceProcAddr skips the loader trampoline that vkGetInstanceProcAddr
// requires for device-level functions. The other PFNs above predate this
// realization; opportunistic adoption for new entry points only.
vkCmdDecompressMemoryEXT = reinterpret_cast<PFN_vkCmdDecompressMemoryEXT>(
vkGetDeviceProcAddr(device, "vkCmdDecompressMemoryEXT"));
if (vkCmdDecompressMemoryEXT == nullptr) {
// Driver advertised the extension but didn't expose the entry
// point — defensively fall back to CPU decode.
memoryDecompressionSupported = false;
}
}
}
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties) {

View file

@ -0,0 +1,683 @@
/*
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 as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
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 <libudev.h>
#include <libevdev/libevdev.h>
#include <linux/input.h>
#include <linux/input-event-codes.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <cstring>
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#define COBJMACROS
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <windows.gaming.input.h>
#include <roapi.h>
#include <initguid.h>
#endif
module Crafter.Graphics;
import std;
using namespace Crafter;
// ────────────────────────────────────────────────────────────────────
// Linux backend (libudev + libevdev)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
namespace {
// Per-device runtime state. The public `Gamepad::Device` carries the
// identity + polled state + events; this struct holds the libevdev /
// fd / mapping tables we need to drain events from.
struct LinuxBackend {
libevdev* evdev = nullptr;
int fd = -1;
// Per-axis calibration (libevdev's min/max so we can normalize).
// Indexed by Gamepad::Axis. -1 in `code` means "this axis isn't
// exposed by this device".
struct AxisInfo {
int code = -1; // kernel ABS_* code, or -1
int min = 0;
int max = 0;
float flat = 0.0f; // deadzone hint (raw units)
bool invertY = false; // negate output (stick Y by convention)
};
AxisInfo axisInfo[(std::size_t)Crafter::Gamepad::Axis::Max];
// FF effect id from EVIOCSFF, -1 if no rumble support / not active.
int ffEffectId = -1;
std::chrono::steady_clock::time_point rumbleExpiry{};
};
std::unordered_map<Crafter::Gamepad::Device*, std::unique_ptr<LinuxBackend>> g_backends;
udev* g_udev = nullptr;
udev_monitor* g_monitor = nullptr;
bool g_initialized = false;
std::uint32_t SyspathToId(const char* syspath) {
// FNV-1a 32-bit, stable across runs for the same physical device.
std::uint32_t h = 0x811C9DC5u;
for (const char* p = syspath; *p; ++p) {
h ^= static_cast<std::uint8_t>(*p);
h *= 0x01000193u;
}
// Reserve 0 as "unmapped".
return h == 0 ? 1 : h;
}
// BTN_* → Gamepad::Button. -1 = not a button we care about.
int ButtonFromKernel(int code) {
using B = Crafter::Gamepad::Button;
switch (code) {
case BTN_SOUTH: return (int)B::South;
case BTN_EAST: return (int)B::East;
case BTN_NORTH: return (int)B::North;
case BTN_WEST: return (int)B::West;
case BTN_TL: return (int)B::LeftBumper;
case BTN_TR: return (int)B::RightBumper;
case BTN_TL2: return (int)B::LeftTrigger;
case BTN_TR2: return (int)B::RightTrigger;
case BTN_SELECT: return (int)B::Select;
case BTN_START: return (int)B::Start;
case BTN_MODE: return (int)B::Home;
case BTN_THUMBL: return (int)B::LeftStickClick;
case BTN_THUMBR: return (int)B::RightStickClick;
case BTN_DPAD_UP: return (int)B::DPadUp;
case BTN_DPAD_DOWN: return (int)B::DPadDown;
case BTN_DPAD_LEFT: return (int)B::DPadLeft;
case BTN_DPAD_RIGHT: return (int)B::DPadRight;
default: return -1;
}
}
// Populate `bp->axisInfo` from libevdev. Sticks use ABS_X/Y/RX/RY;
// triggers prefer ABS_Z/RZ but fall back to ABS_BRAKE/GAS.
void DiscoverAxes(LinuxBackend* bp) {
using A = Crafter::Gamepad::Axis;
auto setup = [&](A which, int code, bool invertY = false) {
if (!libevdev_has_event_code(bp->evdev, EV_ABS, code)) return;
auto* info = libevdev_get_abs_info(bp->evdev, code);
if (!info) return;
auto& slot = bp->axisInfo[(std::size_t)which];
slot.code = code;
slot.min = info->minimum;
slot.max = info->maximum;
slot.flat = (float)info->flat;
slot.invertY = invertY;
};
setup(A::LeftStickX, ABS_X);
setup(A::LeftStickY, ABS_Y, /*invertY=*/true);
setup(A::RightStickX, ABS_RX);
setup(A::RightStickY, ABS_RY, /*invertY=*/true);
// Trigger axis: Z/RZ on Xbox, BRAKE/GAS on some others.
if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_Z)) {
setup(A::LeftTrigger, ABS_Z);
} else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_BRAKE)) {
setup(A::LeftTrigger, ABS_BRAKE);
}
if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_RZ)) {
setup(A::RightTrigger, ABS_RZ);
} else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_GAS)) {
setup(A::RightTrigger, ABS_GAS);
}
}
// Normalize a raw axis value to -1..1 (sticks) or 0..1 (triggers).
float NormalizeAxis(const LinuxBackend::AxisInfo& info, int raw, bool isTrigger) {
if (info.code < 0 || info.max == info.min) return 0.0f;
float v;
if (isTrigger) {
v = float(raw - info.min) / float(info.max - info.min);
if (v < 0.0f) v = 0.0f;
if (v > 1.0f) v = 1.0f;
} else {
float mid = 0.5f * (info.max + info.min);
float halfR = 0.5f * (info.max - info.min);
v = (raw - mid) / halfR;
if (v < -1.0f) v = -1.0f;
if (v > 1.0f) v = 1.0f;
if (info.invertY) v = -v;
}
return v;
}
bool IsGamepadDevice(libevdev* evdev) {
// A real gamepad reports BTN_GAMEPAD (a kernel alias for BTN_SOUTH
// when the device is classified as a gamepad). This filters out
// keyboards/mice/touchpads/tablets that share the evdev interface.
return libevdev_has_event_type(evdev, EV_KEY)
&& libevdev_has_event_code(evdev, EV_KEY, BTN_GAMEPAD);
}
Crafter::Gamepad::Device* OpenDevice(const char* devnode, const char* syspath, const char* name) {
int fd = ::open(devnode, O_RDWR | O_NONBLOCK);
if (fd < 0) {
// Read-only is fine if we just can't rumble.
fd = ::open(devnode, O_RDONLY | O_NONBLOCK);
if (fd < 0) return nullptr;
}
libevdev* evdev = nullptr;
if (libevdev_new_from_fd(fd, &evdev) < 0) {
::close(fd);
return nullptr;
}
if (!IsGamepadDevice(evdev)) {
libevdev_free(evdev);
::close(fd);
return nullptr;
}
auto dev = std::make_unique<Crafter::Gamepad::Device>();
dev->id = SyspathToId(syspath);
dev->name = name ? name : libevdev_get_name(evdev);
auto bp = std::make_unique<LinuxBackend>();
bp->evdev = evdev;
bp->fd = fd;
DiscoverAxes(bp.get());
auto* raw = dev.get();
g_backends[raw] = std::move(bp);
Crafter::Gamepad::connected.push_back(std::move(dev));
return raw;
}
void CloseDevice(Crafter::Gamepad::Device* dev) {
auto it = g_backends.find(dev);
if (it != g_backends.end()) {
if (it->second->evdev) libevdev_free(it->second->evdev);
if (it->second->fd >= 0) ::close(it->second->fd);
g_backends.erase(it);
}
auto& v = Crafter::Gamepad::connected;
for (auto vi = v.begin(); vi != v.end(); ++vi) {
if (vi->get() == dev) { v.erase(vi); break; }
}
}
void EnumerateExisting() {
udev_enumerate* en = udev_enumerate_new(g_udev);
if (!en) return;
udev_enumerate_add_match_subsystem(en, "input");
udev_enumerate_scan_devices(en);
udev_list_entry* list = udev_enumerate_get_list_entry(en);
udev_list_entry* entry;
udev_list_entry_foreach(entry, list) {
const char* syspath = udev_list_entry_get_name(entry);
udev_device* d = udev_device_new_from_syspath(g_udev, syspath);
if (!d) continue;
const char* devnode = udev_device_get_devnode(d);
const char* sysname = udev_device_get_sysname(d);
if (devnode && sysname && std::strncmp(sysname, "event", 5) == 0) {
const char* name = udev_device_get_sysattr_value(
udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
"name");
auto* dev = OpenDevice(devnode, syspath, name);
if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
}
udev_device_unref(d);
}
udev_enumerate_unref(en);
}
void HandleHotplugEvent(udev_device* d) {
const char* action = udev_device_get_action(d);
const char* devnode = udev_device_get_devnode(d);
const char* sysname = udev_device_get_sysname(d);
const char* syspath = udev_device_get_syspath(d);
if (!action || !devnode || !sysname || !syspath) return;
if (std::strncmp(sysname, "event", 5) != 0) return;
if (std::strcmp(action, "add") == 0) {
const char* name = udev_device_get_sysattr_value(
udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
"name");
auto* dev = OpenDevice(devnode, syspath, name);
if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
} else if (std::strcmp(action, "remove") == 0) {
std::uint32_t id = SyspathToId(syspath);
for (auto& up : Crafter::Gamepad::connected) {
if (up->id == id) {
Crafter::Gamepad::Device* dev = up.get();
Crafter::Gamepad::onDisconnected.Invoke(dev);
CloseDevice(dev);
return;
}
}
}
}
void DrainHotplug() {
if (!g_monitor) return;
for (;;) {
udev_device* d = udev_monitor_receive_device(g_monitor);
if (!d) break;
HandleHotplugEvent(d);
udev_device_unref(d);
}
}
void DrainDeviceEvents(Crafter::Gamepad::Device* dev, LinuxBackend* bp) {
using A = Crafter::Gamepad::Axis;
using B = Crafter::Gamepad::Button;
// Walk libevdev's internal queue; SYNC means we missed events and
// need to resync the state. We treat the synced events the same
// way as normal events — net effect is the polled state ends up
// correct and the event stream reports the deltas.
input_event ev;
int rc;
while ((rc = libevdev_next_event(bp->evdev,
LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC,
&ev)) >= 0)
{
if (rc == LIBEVDEV_READ_STATUS_SYNC) continue; // handled in next iter
if (ev.type == EV_KEY) {
int b = ButtonFromKernel(ev.code);
if (b < 0) continue;
bool pressed = (ev.value != 0);
if (dev->buttons[b] == pressed) continue;
dev->buttons[b] = pressed;
if (pressed) dev->onButtonDown.Invoke((B)b);
else dev->onButtonUp .Invoke((B)b);
} else if (ev.type == EV_ABS) {
// ABS_HAT0X / ABS_HAT0Y emulate the D-pad on some
// controllers. Translate to the four DPad buttons so
// bindings can be uniform.
if (ev.code == ABS_HAT0X) {
bool left = ev.value < 0;
bool right = ev.value > 0;
auto edge = [&](int idx, bool now) {
if (dev->buttons[idx] == now) return;
dev->buttons[idx] = now;
if (now) dev->onButtonDown.Invoke((B)idx);
else dev->onButtonUp .Invoke((B)idx);
};
edge((int)B::DPadLeft, left);
edge((int)B::DPadRight, right);
continue;
}
if (ev.code == ABS_HAT0Y) {
bool up = ev.value < 0;
bool down = ev.value > 0;
auto edge = [&](int idx, bool now) {
if (dev->buttons[idx] == now) return;
dev->buttons[idx] = now;
if (now) dev->onButtonDown.Invoke((B)idx);
else dev->onButtonUp .Invoke((B)idx);
};
edge((int)B::DPadUp, up);
edge((int)B::DPadDown, down);
continue;
}
// Look up which Gamepad::Axis this kernel code maps to
// (by scanning the calibration table — sticks first,
// triggers second).
for (std::size_t i = 0; i < (std::size_t)A::Max; ++i) {
if (bp->axisInfo[i].code != ev.code) continue;
bool isTrigger = (i == (std::size_t)A::LeftTrigger
|| i == (std::size_t)A::RightTrigger);
float v = NormalizeAxis(bp->axisInfo[i], ev.value, isTrigger);
if (dev->axes[i] == v) break;
dev->axes[i] = v;
dev->onAxisChanged.Invoke((A)i);
break;
}
}
}
}
void ExpireRumbles() {
auto now = std::chrono::steady_clock::now();
for (auto& [dev, bp] : g_backends) {
if (bp->ffEffectId >= 0 && now >= bp->rumbleExpiry) {
input_event stop{};
stop.type = EV_FF;
stop.code = bp->ffEffectId;
stop.value = 0;
if (bp->fd >= 0) ::write(bp->fd, &stop, sizeof(stop));
ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
bp->ffEffectId = -1;
}
}
}
void EnsureInit() {
if (g_initialized) return;
g_initialized = true;
g_udev = udev_new();
if (!g_udev) return;
g_monitor = udev_monitor_new_from_netlink(g_udev, "udev");
if (g_monitor) {
udev_monitor_filter_add_match_subsystem_devtype(g_monitor, "input", nullptr);
udev_monitor_enable_receiving(g_monitor);
}
EnumerateExisting();
}
}
void Crafter::Gamepad::Tick() {
EnsureInit();
DrainHotplug();
for (auto& up : Gamepad::connected) {
Device* dev = up.get();
auto it = g_backends.find(dev);
if (it == g_backends.end()) continue;
DrainDeviceEvents(dev, it->second.get());
}
ExpireRumbles();
}
void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
std::chrono::milliseconds duration)
{
auto it = g_backends.find(&dev);
if (it == g_backends.end()) return;
LinuxBackend* bp = it->second.get();
if (bp->fd < 0) return;
// Cancel any active effect first.
if (bp->ffEffectId >= 0) {
input_event stop{};
stop.type = EV_FF; stop.code = bp->ffEffectId; stop.value = 0;
::write(bp->fd, &stop, sizeof(stop));
ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
bp->ffEffectId = -1;
}
if (duration.count() <= 0 || (low <= 0.0f && high <= 0.0f)) return;
auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
ff_effect effect{};
effect.type = FF_RUMBLE;
effect.id = -1;
effect.u.rumble.strong_magnitude = (std::uint16_t)(clamp01(low) * 0xFFFFu);
effect.u.rumble.weak_magnitude = (std::uint16_t)(clamp01(high) * 0xFFFFu);
effect.replay.length = (std::uint16_t)std::min<std::int64_t>(duration.count(), 0xFFFF);
if (ioctl(bp->fd, EVIOCSFF, &effect) < 0) return;
bp->ffEffectId = effect.id;
bp->rumbleExpiry = std::chrono::steady_clock::now() + duration;
input_event play{};
play.type = EV_FF;
play.code = effect.id;
play.value = 1;
::write(bp->fd, &play, sizeof(play));
}
#endif
// ────────────────────────────────────────────────────────────────────
// Win32 backend (Windows.Gaming.Input via C ABI)
// ────────────────────────────────────────────────────────────────────
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
namespace {
using IGamepad = __x_ABI_CWindows_CGaming_CInput_CIGamepad;
using IGamepadStatics = __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics;
using GamepadAddedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
using GamepadRemovedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
using GamepadReading = __x_ABI_CWindows_CGaming_CInput_CGamepadReading;
using GamepadVibration = __x_ABI_CWindows_CGaming_CInput_CGamepadVibration;
using GamepadButtons = __x_ABI_CWindows_CGaming_CInput_CGamepadButtons;
IGamepadStatics* g_statics = nullptr;
EventRegistrationToken g_addedToken {};
EventRegistrationToken g_removedToken {};
bool g_initialized = false;
// Cross-thread queue. WGI fires Added/Removed on a thread pool; the
// event loop drains this on the main thread inside Tick().
struct QueueEntry {
bool added;
IGamepad* gamepad; // AddRef'd
};
std::mutex g_queueMutex;
std::vector<QueueEntry> g_queue;
// Maps a live IGamepad* to its Crafter::Gamepad::Device. Pointer is
// stable from WGI's perspective for the lifetime of the connection.
std::unordered_map<IGamepad*, Crafter::Gamepad::Device*> g_byGamepad;
std::uint32_t PointerToId(IGamepad* g) {
// Hash the pointer for a stable-while-connected id. (NonRoamableId
// would be more durable across reconnects but adds another COM
// round-trip; pointer hash is fine for v1.)
std::uintptr_t v = reinterpret_cast<std::uintptr_t>(g);
v = (v ^ (v >> 16)) * 0x85EBCA6Bu;
v = (v ^ (v >> 13)) * 0xC2B2AE35u;
v = v ^ (v >> 16);
std::uint32_t out = (std::uint32_t)v;
return out == 0 ? 1 : out;
}
// ── Delegate vtable for GamepadAdded / GamepadRemoved ───────────
struct AddedDelegate {
// The first member matches the C ABI vtable layout. Cast yields
// a valid GamepadAddedHandler*.
void* vtbl;
LONG refCount;
bool isAdded;
};
extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl;
extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl;
HRESULT STDMETHODCALLTYPE DelegateQI(GamepadAddedHandler* This, REFIID riid, void** out) {
if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IAgileObject) ||
IsEqualIID(riid, &IID___FIEventHandler_1_Windows__CGaming__CInput__CGamepad))
{
*out = This;
This->lpVtbl->AddRef(This);
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE DelegateAddRef(GamepadAddedHandler* This) {
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
return (ULONG)InterlockedIncrement(&d->refCount);
}
ULONG STDMETHODCALLTYPE DelegateRelease(GamepadAddedHandler* This) {
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
LONG r = InterlockedDecrement(&d->refCount);
return (ULONG)r;
}
HRESULT STDMETHODCALLTYPE DelegateInvoke(GamepadAddedHandler* This, IInspectable* sender, IGamepad* gamepad) {
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
if (gamepad) IGamepad_AddRef(gamepad);
{
std::lock_guard<std::mutex> lock(g_queueMutex);
g_queue.push_back({ d->isAdded, gamepad });
}
return S_OK;
}
GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl = {
DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
};
GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl = {
DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
};
AddedDelegate g_addedDelegate { &g_addedVtbl, 1, true };
AddedDelegate g_removedDelegate { &g_removedVtbl, 1, false };
// Translate GamepadReading.Buttons bitfield to our Button enum.
bool ReadingHas(const GamepadReading& r, GamepadButtons b) {
return (r.Buttons & b) != 0;
}
void ApplyReading(Crafter::Gamepad::Device* dev, const GamepadReading& r) {
using B = Crafter::Gamepad::Button;
using A = Crafter::Gamepad::Axis;
struct Map { B which; GamepadButtons mask; };
static const Map buttonMap[] = {
{ B::DPadUp, GamepadButtons_DPadUp },
{ B::DPadDown, GamepadButtons_DPadDown },
{ B::DPadLeft, GamepadButtons_DPadLeft },
{ B::DPadRight, GamepadButtons_DPadRight },
{ B::Start, GamepadButtons_Menu },
{ B::Select, GamepadButtons_View },
{ B::LeftStickClick, GamepadButtons_LeftThumbstick },
{ B::RightStickClick,GamepadButtons_RightThumbstick },
{ B::LeftBumper, GamepadButtons_LeftShoulder },
{ B::RightBumper, GamepadButtons_RightShoulder },
{ B::South, GamepadButtons_A },
{ B::East, GamepadButtons_B },
{ B::West, GamepadButtons_X },
{ B::North, GamepadButtons_Y },
};
for (const Map& m : buttonMap) {
bool now = ReadingHas(r, m.mask);
bool& cur = dev->buttons[(std::size_t)m.which];
if (cur == now) continue;
cur = now;
if (now) dev->onButtonDown.Invoke(m.which);
else dev->onButtonUp .Invoke(m.which);
}
// WGI doesn't have a Home/Guide button in the standard Gamepad
// interface (it's reserved for the OS). Leave dev->buttons[Home]
// at false.
auto axis = [&](A which, float v) {
float& cur = dev->axes[(std::size_t)which];
if (cur == v) return;
cur = v;
dev->onAxisChanged.Invoke(which);
};
axis(A::LeftStickX, (float)r.LeftThumbstickX);
axis(A::LeftStickY, (float)r.LeftThumbstickY);
axis(A::RightStickX, (float)r.RightThumbstickX);
axis(A::RightStickY, (float)r.RightThumbstickY);
axis(A::LeftTrigger, (float)r.LeftTrigger);
axis(A::RightTrigger, (float)r.RightTrigger);
// Triggers also expose digital "pressed" buttons at a threshold.
constexpr float kTriggerThreshold = 0.1f;
auto trigBtn = [&](B which, double value) {
bool now = value >= kTriggerThreshold;
bool& cur = dev->buttons[(std::size_t)which];
if (cur == now) return;
cur = now;
if (now) dev->onButtonDown.Invoke(which);
else dev->onButtonUp .Invoke(which);
};
trigBtn(B::LeftTrigger, r.LeftTrigger);
trigBtn(B::RightTrigger, r.RightTrigger);
}
void EnsureInit() {
if (g_initialized) return;
g_initialized = true;
// RoInitialize is required once per thread; ignore RPC_E_CHANGED_MODE
// (some hosts initialize as STA, that's fine for WGI).
RoInitialize(RO_INIT_MULTITHREADED);
HSTRING_HEADER header;
HSTRING className;
const wchar_t* name = RuntimeClass_Windows_Gaming_Input_Gamepad;
if (FAILED(WindowsCreateStringReference(name,
(UINT32)wcslen(name), &header, &className))) return;
if (FAILED(RoGetActivationFactory(className,
&IID___x_ABI_CWindows_CGaming_CInput_CIGamepadStatics,
(void**)&g_statics))) return;
__x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadAdded(
g_statics,
reinterpret_cast<GamepadAddedHandler*>(&g_addedDelegate),
&g_addedToken);
__x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadRemoved(
g_statics,
reinterpret_cast<GamepadAddedHandler*>(&g_removedDelegate),
&g_removedToken);
}
}
void Crafter::Gamepad::Tick() {
EnsureInit();
if (!g_statics) return;
// Drain the cross-thread connect/disconnect queue.
std::vector<QueueEntry> pending;
{
std::lock_guard<std::mutex> lock(g_queueMutex);
pending.swap(g_queue);
}
for (QueueEntry& e : pending) {
if (!e.gamepad) continue;
if (e.added) {
if (g_byGamepad.find(e.gamepad) != g_byGamepad.end()) {
IGamepad_Release(e.gamepad);
continue;
}
auto dev = std::make_unique<Crafter::Gamepad::Device>();
dev->id = PointerToId(e.gamepad);
dev->name = "Gamepad";
auto* raw = dev.get();
g_byGamepad[e.gamepad] = raw;
connected.push_back(std::move(dev));
onConnected.Invoke(raw);
// keep AddRef'd; released on disconnect
} else {
auto it = g_byGamepad.find(e.gamepad);
if (it != g_byGamepad.end()) {
Crafter::Gamepad::Device* raw = it->second;
onDisconnected.Invoke(raw);
for (auto vi = connected.begin(); vi != connected.end(); ++vi) {
if (vi->get() == raw) { connected.erase(vi); break; }
}
g_byGamepad.erase(it);
}
IGamepad_Release(e.gamepad);
}
}
// Poll each connected gamepad.
for (auto& [gp, dev] : g_byGamepad) {
GamepadReading reading{};
if (FAILED(__x_ABI_CWindows_CGaming_CInput_CIGamepad_GetCurrentReading(gp, &reading))) continue;
ApplyReading(dev, reading);
}
}
void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
std::chrono::milliseconds /*duration*/)
{
// WGI vibration is "set and forget" — no duration. Caller can stop
// by re-calling with zero magnitudes. We honor 0 duration as stop.
IGamepad* gp = nullptr;
for (auto& [g, d] : g_byGamepad) {
if (d == &dev) { gp = g; break; }
}
if (!gp) return;
auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
GamepadVibration vib{};
vib.LeftMotor = clamp01(low);
vib.RightMotor = clamp01(high);
__x_ABI_CWindows_CGaming_CInput_CIGamepad_put_Vibration(gp, vib);
}
#endif
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
for (auto& up : connected) {
if (up->id == id) return up.get();
}
return nullptr;
}

View file

@ -0,0 +1,454 @@
/*
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 as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
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;
module Crafter.Graphics;
import std;
import Crafter.Math;
import Crafter.Event;
using namespace Crafter;
using namespace Crafter::Input;
// ────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────
namespace {
// Look up a connected gamepad by stable id. Returns nullptr if not
// currently connected — the binding becomes inert until reconnect.
Gamepad::Device* FindGamepad(std::uint32_t id) {
return Gamepad::FindById(id);
}
float ApplyDeadzone(float v, float dz) {
float a = std::abs(v);
if (a <= dz) return 0.0f;
float sign = v < 0 ? -1.0f : 1.0f;
return sign * (a - dz) / (1.0f - dz);
}
Vector<float, 2> ApplyRadialDeadzone(Vector<float, 2> v, float dz) {
float mag = std::sqrt(v.x * v.x + v.y * v.y);
if (mag <= dz) return Vector<float, 2>{0.0f, 0.0f};
float scaled = (mag - dz) / (1.0f - dz);
float k = scaled / mag;
return Vector<float, 2>{ v.x * k, v.y * k };
}
// ─── Evaluate one binding to its raw contribution. ───────────────
// Buttons: bool. Axis: float. Vector2: pair.
struct Eval {
bool button = false;
float value = 0.0f;
Vector<float, 2> vec { 0.0f, 0.0f };
};
Eval EvalBinding(const Binding& b, Window* w, std::int32_t scrollAccum,
Vector<float, 2> mouseDelta, float deadzone)
{
Eval r;
std::visit([&](auto const& bb) {
using T = std::decay_t<decltype(bb)>;
if constexpr (std::is_same_v<T, KeyBind>) {
if (w && w->heldKeys.contains(bb.code)) r.button = true;
r.value = r.button ? 1.0f : 0.0f;
} else if constexpr (std::is_same_v<T, MouseButtonBind>) {
if (w) {
if (bb.button == 0 && w->mouseLeftHeld) r.button = true;
if (bb.button == 1 && w->mouseRightHeld) r.button = true;
}
r.value = r.button ? 1.0f : 0.0f;
} else if constexpr (std::is_same_v<T, MouseScrollBind>) {
r.value = (float)scrollAccum;
r.button = scrollAccum != 0;
} else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
r.vec = Vector<float, 2>{
mouseDelta.x * bb.scale,
mouseDelta.y * bb.scale
};
} else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
auto* gp = FindGamepad(bb.gamepadId);
if (gp) r.button = gp->buttons[(std::size_t)bb.button];
r.value = r.button ? 1.0f : 0.0f;
} else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
auto* gp = FindGamepad(bb.gamepadId);
if (gp) {
float v = gp->axes[(std::size_t)bb.axis];
if (bb.invert) v = -v;
// Triggers (0..1) and sticks (-1..1) share this path;
// deadzone applies symmetrically.
r.value = ApplyDeadzone(v, deadzone);
r.button = std::abs(r.value) > 0.0f;
}
} else if constexpr (std::is_same_v<T, GamepadStickBind>) {
auto* gp = FindGamepad(bb.gamepadId);
if (gp) {
using A = Gamepad::Axis;
A xAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickX : A::RightStickX;
A yAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickY : A::RightStickY;
Vector<float, 2> raw{
gp->axes[(std::size_t)xAxis],
gp->axes[(std::size_t)yAxis]
};
r.vec = ApplyRadialDeadzone(raw, deadzone);
}
} else if constexpr (std::is_same_v<T, WASDBind>) {
Vector<float, 2> v{ 0.0f, 0.0f };
if (w) {
if (w->heldKeys.contains(bb.left)) v.x -= 1.0f;
if (w->heldKeys.contains(bb.right)) v.x += 1.0f;
if (w->heldKeys.contains(bb.up)) v.y += 1.0f;
if (w->heldKeys.contains(bb.down)) v.y -= 1.0f;
}
r.vec = v;
}
}, b);
return r;
}
// Combine bindings for the action's type. For Button: OR. For Axis1D:
// sum-then-clamp (so two analog inputs add but cap at ±1; two
// digital inputs cap at 1 too). For Vector2: per-axis sum-and-clamp.
void EvaluateAction(Action& a, Window* w, std::int32_t scrollAccum,
Vector<float, 2> mouseDelta)
{
bool nextPressed = false;
float nextValue = 0.0f;
Vector<float, 2> nextVec { 0.0f, 0.0f };
for (const Binding& b : a.bindings) {
Eval e = EvalBinding(b, w, scrollAccum, mouseDelta, a.deadzone);
if (e.button) nextPressed = true;
nextValue += e.value;
nextVec.x += e.vec.x;
nextVec.y += e.vec.y;
}
// Clamp scalars/vectors to ±1; sum semantics for combining.
auto clamp = [](float v) { return v < -1.0f ? -1.0f : v > 1.0f ? 1.0f : v; };
nextValue = clamp(nextValue);
nextVec.x = clamp(nextVec.x);
nextVec.y = clamp(nextVec.y);
// Dispatch edges + value changes.
if (a.type == ActionType::Button) {
if (nextPressed && !a.pressed) {
a.pressed = true;
a.onPerformed.Invoke();
} else if (!nextPressed && a.pressed) {
a.pressed = false;
a.onCanceled.Invoke();
}
} else if (a.type == ActionType::Axis1D) {
bool wasNonZero = a.value != 0.0f;
bool isNonZero = nextValue != 0.0f;
if (nextValue != a.value) {
a.value = nextValue;
a.onValueChanged.Invoke(nextValue);
}
if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
} else { // Vector2
bool wasNonZero = (a.vector2.x != 0.0f || a.vector2.y != 0.0f);
bool isNonZero = (nextVec.x != 0.0f || nextVec.y != 0.0f);
if (nextVec.x != a.vector2.x || nextVec.y != a.vector2.y) {
a.vector2 = nextVec;
a.onVector2Changed.Invoke(nextVec);
}
if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
}
}
// ─── Rebind detection ───────────────────────────────────────────
// Scan for a fresh down-edge against the snapshot taken at
// StartRebind. Returns the captured binding if found, nullopt if
// nothing fresh is held. The first match wins; preference goes
// keyboard > mouse > gamepad to keep the behavior predictable.
std::optional<Binding> DetectRebindEdge(Map::RebindState& s, Window* w) {
// Keyboard
if (w && HasFlag(s.mask, CaptureMask::Keyboard)) {
for (KeyCode c : w->heldKeys) {
if (!s.keysHeldAtStart.contains(c)) {
return Binding{ KeyBind{c} };
}
}
}
// Mouse buttons
if (w && HasFlag(s.mask, CaptureMask::Mouse)) {
if (w->mouseLeftHeld && !s.mouseLeftHeldAtStart) {
return Binding{ MouseButtonBind{0} };
}
if (w->mouseRightHeld && !s.mouseRightHeldAtStart) {
return Binding{ MouseButtonBind{1} };
}
}
// Gamepad buttons + axes
if (HasFlag(s.mask, CaptureMask::Gamepad)) {
for (auto& up : Gamepad::connected) {
Gamepad::Device* gp = up.get();
auto it = s.gamepadButtonsAtStart.find(gp->id);
bool hadSnapshot = it != s.gamepadButtonsAtStart.end();
for (std::size_t i = 0; i < (std::size_t)Gamepad::Button::Max; ++i) {
bool now = gp->buttons[i];
bool before = hadSnapshot ? it->second[i] : false;
if (now && !before) {
return Binding{ GamepadButtonBind{
gp->id, (Gamepad::Button)i
} };
}
}
// Axes: any value past 0.5 captures as a 1-D axis bind.
constexpr float kRebindAxisThreshold = 0.5f;
for (std::size_t i = 0; i < (std::size_t)Gamepad::Axis::Max; ++i) {
float v = gp->axes[i];
if (std::abs(v) >= kRebindAxisThreshold) {
return Binding{ GamepadAxisBind{
gp->id, (Gamepad::Axis)i, v < 0.0f
} };
}
}
}
}
return std::nullopt;
}
}
// ────────────────────────────────────────────────────────────────────
// Map
// ────────────────────────────────────────────────────────────────────
Action& Map::AddAction(std::string name, ActionType type) {
auto up = std::make_unique<Action>();
up->name = std::move(name);
up->type = type;
Action& ref = *up;
actions.push_back(std::move(up));
return ref;
}
Action* Map::Find(std::string_view name) {
for (auto& up : actions) {
if (up->name == name) return up.get();
}
return nullptr;
}
void Map::Attach(Window& w) {
Detach();
window = &w;
// Only event-driven source we need is mouse scroll — everything
// else (held keys, mouse buttons, mouse position, gamepad state) is
// polled directly. Accumulate scroll between ticks so a flick that
// fires multiple wheel events in one frame all count.
scrollListener = std::make_unique<EventListener<std::uint32_t>>(
&w.onMouseScroll,
[this](std::uint32_t delta) {
// Wayland and Win32 both deliver scroll as a signed delta
// packed into a uint32; we reinterpret to preserve sign.
scrollAccumulator += (std::int32_t)delta;
});
lastMousePos.reset();
scrollAccumulator = 0;
}
void Map::Detach() {
if (scrollListener) {
scrollListener->Clear();
scrollListener.reset();
}
window = nullptr;
scrollAccumulator = 0;
lastMousePos.reset();
rebind.reset();
}
void Map::Tick() {
// Mouse delta: derived from window position vs. last frame.
Vector<float, 2> mouseDelta{ 0.0f, 0.0f };
if (window) {
if (lastMousePos.has_value()) {
mouseDelta.x = window->currentMousePos.x - lastMousePos->x;
mouseDelta.y = window->currentMousePos.y - lastMousePos->y;
}
lastMousePos = window->currentMousePos;
}
// Rebind takes priority. If a fresh edge is detected, fire the
// callback and exit rebind mode. Actions don't evaluate this tick
// for the captured input — the user usually wants the bind to take
// effect on the NEXT press of that input.
if (rebind.has_value()) {
if (auto captured = DetectRebindEdge(*rebind, window)) {
auto cb = std::move(rebind->onCaptured);
rebind.reset();
// Drain scroll so the next Tick doesn't double-fire on the
// same wheel input that may have triggered the rebind.
scrollAccumulator = 0;
cb(*captured);
return;
}
}
std::int32_t scrollThisTick = scrollAccumulator;
scrollAccumulator = 0;
for (auto& up : actions) {
EvaluateAction(*up, window, scrollThisTick, mouseDelta);
}
}
void Map::StartRebind(Action& action, CaptureMask mask,
std::function<void(Binding)> onCaptured)
{
RebindState s;
s.action = &action;
s.mask = mask;
s.onCaptured = std::move(onCaptured);
if (window) {
s.keysHeldAtStart = window->heldKeys;
s.mouseLeftHeldAtStart = window->mouseLeftHeld;
s.mouseRightHeldAtStart = window->mouseRightHeld;
}
for (auto& up : Gamepad::connected) {
std::array<bool, (std::size_t)Gamepad::Button::Max> snap{};
for (std::size_t i = 0; i < snap.size(); ++i) {
snap[i] = up->buttons[i];
}
s.gamepadButtonsAtStart[up->id] = snap;
}
rebind.emplace(std::move(s));
}
void Map::StopRebind() {
rebind.reset();
}
bool Map::IsRebinding() const {
return rebind.has_value();
}
// ────────────────────────────────────────────────────────────────────
// Serialization
// ────────────────────────────────────────────────────────────────────
std::string Crafter::Input::BindingToString(const Binding& b) {
std::string out;
std::visit([&](auto const& bb) {
using T = std::decay_t<decltype(bb)>;
if constexpr (std::is_same_v<T, KeyBind>) {
out = std::format("key:{:x}", bb.code);
} else if constexpr (std::is_same_v<T, MouseButtonBind>) {
out = std::format("mb:{}", (int)bb.button);
} else if constexpr (std::is_same_v<T, MouseScrollBind>) {
out = "mscroll";
} else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
out = std::format("mdelta:{}", bb.scale);
} else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
out = std::format("gpb:{}:{}", bb.gamepadId, (int)bb.button);
} else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
out = std::format("gpa:{}:{}:{}", bb.gamepadId, (int)bb.axis, bb.invert ? 1 : 0);
} else if constexpr (std::is_same_v<T, GamepadStickBind>) {
out = std::format("gps:{}:{}", bb.gamepadId, (int)bb.stick);
} else if constexpr (std::is_same_v<T, WASDBind>) {
out = std::format("wasd:{:x}:{:x}:{:x}:{:x}",
bb.up, bb.down, bb.left, bb.right);
}
}, b);
return out;
}
namespace {
// Split on ':' and parse. Tolerates leading whitespace; rejects
// malformed input by returning nullopt.
std::vector<std::string_view> Split(std::string_view s) {
std::vector<std::string_view> parts;
std::size_t start = 0;
for (std::size_t i = 0; i < s.size(); ++i) {
if (s[i] == ':') {
parts.emplace_back(s.substr(start, i - start));
start = i + 1;
}
}
parts.emplace_back(s.substr(start));
return parts;
}
bool ParseHex(std::string_view s, std::uint32_t& out) {
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 16);
return ec == std::errc{} && p == s.data() + s.size();
}
bool ParseDec(std::string_view s, std::uint32_t& out) {
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 10);
return ec == std::errc{} && p == s.data() + s.size();
}
bool ParseFloat(std::string_view s, float& out) {
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out);
return ec == std::errc{} && p == s.data() + s.size();
}
}
std::optional<Binding> Crafter::Input::BindingFromString(std::string_view s) {
auto parts = Split(s);
if (parts.empty()) return std::nullopt;
const std::string_view tag = parts[0];
if (tag == "key" && parts.size() == 2) {
std::uint32_t v;
if (!ParseHex(parts[1], v)) return std::nullopt;
return Binding{ KeyBind{ v } };
}
if (tag == "mb" && parts.size() == 2) {
std::uint32_t v;
if (!ParseDec(parts[1], v) || v > 255) return std::nullopt;
return Binding{ MouseButtonBind{ (std::uint8_t)v } };
}
if (tag == "mscroll") return Binding{ MouseScrollBind{} };
if (tag == "mdelta" && parts.size() == 2) {
float scale;
if (!ParseFloat(parts[1], scale)) return std::nullopt;
return Binding{ MouseDeltaBind{ scale } };
}
if (tag == "gpb" && parts.size() == 3) {
std::uint32_t id, btn;
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], btn)) return std::nullopt;
if (btn >= (std::uint32_t)Gamepad::Button::Max) return std::nullopt;
return Binding{ GamepadButtonBind{ id, (Gamepad::Button)btn } };
}
if (tag == "gpa" && parts.size() == 4) {
std::uint32_t id, ax, inv;
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], ax) || !ParseDec(parts[3], inv)) {
return std::nullopt;
}
if (ax >= (std::uint32_t)Gamepad::Axis::Max) return std::nullopt;
return Binding{ GamepadAxisBind{ id, (Gamepad::Axis)ax, inv != 0 } };
}
if (tag == "gps" && parts.size() == 3) {
std::uint32_t id, st;
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], st)) return std::nullopt;
if (st > 1) return std::nullopt;
return Binding{ GamepadStickBind{ id, (Gamepad::Stick)st } };
}
if (tag == "wasd" && parts.size() == 5) {
std::uint32_t u, d, l, r;
if (!ParseHex(parts[1], u) || !ParseHex(parts[2], d) ||
!ParseHex(parts[3], l) || !ParseHex(parts[4], r)) {
return std::nullopt;
}
return Binding{ WASDBind{ u, d, l, r } };
}
return std::nullopt;
}

View file

@ -23,6 +23,7 @@ import :UI;
import :UIComponents;
import :Font;
import :Types;
import :Keys;
import std;
using namespace Crafter;
@ -87,32 +88,26 @@ void Crafter::InputField_OnText(InputField& f, std::string_view utf8) {
f.cursorPos += utf8.size();
}
void Crafter::InputField_OnKey(InputField& f, CrafterKeys key) {
switch (key) {
case CrafterKeys::Backspace:
if (f.cursorPos > 0 && !f.value.empty()) {
f.value.erase(f.cursorPos - 1, 1);
--f.cursorPos;
}
break;
case CrafterKeys::Delete:
if (f.cursorPos < f.value.size()) {
f.value.erase(f.cursorPos, 1);
}
break;
case CrafterKeys::Left:
if (f.cursorPos > 0) --f.cursorPos;
break;
case CrafterKeys::Right:
if (f.cursorPos < f.value.size()) ++f.cursorPos;
break;
case CrafterKeys::Home:
f.cursorPos = 0;
break;
case CrafterKeys::End:
f.cursorPos = f.value.size();
break;
default: break;
void Crafter::InputField_OnKey(InputField& f, KeyCode code) {
// `Key(...)` is consteval — every comparison below folds to an immediate
// integer at compile time, so this is just a sequence of int-eq tests.
if (code == Key(CrafterKeys::Backspace)) {
if (f.cursorPos > 0 && !f.value.empty()) {
f.value.erase(f.cursorPos - 1, 1);
--f.cursorPos;
}
} else if (code == Key(CrafterKeys::Delete)) {
if (f.cursorPos < f.value.size()) {
f.value.erase(f.cursorPos, 1);
}
} else if (code == Key(CrafterKeys::Left)) {
if (f.cursorPos > 0) --f.cursorPos;
} else if (code == Key(CrafterKeys::Right)) {
if (f.cursorPos < f.value.size()) ++f.cursorPos;
} else if (code == Key(CrafterKeys::Home)) {
f.cursorPos = 0;
} else if (code == Key(CrafterKeys::End)) {
f.cursorPos = f.value.size();
}
}

View file

@ -21,16 +21,112 @@ module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:Mesh_impl;
import Crafter.Math;
import Crafter.Asset;
import :Mesh;
import :Device;
import :Decompress;
import :Types;
import std;
using namespace Crafter;
namespace {
// Buffer-usage flag set shared by both Build paths. The compressed path
// appends VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT.
constexpr VkBufferUsageFlags2 kVertexUsageBase =
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR;
constexpr VkBufferUsageFlags2 kIndexUsageBase =
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) {
VkDeviceOrHostAddressConstKHR vertexAddr;
vertexAddr.deviceAddress = self.vertexBuffer.address;
VkDeviceOrHostAddressConstKHR indexAddr;
indexAddr.deviceAddress = self.indexBuffer.address;
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
.vertexData = vertexAddr,
.vertexStride = sizeof(Vector<float, 3, 3>),
.maxVertex = vertexCount - 1,
.indexType = VK_INDEX_TYPE_UINT32,
.indexData = indexAddr,
.transformData = {.deviceAddress = 0}
};
VkAccelerationStructureGeometryDataKHR geometryData(trianglesData);
VkAccelerationStructureGeometryKHR blasGeometry {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR,
.geometry = geometryData,
.flags = VK_GEOMETRY_OPAQUE_BIT_KHR
};
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR,
.geometryCount = 1,
.pGeometries = &blasGeometry,
};
auto primitiveCount = indexCount / 3;
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
};
Device::vkGetAccelerationStructureBuildSizesKHR(
Device::device,
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&blasBuildGeometryInfo,
&primitiveCount,
&blasBuildSizes
);
self.scratchBuffer.Resize(
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
blasBuildSizes.buildScratchSize);
blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address;
self.blasBuffer.Resize(
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
blasBuildSizes.accelerationStructureSize);
VkAccelerationStructureCreateInfoKHR blasCreateInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
.buffer = self.blasBuffer.buffer,
.offset = 0,
.size = blasBuildSizes.accelerationStructureSize,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
};
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure));
blasBuildGeometryInfo.dstAccelerationStructure = self.accelerationStructure;
VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo {
.primitiveCount = primitiveCount,
.primitiveOffset = 0,
.firstVertex = 0,
.transformOffset = 0
};
VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo;
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP);
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
.accelerationStructure = self.accelerationStructure
};
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
}
}
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
vertexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size());
indexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size());
vertexBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size());
indexBuffer.Resize(kIndexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size());
std::memcpy(vertexBuffer.value, verticies.data(), verticies.size() * sizeof(Vector<float, 3, 3>));
std::memcpy(indexBuffer.value, indicies.data(), indicies.size() * sizeof(std::uint32_t));
@ -38,91 +134,72 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
VkDeviceOrHostAddressConstKHR vertexAddr;
vertexAddr.deviceAddress = vertexBuffer.address;
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), cmd);
}
VkDeviceOrHostAddressConstKHR indexAddr;
indexAddr.deviceAddress = indexBuffer.address;
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
if (!Device::memoryDecompressionSupported) {
// CPU fallback: decompress into temporary host vectors, then take
// the existing uncompressed path. The data region is decompressed
// into a discard buffer (consumer is expected to handle data-stream
// decoding via Compression::DecompressCPU on its own buffer).
std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount);
std::vector<std::uint32_t> indices(asset.indexCount);
std::vector<std::byte> dataDiscard(
asset.blob.regions.size() >= 3 ? asset.blob.regions[2].decompressedSize : 0);
std::array<std::span<std::byte>, 3> outputs = {
std::as_writable_bytes(std::span(vertices)),
std::as_writable_bytes(std::span(indices)),
std::span<std::byte>(dataDiscard),
};
Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size()));
Build(vertices, indices, cmd);
return;
}
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
.vertexData = vertexAddr,
.vertexStride = sizeof(Vector<float, 3, 3>),
.maxVertex = static_cast<std::uint32_t>(verticies.size())-1,
.indexType = VK_INDEX_TYPE_UINT32,
.indexData = indexAddr,
.transformData = {.deviceAddress = 0}
};
VkAccelerationStructureGeometryDataKHR geometryData(trianglesData);
VkAccelerationStructureGeometryKHR blasGeometry {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR,
.geometry = geometryData,
.flags = VK_GEOMETRY_OPAQUE_BIT_KHR
vertexBuffer.Resize(
kVertexUsageBase | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
asset.vertexCount);
indexBuffer.Resize(
kIndexUsageBase | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
asset.indexCount);
compressedStaging.Resize(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
static_cast<std::uint32_t>(asset.blob.bytes.size()));
std::memcpy(compressedStaging.value, asset.blob.bytes.data(), asset.blob.bytes.size());
compressedStaging.FlushDevice();
std::array<VkDeviceAddress, 3> dstAddresses = {
vertexBuffer.address,
indexBuffer.address,
0, // data region is not consumed by Mesh; caller handles it separately.
};
std::vector<VkDecompressMemoryRegionEXT> regions;
for (std::size_t i = 0; i < asset.blob.regions.size() && i < 2; ++i) {
const Compression::RegionMeta& r = asset.blob.regions[i];
if (r.decompressedSize == 0) continue;
std::span<const std::byte> streamBytes(
asset.blob.bytes.data() + r.srcOffset,
static_cast<std::size_t>(r.compressedSize));
Decompress::ExpandStreamToTileRegions(
streamBytes,
compressedStaging.address + r.srcOffset,
dstAddresses[i],
regions);
}
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR,
.geometryCount = 1,
.pGeometries = &blasGeometry,
};
Decompress::DecompressOnGPU(
cmd,
regions,
VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR);
// Query the memory sizes that will be needed for this BLAS
auto primitiveCount = static_cast<uint32_t>(indicies.size() / 3);
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd);
}
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
};
Device::vkGetAccelerationStructureBuildSizesKHR(
Device::device,
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&blasBuildGeometryInfo,
&primitiveCount,
&blasBuildSizes
);
scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, blasBuildSizes.buildScratchSize);
VkPhysicalDeviceAccelerationStructurePropertiesKHR asProps{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_PROPERTIES_KHR
};
VkDeviceAddress scratchAddr = scratchBuffer.address;
blasBuildGeometryInfo.scratchData.deviceAddress = scratchAddr;
blasBuffer.Resize(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, blasBuildSizes.accelerationStructureSize);
// Create and store the BLAS handle
VkAccelerationStructureCreateInfoKHR blasCreateInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
.buffer = blasBuffer.buffer,
.offset = 0,
.size = blasBuildSizes.accelerationStructureSize,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
};
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &accelerationStructure));
blasBuildGeometryInfo.dstAccelerationStructure = accelerationStructure;
// Prepare the build range for the BLAS
VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo {
.primitiveCount = primitiveCount,
.primitiveOffset = 0,
.firstVertex = 0,
.transformOffset = 0
};
VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo;
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP);
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
.accelerationStructure = accelerationStructure
};
blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
}

View file

@ -68,6 +68,14 @@ void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkComman
for (auto& h : heap_->resourceHeap) h.FlushDevice();
for (auto& h : heap_->samplerHeap) h.FlushDevice();
// Re-bind the swapchain image descriptors after every resize. Window
// already drained the queue and rebuilt the imageViews[] before
// firing the event, so vkWriteResourceDescriptorsEXT is safe here.
resizeSub_.SetEvent(&window.onResize, [this]() {
WriteSwapchainDescriptors();
for (auto& h : heap_->resourceHeap) h.FlushDevice();
});
(void)initCmd; // reserved for future image-layout tweaks
}

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);
}