# Controller support and Steam integration This document records the controller research behind Runi's input implementation. Observed facts belong here; future product work belongs in `roadmap.md`. ## Current architecture Runi detects the 2026 Steam Controller at startup and on hotplug through SDL3, with evdev as a legacy fallback. During a direct game session it maps the SDL state either to an Xbox 360-shaped `uinput` device or to separate virtual keyboard and mouse devices, then temporarily grabs the controller's desktop keyboard/mouse event nodes. Launcher navigation is locked while the game owns the controller. The Xbox path was validated with DiRT Showdown: Wine sees the virtual device as XInput and the game accepts it. The direct USB path was validated on real `28de:1302` hardware: launcher navigation worked, a Direct game received one controller, Steam's desktop mouse/keyboard output stayed suppressed during the session, and launcher control returned after the game exited. The wireless puck uses `28de:1304` through the same bridge and suppression path. Every Play action asks for one of two modes: - **Runi Direct — Recommended** never requires Steam. It automatically selects either SDL3 → `uinput` → Wine XInput or a per-game Keyboard & Mouse profile. The launch dialog can override that choice for one session. - **Steam Input** uses a deterministic non-Steam shortcut. The shortcut runs `runi steam-launch `; that command stays alive until the game exits so Steam does not fall back to its Desktop layout. The local bridge is not started in this mode, avoiding duplicate controllers. Steam is detected in native and Flatpak locations. The most recently used local account is selected in v1. Runi preserves unknown VDF fields, backs up `shortcuts.vdf`, writes atomically, tags only its own shortcut, and derives a stable shortcut AppID from `runi-installation-id`. Steam must be closed while a shortcut is created, repaired or removed. Controller troubleshooting is explicit rather than permanently verbose. Overview → Technology → **Run Controller Diagnostic** launches the Direct path with temporary Wine HID/XInput tracing, counts SDL3-to-uinput changes, checks whether the virtual device is observable in the launched process tree, and stores a per-installation stage report. UMU's launcher output and Proton's separate `steam-.log` are correlated because the Wine HID/XInput trace is written to the latter. Normal launches do not enable these trace channels. ## Keyboard & Mouse profiles Games without native controller support can use a Runi-owned profile. The left stick emits WASD, the right trackpad controls the mouse with a right-stick fallback, and buttons, triggers and paddles emit configured keyboard or mouse actions. Gyro and haptics are not part of this first profile version. Bundled profiles live in `runi/controller_profiles`. User changes are stored in `~/.config/runi/controller-profiles/.toml`; the filename uses the canonical Runi Game ID, so a profile survives removal and re-import of an installation. Config → Controller edits the persistent method, pointer tuning and bindings. Reset removes only the user override and reveals the bundled profile again. The Thing That Happened is the first curated Keyboard & Mouse profile. Its mapping is based on the game's own key-binding screen: triggers become aim and shoot, face buttons cover jump/crouch/use/reload, shoulders cycle weapons, D-pad selects or holsters weapons, rear buttons cover grenade/fire mode and flashlight/night vision, and Quick Access opens the map. The Guide button remains reserved. ## Steam Input is not XInput Some games ask Valve's native action API for named digital and analog actions instead of polling XInput. Technology Scan detects embedded interface names such as `SteamInput006` and recommends Steam mode, but Direct remains available with a warning. Digimon Story Time Stranger is the reference compatibility case. Its executable requests `SteamInput006`. The Runi virtual Xbox reaches Wine correctly, and the Wine trace confirms `VID_045E&PID_028E`, a `WINEXINPUT` device and an XInput controller opened at index 0. Finding action-set files is configuration evidence, not proof that the provider consumed input. The inspected installation initially contained a 7.3 MB `steam_api64.dll` from `gbe_fork`, built on 27 November 2025, plus action manifests named `InGameControls.txt` and `MenuControls.txt`. Updating the provider to the verified Windows x64 VS2022 regular build from `gbe_fork 2026-05-30` did not by itself fix input. A temporary debug build produced `STEAM_LOG_*.log`, which showed that the provider repeatedly found one connected controller, while the game requested these current named actions: ```text Square Cross Circle Triangle L1 R1 L2 R2 Select Start L3 R3 Up Right Down Left L_Analog_controls R_Analog_controls ``` The bundled manifests used different action names. Nearly every lookup therefore returned handle `0`; only `Select` matched by accident and was incorrectly bound to `A`. The game made no `GetActionSetHandle` or `ActivateActionSet` calls, so the provider retained its lexicographically first action set. The validated fix was to add `steam_settings/controller/00_Default.txt` so the compatible set becomes the default: ```ini Square=X Cross=A Circle=B Triangle=Y L1=LBUMPER R1=RBUMPER L2=DLTRIGGER R2=DRTRIGGER Select=BACK Start=START L3=LSTICK R3=RSTICK Up=DUP Right=DRIGHT Down=DDOWN Left=DLEFT L_Analog_controls=LJOY=joystick_move R_Analog_controls=RJOY=joystick_move ``` The regular provider build was restored after tracing. Direct mode was then validated end-to-end without Steam: Steam Controller → SDL3 → uinput Xbox 360 → Wine XInput → `gbe_fork` named actions → game. This is a game/build-specific compatibility record, not a generic mapping to install into unrelated games. Runi models this as a **local Steam API provider**, not as a special case for one project. Detection is capability-based and conservative: an ordinary `steam_api64.dll` is not enough because official games ship it too. Embedded provider markers, the adjacent configuration layout, exported interfaces and controller action sets are normalized by provider adapters. Goldberg original, Goldberg-derived forks, RUNE-style configuration and unknown local providers all produce the same capability report; the launcher never branches on a release-group name. The original Goldberg emulator documents a SteamController/SteamInput bridge that accepts XInput and maps named actions through files in `steam_settings/controller`. A complete original-style configuration can therefore prefer Runi's Direct virtual Xbox path. Goldberg-derived forks may instead expose a supported non-Steam takeover setting in `configs.main.ini`. When that exact capability is detected, Steam mode may set `disable_steamoverlaygameid_env_var=1`, preserving `configs.main.ini.runi-backup`. Unknown providers are reported but never modified. Runi does not replace or rename Steam API DLLs. A second Digimon installation supplied a useful independent validation. It contains the same game executable and original Steam API payload as the GBE installation, but uses `steam_emu.ini` from RUNE. That configuration declares `SteamInput006`, an enabled controller, a `controls` action set and explicit Xbox mappings. Runi therefore reports `steam_input_via_xinput` and prefers Direct without modifying the provider. The same configuration also exposes its per-prefix save location under `users/Public/Documents/Steam/RUNE/1984270`. RUNE is implemented as one adapter; future layouts can add an adapter while recommendations remain driven by observed capabilities. ## Projects inspected - [Steam Controller Desktop Input Toggle](https://github.com/GloriousEggroll/steam-controller-desktop-input) demonstrates that Steam's internal controller API can persistently select its empty Desktop action-set configuration for direct USB (`28de:1302`) and wireless-dongle (`28de:1304`) connections. Runi uses those transport identities for its session-scoped Direct bridge, but does not integrate the tool: the inspected repository has no explicit license, is very new, and requires an unauthenticated local CEF debugging endpoint plus undocumented Steam UI APIs. Any future integration needs an opt-in security review, verified restore semantics and crash recovery; Node.js and CEF remote debugging are not Runi runtime requirements. - [SteamlessController](https://github.com/ddeverill/SteamlessController) validates the same architecture on Windows: raw Steam Controller HID, lizard-mode suppression and a virtual Xbox through ViGEm. Useful future ideas include its documented 2026 HID reports, heartbeat, reverse rumble, multiple devices, back buttons, trackpads, gyro and diagnostic probes. - [SISR](https://github.com/Alia5/SISR) redirects Steam Input configurations to system-level virtual devices. On Linux it currently requires Steam, CEF remote debugging, USBIP and a VIIPER system service; it is inspiration, not a Runi dependency. - reWASD is a proprietary Windows product and is useful only as UX inspiration for per-game profiles and remapping. - [Steam Shortcut Manager](https://github.com/CorporalQuesadilla/Steam-Shortcut-Manager/wiki/Steam-Shortcuts-Documentation) documents `shortcuts.vdf`, CRC-based IDs and `steam://rungameid` URLs. Its 2018 byte parser is not reused; Runi uses Python's `vdf` package and preserves modern and unknown fields. - Heroic and Lutris do not provide their own Steam Input implementation. They run normally without Steam and optionally create non-Steam shortcuts. Their reported failure mode—launcher bootstrap exits while the game remains open—informed Runi's blocking `steam-launch` command. - `sc-controller` remains design input for profiles, gyro, haptics and a virtual keyboard, not a runtime dependency. ## Known limitations in v1 - Only the Steam Controller is a supported physical controller. - Xbox Direct exposes the standard Xbox control set. Keyboard & Mouse profiles can use rear buttons and the right trackpad; gyro, rumble and per-mode layers are not yet forwarded. - Steam mode needs a locally installed Steam client and a signed-in user. - Account selection is automatic when several local accounts exist. - A non-Steam shortcut cannot make every replaced or incompatible local Steam API provider communicate with the real Steam client. Known behavior is selected from reported capabilities; unknown providers remain advisory. - No Steam API DLL, driver, SISR, VIIPER or reWASD component is distributed.