11 KiB
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 <installation-id>; 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-<appid>.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/<pgid>.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:
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:
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
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 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 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
documents
shortcuts.vdf, CRC-based IDs andsteam://rungameidURLs. Its 2018 byte parser is not reused; Runi uses Python'svdfpackage 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-launchcommand. sc-controllerremains 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.