You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WebHID is the foundational input spine; Steam Input is an optional, removable adapter that conforms to the same normalized InputFrame contract — forced only where raw HID is impossible (Steam Deck built-in controls), opt-in everywhere else. Nothing in core, visualizer, or consuming games (Tandemonium) may depend on Steam Input. If @usersfirst/controller-steam is uninstalled, the only thing lost is Deck built-in gyro + Steam glyphs.
This generalizes the decision already made in #8 (parse the controller via WebHID without Steam Input) into a whole-system architecture.
Why WebHID-first (not the usual Steam-native approach)
Steam Input erases what the overlay exists to show: when a controller is bound to the Steam Input API, Steam drops the emulated XInput pad, abstracts away device identity, and on Steam Deck the kernel occludes the raw HID node and gyro-strips the virtual pad. Our per-vendor 3D model + real sensor-fusion gyro is the product.
WebHID is the only channel that works off-Steam (browser, Epic, GOG, itch).
Steam Input API and raw HID are mutually exclusive per controller (a controller on the API won't return via GetControllerForGamepadIndex) — so one channel must be chosen per device, and WebHID is the default.
Glyphs — the one genuinely Steam-exclusive feature — we already produce ourselves via visualizer/controller-profiles.js (hudLabels + GLBs). So no hard dependency remains.
Design
InputFrame — source-agnostic normalized per-controller frame (identity/profileKey, buttons, axes, triggers, motion{quaternion,gyro,accel}, optional glyphs). New in core.
ControllerSource producers: WebHIDSource (~already built — wraps drivers + fusion), GamepadAPISource, and the new SteamInputSource.
ControllerManager evolves into a WebHID-first multi-source arbiter that picks ONE source per physical controller, deduped by identity — never double-reads. (Dedup must handle the phantom-XInput case observed in Multiple Controllers outstanding issues and experience. #35 with DS4Windows.)
New optional package @usersfirst/controller-steam (the long-promised companion in the README) holds SteamInputSource, the glyph provider, IGA/controller_<type>.vdf tooling, and the steamworks-ffi-node/koffi binding. Depends on core one-way; Steamworks SDK stays un-bundled.
The SteamInputSource adapter has two roles: (1) forced for Deck built-in controls (gyro only via GetMotionData); (2) opt-in elsewhere via a per-controller source preference — the library-level successor to Tandemonium's tandemonium_dualsense_source (auto|steam-input|webhid).
Default = emulation mode, no IGA in depot, so the virtual XInput pad stays visible to WebHID/Gamepad API (Tandemonium's forge.config.js already does this deliberately). Only opt a controller type into the native API where forced (Deck).
Phased roadmap
Phase 1 — Contract + WebHID spine (lab, zero Steam code): add InputFrame/ControllerSource; wrap manager+drivers+fusion as WebHIDSource; add GamepadAPISource; evolve ControllerManager into the WebHID-first arbiter with identity dedup + per-controller source preference. Leaves everything working.
Phase 2 — Extract @usersfirst/controller-steam skeleton (optional package): move FFI binding out of Tandemonium; implement SteamInputSource (GetMotionData → motion) + GlyphProvider; move IGA tooling + the steam-input.md runbook here.
Phase 3 — Steam Deck built-in support (the forced case): arbiter detects controller_neptune and forces SteamInputSource; validate gyro units; confirm API can be scoped to the Deck without killing the desktop XInput pad.
Phase 4 — Migrate Tandemonium onto the lab: replace game-side Steam Input juggling with the lab arbiter + package; keep the motion pipeline + emulation-mode shipping decision.
Type: Architecture / epic · Status: Proposed (2026-06-20)
Full plan:
docs/STEAM-INPUT-ARCHITECTURE-PLAN.md(landing alongside this issue)Thesis
WebHID is the foundational input spine; Steam Input is an optional, removable adapter that conforms to the same normalized
InputFramecontract — forced only where raw HID is impossible (Steam Deck built-in controls), opt-in everywhere else. Nothing incore,visualizer, or consuming games (Tandemonium) may depend on Steam Input. If@usersfirst/controller-steamis uninstalled, the only thing lost is Deck built-in gyro + Steam glyphs.This generalizes the decision already made in #8 (parse the controller via WebHID without Steam Input) into a whole-system architecture.
Why WebHID-first (not the usual Steam-native approach)
GetControllerForGamepadIndex) — so one channel must be chosen per device, and WebHID is the default.visualizer/controller-profiles.js(hudLabels+ GLBs). So no hard dependency remains.Design
InputFrame— source-agnostic normalized per-controller frame (identity/profileKey, buttons, axes, triggers,motion{quaternion,gyro,accel}, optional glyphs). New incore.ControllerSourceproducers:WebHIDSource(~already built — wraps drivers + fusion),GamepadAPISource, and the newSteamInputSource.ControllerManagerevolves into a WebHID-first multi-source arbiter that picks ONE source per physical controller, deduped by identity — never double-reads. (Dedup must handle the phantom-XInput case observed in Multiple Controllers outstanding issues and experience. #35 with DS4Windows.)@usersfirst/controller-steam(the long-promised companion in the README) holdsSteamInputSource, the glyph provider, IGA/controller_<type>.vdftooling, and thesteamworks-ffi-node/koffi binding. Depends oncoreone-way; Steamworks SDK stays un-bundled.SteamInputSourceadapter has two roles: (1) forced for Deck built-in controls (gyro only viaGetMotionData); (2) opt-in elsewhere via a per-controller source preference — the library-level successor to Tandemonium'standemonium_dualsense_source(auto|steam-input|webhid).forge.config.jsalready does this deliberately). Only opt a controller type into the native API where forced (Deck).Phased roadmap
InputFrame/ControllerSource; wrap manager+drivers+fusion asWebHIDSource; addGamepadAPISource; evolveControllerManagerinto the WebHID-first arbiter with identity dedup + per-controller source preference. Leaves everything working.@usersfirst/controller-steamskeleton (optional package): move FFI binding out of Tandemonium; implementSteamInputSource(GetMotionData→motion) +GlyphProvider; move IGA tooling + thesteam-input.mdrunbook here.controller_neptuneand forcesSteamInputSource; validate gyro units; confirm API can be scoped to the Deck without killing the desktop XInput pad.controller_triton.vdf(fallbackcontroller_neptune); surface Steam glyphs / Flick Stick for opt-in users.Open questions to verify on real hardware (⚠️ )
GetMotionDataunits (deg/s+g vs rad/s+m/s²) — confirm against shippedisteaminput.h.controller_tritonidentifier + VID/PID (community:0x28de:0x1302) — keepcontroller_neptunefallback.steamworks-ffi-noderobustness with real controllers (it lacks in steamworks.js: runFrame/origins/glyphs) — be ready to drop to raw koffi.Related issues
InputFrame.extras)