BlackMark 8e93312925 Audio render-hook M1: shared audio ring + protocol diag fields
First milestone of the injection render-hook audio path (see
docs/audio-render-hook-plan.md) that fixes the local audio echo without a
virtual device.

- common/include/coop/audio_ring.hpp: new lock-free SPSC shared-memory ring
  for PCM, separate from the input/status SharedBlock. Free-running 64-bit
  positions (release/acquire), format handshake, host-owned capture_enabled
  gate, drop-whole-packet overrun policy.
- common/include/coop/protocol.hpp: add AudioStreamInfo + audio_streams_seen /
  audio_streams[] to the always-present HookStatus for the render-stream-count
  debug view; bump kProtocolVersion 3->4 (new members appended).
- tests/audio_ring_test.cpp: in-process unit test (push/pop integrity,
  wrap-around, format handshake, overrun/drop). No hook or audio device.
- docs/audio-render-hook-plan.md: the green-lit design this implements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:49:18 +02:00

CoopAllTheThings

Steam Remote Play Together (RPT) for any XInput game — without breaking DRM, achievements, or playtime.

Existing "donor game" tools (e.g. RemotePlayWhatever) copy a target game's files into a donor game's folder and rename the executable so Steam streams the target under the donor's appid. That breaks DRM-protected games, breaks achievements, and credits playtime to the donor.

CoopAllTheThings takes a different approach: the real game runs normally under its own appid (so DRM, achievements, and playtime all work), while a lightweight mirror app runs under the donor appid. The mirror presents a borderless window that is a live copy of the game's video + audio, and forwards the guests' input back into the real game. Steam's RPT captures the mirror window — so any XInput game becomes Remote-Play-Together-able.

Architecture

Concern Mechanism Component Status
Receive guest input XInput (RPT delivers guest pads to the focused window) 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
Mirror audio WASAPI process-loopback capture of the game, re-rendered on the host coop_host.exe done
Host ↔ hook IPC Named shared memory (seqlock for input, status back-channel) common/ done

Limitations

  • Anti-cheat: the input path injects coop_hook.dll into the target game. Games protected by kernel-level anti-cheat (Easy Anti-Cheat, BattlEye, Vanguard, etc.) will detect the injected module and may kick the player or issue a ban. Such games are explicitly out of scope and unsupported — do not use CoopAllTheThings with them. The tool targets single-player and co-op/local-multiplayer titles without active anti-cheat.
  • XInput only: the game must read controllers via XInput (the common case). DirectInput-only / RawInput-only games are not handled.
  • x64 only: the host and hook DLL must match the game's bitness, and only x64 is built today. 32-bit games need the x86 hook + injector (see Roadmap).
  • Local audio echo: process-loopback capture does not mute the game, so the game's audio plays locally and the host re-renders it — the local machine hears it twice. Guests hear it once. Fixing this is on the Roadmap.
  • Debug-oriented UI: the ImGui overlay is always visible and laid out for diagnosing the pipeline, not for end use. It can't yet be toggled off.

Status

All phases below are implemented and verified.

  • Phase 0 — donor spike. ✅ Confirmed Steam RPT streams an arbitrary borderless window under a donor appid and routes guest gamepads into it as XInput (correct slot assignment). This is the premise the whole tool rests on.
  • Phase 1a — input forwarding + focus spoofing. ✅ The host injects coop_hook.dll; the DLL hooks XInput (SafetyHook) so the game reads the forwarded pad state and only that state, and spoofs focus so the game keeps running while the host holds the real OS focus. A hook→host status back-channel reports attach state and poll rate.
  • Phase 1b — video mirror (WGC). ✅ The host captures the injected game's window with Windows Graphics Capture and draws it letterboxed as its background, so RPT streams a live mirror.
  • Phase 2 — audio mirror. ✅ The host captures the game's audio by PID via WASAPI process loopback and re-renders it on the default endpoint, so RPT carries game audio to guests.

Roadmap

Future work, roughly in priority order:

  • Make the tool usable, not just demonstrable: toggle the debug overlay on/off so the mirror window can be used clean.
  • Generalize the UI: rework the panels from bug-specific debug readouts into general-purpose status, and add broader debug info (latency, frame timing, per-channel stats).
  • Fix the local audio echo: mute the game's local render (or otherwise avoid the double playback) while still capturing it for the mirror.
  • Present-hook video path: capture the game's frames by hooking IDXGISwapChain::Present in the injected DLL and sharing the backbuffer via a shared D3D11 texture, as a lower-latency / more stable alternative to WGC.
  • Steam Input: consume guest input through the Steam Input API directly rather than XInput.
  • x86 support: add an x86 build of coop_hook.dll plus an x86 injector helper the x64 host spawns, so 32-bit games work (a 64-bit process can't cleanly inject a 32-bit one). Detect target bitness with IsWow64Process2. The shared-memory IPC layout is already fixed-width / bitness-stable.

Building

Requirements: Windows 10/11, Visual Studio 2022 (MSVC + C++ workload), CMake ≥ 3.21.

git clone --recurse-submodules <repo-url>
# or, if already cloned:
git submodule update --init --recursive

cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Debug
# output: bin/Debug/coop_host.exe (+ coop_hook.dll, test exes)

Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under third_party/; the Steamworks SDK is vendored manually there when wired. No vcpkg / package manager is used.

clangd / IDE setup

The Visual Studio CMake generator does not emit compile_commands.json, so clangd has no include paths and reports false errors. Run gen-compile-commands.bat once (and after adding sources or include dirs); it configures a parallel Ninja build in build-clangd/ that produces the database, which .clangd points clangd at. clangd's clang-cl driver resolves the MSVC / Windows SDK system includes on its own.

Tests

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).
  • audio_loopback_test — spawns coop_tone.exe (a standalone WASAPI sine-wave source under tools/audio_tone) and verifies the shipping process-loopback capture receives its audio by PID. Skips cleanly if the machine has no audio endpoint.

Running the tool (manual, end-to-end)

This needs Steam, a donor game that supports Remote Play Together, and a second person/account to receive the stream.

  1. Launch the host under a donor appid. Find the donor's appid (the number in its store URL); the donor only needs RPT support and is never actually played:

    "C:\Program Files (x86)\Steam\steam.exe" -applaunch <donorAppId> "D:\dev\CoopAllTheThings\bin\Debug\coop_host.exe"
    

    The borderless window appears and Steam marks the donor "running". If the donor ignores the trailing path, set the host as the donor's Launch Options ("D:\...\coop_host.exe" %command%) or use a launcher like RemotePlayDetached.

  2. Start the real game windowed or borderless (not exclusive fullscreen — see Lessons learned). In the host's Injection panel, filter for the game's .exe, select it, and click Inject & Connect. Watch Hook status for Attached, a non-zero XInput polled: N/s, and Focus spoof: active.

  3. Mirror video: in the Video mirror panel, tick Mirror game window — the host window now shows a live, letterboxed copy of the game.

  4. Mirror audio: in the Audio mirror panel, tick Mirror game audio. (You'll hear the game twice locally; that's the known echo — guests hear it once.)

  5. Start Remote Play Together from Steam and invite a guest. Verify the guest sees the mirrored video, hears the audio, and that their controller drives the real game.

Useful checks while developing without RPT: tick Forward synthetic test input in the Injection panel to make the game move on its own (proving forwarding is the source), and click away from the game to confirm focus spoofing keeps it running.

Injection access error → run the host as administrator. "Target is 32-bit" → that game needs the x86 hook (see Roadmap).

Lessons learned

Non-obvious things that cost time and constrain the design:

  • RPT only streams the focused window. The game therefore can't hold focus itself; the hook spoofs focus (GetForegroundWindow / GetActiveWindow / GetFocus, plus swallowing window-deactivation messages) so the game keeps polling and rendering while the host owns the real OS focus.
  • Run target games windowed or borderless, never exclusive fullscreen. Exclusive fullscreen minimizes on focus loss (defeating the focus spoof) and can't be window-captured. While unfocused the game gets no OS keyboard/mouse — only the forwarded controller.
  • ActivateAudioInterfaceAsync requires an agile completion handler. If the handler doesn't answer QueryInterface for IAgileObject, the call is rejected synchronously with E_ILLEGAL_METHOD_CALL (0x8000000E) — regardless of COM apartment, MFStartup, device path, or activation params. (WRL/wil-based samples hide this because they make the handler agile for you.) Process loopback also needs the Windows 10 20H1 headers — build with NTDDI_VERSION ≥ 0x0A00000B.
  • WGC captures occluded windows but not minimized ones. The game may sit behind the host window, but must not be minimized.
  • Process-loopback capture doesn't mute the source, hence the local audio echo — capturing a process's render does not stop it reaching the speakers.
Description
No description provided
Readme 845 KiB
Languages
C++ 95.7%
CMake 4.2%