new input system
This commit is contained in:
parent
b3db40ebec
commit
ac2eb7fb0a
31 changed files with 3292 additions and 781 deletions
174
implementations/Crafter.Graphics-Clipboard.cpp
Normal file
174
implementations/Crafter.Graphics-Clipboard.cpp
Normal 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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
683
implementations/Crafter.Graphics-Gamepad.cpp
Normal file
683
implementations/Crafter.Graphics-Gamepad.cpp
Normal 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;
|
||||
}
|
||||
454
implementations/Crafter.Graphics-Input.cpp
Normal file
454
implementations/Crafter.Graphics-Input.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ¤tBuffer));
|
||||
// Acquire the next image from the swap chain. If the surface has
|
||||
// changed size out from under us (compositor/Win32 resize delivered
|
||||
// between Render calls), recreate and retry once.
|
||||
{
|
||||
VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||
semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer);
|
||||
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
RecreateSwapchainAndImages();
|
||||
onResize.Invoke();
|
||||
acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||
semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer);
|
||||
}
|
||||
if (acquire != VK_SUBOPTIMAL_KHR) {
|
||||
Device::CheckVkResult(acquire);
|
||||
}
|
||||
}
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue