Mirror the injected game's window into the host's borderless window so Steam RPT streams a live copy of the game. No injection needed for video. - capture/window_capture: WGC capture of a target HWND. Wraps our D3D11 device as an IDirect3DDevice, creates a free-threaded frame pool + capture session, hides the cursor and (best-effort) the capture border. Frames arrive on a WGC thread and are handed to the render thread, which copies the newest into a shader-resource texture and draws it -- keeping all D3D11 context use on one thread. Handles window resize via frame-pool Recreate. - capture/frame_renderer: fullscreen-triangle shader that blits the captured texture letterboxed (aspect-preserved) into the window. - capture_panel: "Mirror game window" toggle + lifecycle; target HWND comes from the hook's reported game window. - main: winrt apartment init; mirrored frame drawn as background, ImGui on top. - CMake: link windowsapp + d3dcompiler. Verified: builds clean; host starts (apartment init + shader compile succeed). Visual capture quality/latency to be judged on a real game. 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.