Skip to content

Architecture: WebHID-first input spine with Steam Input as an optional adapter (@usersfirst/controller-steam) #80

@petegordon

Description

@petegordon

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 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 (GetMotionDatamotion) + 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.
  • Phase 5 — New Steam Controller (Triton) + opt-in polish: confirm VID/PID; author controller_triton.vdf (fallback controller_neptune); surface Steam glyphs / Flick Stick for opt-in users.

Open questions to verify on real hardware (⚠️)

  • WebHID × active-Steam-Input behavior on Windows (double-input / occlusion) — undocumented; test via the diagnostic harness.
  • Can native API be scoped per-controller-type (Deck on API, desktop in emulation) without suppressing the XInput pad?
  • GetMotionData units (deg/s+g vs rad/s+m/s²) — confirm against shipped isteaminput.h.
  • New Steam Controller controller_triton identifier + VID/PID (community: 0x28de:0x1302) — keep controller_neptune fallback.
  • steamworks-ffi-node robustness with real controllers (it lacks in steamworks.js: runFrame/origins/glyphs) — be ready to drop to raw koffi.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions