/* 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 #include #include #include #include #include #include #include #include #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #define COBJMACROS #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #include #include #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> 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(*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(); dev->id = SyspathToId(syspath); dev->name = name ? name : libevdev_get_name(evdev); auto bp = std::make_unique(); 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(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 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 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(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(This); return (ULONG)InterlockedIncrement(&d->refCount); } ULONG STDMETHODCALLTYPE DelegateRelease(GamepadAddedHandler* This) { AddedDelegate* d = reinterpret_cast(This); LONG r = InterlockedDecrement(&d->refCount); return (ULONG)r; } HRESULT STDMETHODCALLTYPE DelegateInvoke(GamepadAddedHandler* This, IInspectable* sender, IGamepad* gamepad) { AddedDelegate* d = reinterpret_cast(This); if (gamepad) IGamepad_AddRef(gamepad); { std::lock_guard 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(&g_addedDelegate), &g_addedToken); __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadRemoved( g_statics, reinterpret_cast(&g_removedDelegate), &g_removedToken); } } void Crafter::Gamepad::Tick() { EnsureInit(); if (!g_statics) return; // Drain the cross-thread connect/disconnect queue. std::vector pending; { std::lock_guard 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(); 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 // ──────────────────────────────────────────────────────────────────── // DOM backend (browser Gamepad API via env.js polling helpers) // ──────────────────────────────────────────────────────────────────── #ifdef CRAFTER_GRAPHICS_WINDOW_DOM namespace Crafter::DomEnv { // External linkage required for the import_module attribute to bind // against the env.js function. An anonymous namespace would drop the // attribute at link time. __attribute__((import_module("env"), import_name("gamepadPollConnected"))) bool gamepadPollConnected(); __attribute__((import_module("env"), import_name("gamepadPollDisconnected"))) bool gamepadPollDisconnected(); __attribute__((import_module("env"), import_name("gamepadCount"))) std::int32_t gamepadCount(); __attribute__((import_module("env"), import_name("gamepadGetButton"))) std::int32_t gamepadGetButton(std::int32_t idx, std::int32_t buttonIdx); __attribute__((import_module("env"), import_name("gamepadGetAxis"))) double gamepadGetAxis(std::int32_t idx, std::int32_t axisIdx); } namespace { using Crafter::DomEnv::gamepadPollConnected; using Crafter::DomEnv::gamepadPollDisconnected; using Crafter::DomEnv::gamepadCount; using Crafter::DomEnv::gamepadGetButton; using Crafter::DomEnv::gamepadGetAxis; // Standard W3C Gamepad mapping: indices match the "standard" layout // every browser exposes for Xbox / DualShock / DualSense controllers. // Mismatched / non-standard pads fall through with zeroed buttons. constexpr int kStdBtnSouth = 0; constexpr int kStdBtnEast = 1; constexpr int kStdBtnWest = 2; constexpr int kStdBtnNorth = 3; constexpr int kStdBtnLB = 4; constexpr int kStdBtnRB = 5; constexpr int kStdBtnLT = 6; constexpr int kStdBtnRT = 7; constexpr int kStdBtnSelect = 8; constexpr int kStdBtnStart = 9; constexpr int kStdBtnLStickClick = 10; constexpr int kStdBtnRStickClick = 11; constexpr int kStdBtnDPadUp = 12; constexpr int kStdBtnDPadDown = 13; constexpr int kStdBtnDPadLeft = 14; constexpr int kStdBtnDPadRight = 15; constexpr int kStdBtnHome = 16; constexpr int kStdAxisLX = 0, kStdAxisLY = 1, kStdAxisRX = 2, kStdAxisRY = 3; // Map our `Gamepad::Button` ordinal to the W3C standard button index. int StdButtonIdx(Crafter::Gamepad::Button b) { using B = Crafter::Gamepad::Button; switch (b) { case B::South: return kStdBtnSouth; case B::East: return kStdBtnEast; case B::West: return kStdBtnWest; case B::North: return kStdBtnNorth; case B::Select: return kStdBtnSelect; case B::Start: return kStdBtnStart; case B::Home: return kStdBtnHome; case B::LeftStickClick: return kStdBtnLStickClick; case B::RightStickClick: return kStdBtnRStickClick; case B::LeftBumper: return kStdBtnLB; case B::RightBumper: return kStdBtnRB; case B::DPadUp: return kStdBtnDPadUp; case B::DPadDown: return kStdBtnDPadDown; case B::DPadLeft: return kStdBtnDPadLeft; case B::DPadRight: return kStdBtnDPadRight; case B::LeftTrigger: return kStdBtnLT; case B::RightTrigger: return kStdBtnRT; default: return -1; } } } void Crafter::Gamepad::Tick() { // Handle connect/disconnect by rebuilding `connected` from scratch // when the JS flags say something changed. Cheap — only fires on // actual hot-plug, not every frame. if (gamepadPollConnected() || gamepadPollDisconnected() || connected.empty()) { // Snapshot previously-known ids so we can fire onDisconnected // for ones that went away in this rebuild. std::vector previousIds; previousIds.reserve(connected.size()); for (auto& d : connected) previousIds.push_back(d->id); std::int32_t n = gamepadCount(); connected.clear(); for (std::int32_t i = 0; i < n; ++i) { auto dev = std::make_unique(); dev->id = static_cast(i); // index-based; stable for the page dev->name = "Gamepad"; connected.push_back(std::move(dev)); } // Fire connected events for ids that weren't in the previous set. for (auto& d : connected) { bool wasKnown = false; for (std::uint32_t prev : previousIds) if (prev == d->id) { wasKnown = true; break; } if (!wasKnown) onConnected.Invoke(d.get()); } // No way to call onDisconnected with the right Device* once it's // gone — fire with nullptr so subscribers know SOMETHING changed. for (std::uint32_t prev : previousIds) { bool stillThere = false; for (auto& d : connected) if (d->id == prev) { stillThere = true; break; } if (!stillThere) onDisconnected.Invoke(nullptr); } } // Poll buttons + axes on every Tick — same shape as the native paths. for (auto& devUp : connected) { Device& dev = *devUp; std::int32_t idx = static_cast(dev.id); for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) { int stdIdx = StdButtonIdx(static_cast