683 lines
28 KiB
C++
683 lines
28 KiB
C++
/*
|
|
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;
|
|
}
|