Fix input forwarding: default to XInput, make Steam Input opt-in

Making Steam Input the default backend silently broke input forwarding. Merely
initializing Steam Input activates Steam's in-process XInput interception, which
hides controllers from XInputGetState unless they're bound to our action set for
the running appid. With no such binding (the normal case for a donor appid) Steam
Input reports zero controllers AND XInput now sees nothing -> no input at all.

Reproduced with coop_steam_input_probe: without Steam, XInput slot 0 is seen;
with Steam Input initialized, 0 Steam controllers and the XInput fallback goes
empty.

Default to XInput (RPT delivers guest pads there and it works) and make Steam
Input an opt-in Controllers-panel toggle that switches the backend at runtime;
turning it off restores XInput. All 6 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:25:43 +02:00
parent 24d66020f6
commit 914dba4d25
4 changed files with 90 additions and 21 deletions

View File

@@ -19,7 +19,7 @@ XInput game becomes Remote-Play-Together-able.
| Concern | Mechanism | Component | Status |
| --- | --- | --- | --- |
| Receive guest input | Steam Input (primary, when built with the Steamworks SDK) with XInput fallback; RPT delivers guest pads to the focused window | `coop_host.exe` | done |
| Receive guest input | XInput (RPT delivers guest pads to the focused window); optional, opt-in Steam Input when built with the Steamworks SDK | `coop_host.exe` | done |
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` | done |
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` | done |
| Mirror video | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` | done |
@@ -158,17 +158,19 @@ Done:
all subsystems hooked, and the IPC channels (status, audio ring, video share,
log) all flow across the x64↔x86 boundary.
- **Steam Input. ✅** When the host is built with the Steamworks SDK
(auto-detected under `third_party/steamworks_sdk/`), guest input comes through
the Steam Input API (action-based) as the primary path, falling back to XInput
per slot — and to pure XInput if Steam isn't available, so the host always runs.
It initializes SteamAPI + Steam Input and loads a bundled action manifest
- **Steam Input (opt-in). ✅** When the host is built with the Steamworks SDK
(auto-detected under `third_party/steamworks_sdk/`), a **Use Steam Input**
checkbox in the Controllers panel switches the input backend to the Steam Input
API (action-based) at runtime; it loads a bundled action manifest
(`steam_input_actions.vdf`) via `SetInputActionManifestFilePath`, so it needs no
partner-backend config. Verified to initialize against the live Steam client and
enumerate controllers (`coop_steam_input_probe`). Reading actual button/stick
state still requires a controller bound through Steam Input for the running
appid (the donor appid under RPT) — without that binding the XInput path carries
the guest input, as before.
partner-backend config. **It's off by default and XInput is the primary path**:
merely initializing Steam Input activates Steam's in-process XInput interception,
which *hides* controllers from XInput unless they're bound to our action set for
the running appid — so defaulting to it silently broke input forwarding. Enabling
it is only useful once a controller is bound to Steam Input for the donor appid;
otherwise leave it off and the proven XInput path carries the guest input.
Verified to initialize and enumerate controllers against the live Steam client
(`coop_steam_input_probe`), and that toggling it off restores XInput.
All planned phases are now implemented. Possible later work: per-stream audio
mixing for multi-stream games, per-stream format detection for the audio hook, and