Mirror the real game's audio so Steam Remote Play Together (which streams the host's own audio session) carries it to the guest. The host captures the game by PID via WASAPI process loopback and re-renders it on the default endpoint; the game still plays locally too (accepted "double audio" for now). - ProcessLoopbackCapture: process-loopback capture client, frame-sink + stats. The completion handler must be agile (IAgileObject) or ActivateAudioInterfaceAsync rejects every call with E_ILLEGAL_METHOD_CALL. - AudioMirror: wraps capture with an event-driven render client and a primed ring buffer; AudioPanel drives it from the injected game's window/PID. - coop_tone: standalone WASAPI sine-wave process used as a known audio source. - audio_loopback_test (CTest): captures coop_tone by PID and asserts non-silent audio arrives, so the path is verifiable without a second Steam account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
See docs and the in-repo plan for the full design.
Architecture (target)
| Concern | Mechanism | Where |
|---|---|---|
| Receive guest input | Steam Input / XInput (RPT delivers guests to the focused window) | coop_host.exe |
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees only our pad | coop_hook.dll |
| Mirror video | Windows Graphics Capture first; IDXGISwapChain::Present hook as the low-latency upgrade |
host (+ hook) |
| Mirror audio | WASAPI process-loopback capture of the game, re-rendered | coop_host.exe |
| Host ↔ hook IPC | Named shared memory (seqlock for input, shared D3D11 texture for video) | common/ |
Limitations
- Anti-cheat: the input path injects
coop_hook.dllinto 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.
- Architecture match: the host and hook DLL must match the game's bitness. x64 is supported first; x86 support is a later phase (see the plan).
Status
Phase 0 — donor spike. ✅ Validated. coop_host.exe is a borderless D3D11
window with an ImGui overlay listing every controller it sees. Confirmed
end-to-end: Steam RPT streams the window under a donor appid, and guest gamepads
arrive (with correct slot assignment) as XInput.
Phase 1a — input forwarding + focus spoofing (current). The host injects
coop_hook.dll; the DLL hooks XInput (via SafetyHook) so the game reads the
forwarded controller state and only that state. The DLL also spoofs focus
(hooks GetForegroundWindow/GetActiveWindow/GetFocus and subclasses the game
window to swallow deactivation messages) so the game keeps running and polling
while the tool holds the real OS focus — required because Steam RPT only captures
the focused window. A hook→host status back-channel shows whether the hook is
attached and how fast the game is polling it. The in-process hook_selftest
validates the IPC + hook core without needing a game.
Phase 1b — video mirror (current). 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 of the game. See the test steps below.
Still ahead (scoped in the plan): a Present-hook capture path if WGC latency
disappoints, audio (WASAPI process loopback), and x86 support.
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
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.
Phase 0: validating the donor-launch assumption
This is a manual test — it needs Steam, a second person (or second account), and a donor game that supports Remote Play Together.
-
Pick a donor game you own that supports Remote Play Together (check the "Remote Play Together" tag on its store page). The donor only needs RPT support; it is never actually played.
-
Launch the host under the donor's appid. Find the donor's appid (the number in its store URL), then run:
"C:\Program Files (x86)\Steam\steam.exe" -applaunch <donorAppId> "D:\dev\CoopAllTheThings\bin\Debug\coop_host.exe"The borderless overlay window should appear and Steam should consider the donor "running" (green status / "Stop" button in the library).
If the donor ignores the trailing path, set the host as the donor's Launch Options instead (
"D:\...\coop_host.exe" %command%variants), or use a launcher such as RemotePlayDetached. Recording which method makes Steam attribute our window to the donor is the main deliverable of Phase 0. -
Start Remote Play Together from the Steam friends list / overlay and invite a friend (or a second machine/account).
-
Verify on the guest side:
- The guest sees the borderless overlay window streamed (not a black screen).
- The guest presses buttons on their controller and the corresponding slot in the overlay lights up. This proves RPT routes guest input into our window as XInput — the foundation the whole tool relies on.
-
Press Esc in the host window to quit.
If steps 2 and 4 both work, the core premise holds and we proceed to Phase 1 (window capture + input injection). If not, we revisit the donor-attribution approach before building further.
Phase 1a: testing input forwarding locally
This needs no RPT, donor, or second account — just the host, the hook, a
controller, and a target game. coop_host.exe and coop_hook.dll must sit in
the same folder (the build places both in bin/<Config>/).
Requirement: run the target game windowed or borderless, not exclusive fullscreen. Exclusive fullscreen minimizes on focus loss (defeating the focus spoof) and can't be window-captured later. Only controller input is forwarded — while the game is unfocused it won't receive OS keyboard/mouse.
- Start a DRM-free, non-anti-cheat, XInput game in windowed/borderless mode and get to a screen that reads the pad.
- Run
bin\Debug\coop_host.exe. In the Injection panel, filter for the game's.exe, select it, and click Inject & Connect. - Watch the Hook status section. Once it shows Attached and a non-zero "XInput polled: N/s", the game is provably reading our hook — injection works. Focus spoof: active confirms the window was found and subclassed.
- Prove forwarding is the source: tick Forward synthetic test input. The game should now move on its own — left stick sweeping a circle, A pressed every other second — independent of your physical controller. Untick it to return control to your pad.
- Sanity-check the focus spoof: click into another window so the game loses real focus. It should keep running/animating (not pause), and the poll rate should stay non-zero.
- Click Stop forwarding (or quit the host) to tear down the channel.
If injection fails with an access error, run the host as administrator. If it reports "target is 32-bit", that game needs the x86 hook (a later phase).
For a quick sanity check of the forwarding core without a game, run
bin\Debug\hook_selftest.exe — it should print SELFTEST PASS.
Phase 1b: video mirror (Windows Graphics Capture)
The Video mirror panel captures the injected game's window (via Windows Graphics Capture — no injection needed for video) and draws it, letterboxed, as the host window's background. This is what Steam RPT streams to guests.
- Inject into a game as in Phase 1a (the hook reports the game's window, which the mirror uses as its source).
- In the Video mirror panel, tick Mirror game window. The host window should now show a live copy of the game; the panel reports the capture resolution and render FPS.
- The game can be behind the host window — WGC still captures occluded (but not minimized) windows, and focus spoofing keeps it rendering.
Requires Windows 10 1903+ for WGC; hiding the capture border needs build 20348+.
This is the place to judge capture latency/stutter; if it's not good enough, the
fallback is a Present-hook capture path in the same injected DLL.