> For the complete documentation index, see [llms.txt](https://docs.kouch.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kouch.dev/docs/adr/0005-hybrid-input-sources.md).

# ADR 0005: Hybrid input — Steam Input for motion, OS gamepad layer for buttons

## Status

Accepted (2026-09-17, owner decision after Early Test 1 — see `docs/ARCHITECTURE.md` "Input sources" and `docs/EARLY_TESTS.md` rows 0–1).

## Context

The original plan (`ARCHITECTURE.md` "Threading & latency", ADR 0002) read **everything** — buttons, sticks, triggers, motion — through Steam Input on one thread, because it is the only API that exposes gyro for Steam Controller / Steam Deck-class pads and it hands Kouch every controller Steam knows about.

Early Test 1 measured, on the owner's PC with five controllers, what happens the moment an emulator window takes focus: Steam re-activates the desktop layout for the child process and Kouch's **action channel goes inactive (0/65 actions active, `BNewDataAvailable` stops)** for as long as the child is up — while **`GetMotionData` keeps flowing at full rate (4860/4960 ticks)**. Kouch's whole purpose is to keep feeding 16 slots *while an emulator has focus*, so "Steam Input for everything" cannot work as the sole reader on Windows.

Early Test 0 also established the two conditions for Steam Input to activate at all: a real top-level window, and Steam as the parent process under an app id flagged for the Steam Input API. Both are satisfied by the Tauri window plus Kouch's own app id (dev builds use the "Kouch Lab" non-Steam shortcut with a borrowed id until Steam Direct clears).

## Decision

`kouch-input` runs **two `InputSource`s from Phase 1** and merges them per controller on the input thread:

* **`SteamSource`** — `GetMotionData` (accel/gyro/quat) for every pad, and actions only for Steam-Input-only pads (Steam Controller, Deck built-ins) while Kouch's window is foreground. Also the feedback path (rumble, LED, haptics).
* **`OsSource`** — SDL3's gamepad API for buttons, sticks and triggers. This is the same OS layer (Steam's XInput emulation / DirectInput / HID) that emulators launched from other frontends already read, so it survives a child window taking focus.

Identity between the two is joined by controller kind + connect order (+ USB path/serial when both sides expose it); a mismatch shows as a motion badge on the wrong slot and is fixable from the claim screen. Emulator profiles default to `native_gamepads: disable_in_emulator_config` so the emulator sees only Kouch's DSU slots, never the same pad twice.

## Consequences

* `sdl3` (built from source; needs CMake ≥ 3.16 on the build machine) is a Phase 1 dependency of `kouch-input` instead of a Phase 8 one.
* Steam-Input-only pads keep their button channel only while a Kouch window is foreground. The permanently present, click-through Quick Menu overlay window is the intended way to stay "foreground in Steam's eyes" during play — to be verified in Milestone 9 (Early Test 9).
* Motion for all 8 motion slots still comes from Steam Input, so the DSU/Cemuhook motion path is unchanged (ADR 0002).
* The SDL event pump runs on the input thread alongside `RunFrame`; the "all ISteamInput calls on one thread" rule (`CLAUDE.md` rule 4) still holds.
* Linux/Deck keeps the same split; the OS layer there is SDL3 over evdev.

## Addendum 2026-09-26: SDL sees Steam's pads again

Measured live with the owner (`kouch-lab steam-hybrid`, Early Test 1b): Steam hands every process it launches `SDL_GAMECONTROLLER_IGNORE_DEVICES` (\~12 KB, nearly every pad, plus `SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD`). Honouring it, a Steam-launched Kouch's SDL layer saw **0 pads**. With a child window focused, Steam's actions dropped to 10 of 7231 ticks on every pad while motion stayed at 7231/7231. So in a game no buttons or sticks reached the emulator at all, only motion. The decision above assumed SDL would carry buttons there; under Steam it could not.

* `OsSource` overrides that list, with an override-priority SDL hint, whenever Steam launched Kouch (`b2c1dbb`). The hint lives in Kouch's process only, so the emulators it starts still get Steam's list. They read DSU anyway. `KOUCH_SDL_RESPECT_STEAM=1` opts out. With the override, SDL delivered the Steam Controller's presses while a child window had focus (2984 frames), with Steam delivering none.
* SDL now also reads Valve's own pads. `kind_from_vid_pid` maps `28de:1304` to the 2026 Steam Controller and `28de:1205` to the Deck's built-in controls. `identity::pair_family` lets both Steam Controller generations pair across sources and match a reserved seat (`a7213f7`, seat match since then), so one pad takes one seat.
* SDL reports raw sticks. A 0.07 radial rest dead zone at the source stops resting noise (±0.02–0.05) from reading as movement on every DSU packet.
* Consequence: the "Steam-Input-only pads keep their buttons only while a Kouch window is foreground" limit above no longer applies on Windows. SDL carries them during play. Steam actions remain the path while Kouch is foreground, and for motion always.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kouch.dev/docs/adr/0005-hybrid-input-sources.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
