Forward mouse + keyboard to DirectInput and Raw Input games
The MKB subsystem covered message-loop (PostMessage) and polling (GetAsyncKeyState/GetKeyboardState/GetCursorPos) games. Add the two remaining read paths, both fed from the same synthesized state: - DirectInput: vtable-swap IDirectInputDevice8::GetDeviceState (a COM method -> vtable swap, not inline, per the x86 COM-prologue trap), reading the shared vtable from a kept-alive probe device (the game made its devices before we injected). Dispatch on cbData: 256 = keyboard BYTE[256] indexed by DIK scan-code (map VK->DIK via MapVirtualKey VK_TO_VSC); DIMOUSESTATE = mouse buttons + relative deltas from the forwarded cursor. - Raw Input: games get no WM_INPUT while unfocused, so mkb_pump synthesizes it (PostMessage WM_INPUT with lParam = one of our RAWINPUT slots) and the hooked GetRawInputData serves that slot back (RID_HEADER + RID_INPUT). Covers keys + buttons; relative raw-mouse movement isn't in the position-based MKB stream. Both detours use the DetourGate safe-unhook guard, and the mock_game_test storm exercises their install/remove. dinput_hook_test drives the real path end-to-end: forward a key + mouse button through the ring, then a real DI keyboard/mouse device's GetDeviceState returns them. The Raw Input round-trip is operator- validated against a real game (too brittle to assert in-process). vtable_hook.hpp factors the COM vtable-swap helper out for reuse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
31
README.md
31
README.md
@@ -25,7 +25,7 @@ and forwards guest controllers back into it.
|
||||
| --- | --- | --- |
|
||||
| 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` |
|
||||
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` |
|
||||
| Forward mouse + keyboard | Opt-in MKB subsystem: host streams its window's clicks/keys, the hook posts the matching window messages and synthesizes `GetAsyncKeyState`/`GetKeyboardState`/`GetCursorPos` for polling games | `coop_hook.dll` + `coop_host.exe` |
|
||||
| Forward mouse + keyboard | Opt-in MKB subsystem: host streams its window's clicks/keys; the hook posts the matching window messages, synthesizes `GetAsyncKeyState`/`GetKeyboardState`/`GetCursorPos` for polling games, augments `IDirectInputDevice8::GetDeviceState` for **DirectInput** games, and synthesizes `WM_INPUT` + `GetRawInputData` for **Raw Input** games | `coop_hook.dll` + `coop_host.exe` |
|
||||
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` |
|
||||
| Mirror video (default) | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` |
|
||||
| Mirror video (hooked) | Injected Present / OpenGL hook copies the backbuffer into a shared keyed-mutex texture the host samples (lower latency, no capture border) | `coop_hook.dll` + `coop_host.exe` |
|
||||
@@ -110,14 +110,6 @@ default** and covers anything the hooked path doesn't.
|
||||
|
||||
### Current Tasks
|
||||
|
||||
- **Mouse + keyboard forwarding for Raw Input / DirectInput games.** The MKB
|
||||
subsystem forwards via window messages (`PostMessage`) plus synthesized
|
||||
`GetAsyncKeyState` / `GetKeyboardState` / `GetCursorPos`, which covers message-loop
|
||||
and polling games. Games that read keyboard/mouse via **Raw Input** (`WM_INPUT` /
|
||||
`GetRawInputData`, e.g. Trails through Daybreak) or **DirectInput**
|
||||
(`IDirectInputDevice8::GetDeviceState/GetDeviceData`) don't see it. Add hooks for
|
||||
those paths to synthesize the forwarded input there too.
|
||||
|
||||
- **Validate the Vulkan backend against a real game.** Exercise the Vulkan capture path
|
||||
end-to-end on a shipping title — **Sphere Spectacle** (Steam appid 1123040,
|
||||
`start steam://rungameid/1123040`) — not just `coop_mock_game`. Cover **both** early-presence
|
||||
@@ -192,6 +184,13 @@ ctest --test-dir build -C Debug --output-on-failure
|
||||
|
||||
- **`hook_selftest`** — in-process check of the IPC + XInput hook core (no game,
|
||||
no controller needed).
|
||||
- **`dinput_hook_test`** — in-process self-test of the **DirectInput** forwarding path. Reuses
|
||||
`mkb_hook.cpp`, installs the MKB hooks (which vtable-swap `IDirectInputDevice8::GetDeviceState`
|
||||
via a kept-alive probe device), forwards a synthetic key + mouse button through the MKB ring,
|
||||
pumps it, then creates a *real* DirectInput keyboard + mouse device and asserts `GetDeviceState`
|
||||
returns the forwarded input (the key at its DIK scan-code, the left mouse button). Skips cleanly
|
||||
if DirectInput can't acquire a device. (The Raw Input path is operator-validated against a real
|
||||
game — an in-process WM_INPUT round-trip is too brittle to assert reliably.)
|
||||
- **`audio_ring_test`** — unit test of the shared audio ring (lock-free SPSC
|
||||
push/pop, wrap-around, format handshake, overrun/drop). No device needed.
|
||||
- **`audio_mix_test`** — unit test of the multi-stream mixer math (decode / sum /
|
||||
@@ -649,3 +648,17 @@ Non-obvious things that cost time and constrain the design:
|
||||
in-process XInput interception, which hides controllers from `XInputGetState`
|
||||
unless they're bound to the running appid's action set — defaulting to it
|
||||
silently broke forwarding. XInput is primary; Steam Input is opt-in.
|
||||
- **Forwarding keyboard/mouse means covering three read paths, each differently.** Games read
|
||||
keyboard/mouse three ways and a forwarder has to satisfy all of them from one synthesized state.
|
||||
(1) **Message-loop** games get `PostMessage`d `WM_KEYDOWN`/`WM_*BUTTON*`. (2) **Polling** games
|
||||
get synthesized `GetAsyncKeyState`/`GetKeyboardState`/`GetCursorPos`. (3) **DirectInput** games
|
||||
call `IDirectInputDevice8::GetDeviceState` — a COM method, so it's hooked by **vtable swap, not
|
||||
inline** (the x86 COM-prologue trap), reading the vtable from a kept-alive *probe* device we
|
||||
create (the game made its devices before we injected, but all DI devices share one vtable);
|
||||
dispatch on `cbData` (256 = keyboard `BYTE[256]` indexed by **DIK scan-code**, not VK — map with
|
||||
`MapVirtualKey(VK_TO_VSC)`; `sizeof(DIMOUSESTATE)` = mouse). (4) **Raw Input** games read
|
||||
`WM_INPUT` → `GetRawInputData`, but get **no `WM_INPUT` while unfocused** — so we *synthesize* it:
|
||||
post `WM_INPUT` with `lParam` = the address of one of our `RAWINPUT` slots, and the hooked
|
||||
`GetRawInputData` serves that slot's data back (`RID_HEADER` and `RID_INPUT`). Relative raw mouse
|
||||
*movement* isn't in the position-based MKB event stream, so raw-input forwarding covers keys +
|
||||
buttons, not free-look — a known limitation.
|
||||
|
||||
Reference in New Issue
Block a user