Crafter.Graphics/implementations/Crafter.Graphics-Gamepad.cpp

838 lines
35 KiB
C++
Raw Normal View History

2026-05-12 00:24:48 +02:00
/*
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
2026-05-18 02:07:48 +02:00
// ────────────────────────────────────────────────────────────────────
// 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<std::uint32_t> 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<Device>();
dev->id = static_cast<std::uint32_t>(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<std::int32_t>(dev.id);
for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) {
int stdIdx = StdButtonIdx(static_cast<Button>(b));
bool newState = stdIdx >= 0 && gamepadGetButton(idx, stdIdx) != 0;
if (newState != dev.buttons[b]) {
dev.buttons[b] = newState;
if (newState) dev.onButtonDown.Invoke(static_cast<Button>(b));
else dev.onButtonUp .Invoke(static_cast<Button>(b));
}
}
// Axes — sticks come straight through, triggers come from button
// analog values (the standard mapping exposes those too, with
// buttons[6/7].value, but the polling shim returns binary
// pressed-state. For V1, triggers report 0 / 1 only).
float lx = static_cast<float>(gamepadGetAxis(idx, kStdAxisLX));
float ly = static_cast<float>(gamepadGetAxis(idx, kStdAxisLY));
float rx = static_cast<float>(gamepadGetAxis(idx, kStdAxisRX));
float ry = static_cast<float>(gamepadGetAxis(idx, kStdAxisRY));
auto setAxis = [&](Axis a, float v) {
std::size_t i = (std::size_t)a;
if (dev.axes[i] != v) {
dev.axes[i] = v;
dev.onAxisChanged.Invoke(a);
}
};
setAxis(Axis::LeftStickX, lx);
setAxis(Axis::LeftStickY, ly);
setAxis(Axis::RightStickX, rx);
setAxis(Axis::RightStickY, ry);
setAxis(Axis::LeftTrigger, gamepadGetButton(idx, kStdBtnLT) ? 1.0f : 0.0f);
setAxis(Axis::RightTrigger, gamepadGetButton(idx, kStdBtnRT) ? 1.0f : 0.0f);
}
}
void Crafter::Gamepad::Rumble(Device& /*dev*/, float /*low*/, float /*high*/,
std::chrono::milliseconds /*duration*/) {
// Browser Gamepad rumble (HapticActuator API) is patchy across
// engines. V1: no-op. Pads that don't support it would silently
// fall through anyway.
}
#endif
2026-05-12 00:24:48 +02:00
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
for (auto& up : connected) {
if (up->id == id) return up.get();
}
return nullptr;
}