In the full app the host creates the audio ring only when the operator toggles
audio mirroring on -- after injection. So the hook registers the game's primary
render stream while the ring is still null, and register_render_client_locked
skips publishing the format (nothing to publish to). When the ring later
attaches via set_audio_ring, the already-registered stream's format was never
re-published: format_valid stayed 0, the host's wait_for_format timed out, and
it fell back to loopback (the echo) -- on every game, including Phantom Brave.
The in-process probe created the ring before injecting, so it never reproduced
this.
Fix: the hook stores the primary stream's format and republish_audio_format()
publishes it whenever a ring is attached but has no format yet -- called from
set_audio_ring and once per worker tick (the tick also covers the host
re-initializing the ring on a mirror re-toggle, which clears format_valid).
coop_audio_probe now creates the ring ~1.5 s AFTER injecting by default
(ring_delay_ms arg) to match the app's ordering. Verified against Phantom
Brave: the log shows "primary stream set ... no ring yet" at inject, then
"republish_audio_format: published 48000Hz/2ch/32bit" when the ring attaches,
and the host-shaped consumer then drains real audio with zero overruns.
All four tests still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
List the human/full-setup checks that producer-side capture can't prove on
its own: hooked audio over RPT end-to-end, the mix-format assumption for
streams that predate injection, and multi-stream games.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the plan's status (validated against a real already-playing game),
add the "reactive-only hooking fails on late injection" lesson and the
self-deadlock ordering sub-lesson, and document tools/audio_probe in the
README. Mirrors commit c7be4ee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: audio mirror is now the injected render-hook with process-loopback as
automatic fallback; update Architecture/Limitations/Status/Roadmap and the
manual run step (Source indicator + render-stream table). Add tests
(audio_ring_test, audio_hook_test) and a lessons-learned note on COM vtable
indices (IAudioClient::GetService is 14, not 13).
- plan doc: mark M1-M4 implemented/committed, M5 (real-game end-to-end) pending,
and record the GetService index correction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reflect audio mirror (Phase 2) and video mirror (Phase 1b) as implemented;
drop stale "(current)"/"still ahead" framing and the broken docs/ link.
- Add a Roadmap section for the planned work: toggle/generalize the debug UI,
fix the local audio echo, Present-hook video path, Steam Input, x86 support.
- Add Lessons learned (RPT focus requirement, no exclusive fullscreen, the
ActivateAudioInterfaceAsync agile-handler gotcha, WGC occlusion, audio echo).
- Document clangd/compile_commands setup and the CTest suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Two problems surfaced in testing: (1) no way to tell whether the injected
hook was actually the input source, and (2) the final design needs the tool
window focused for Steam RPT capture, which would pause/silence games that
react to focus loss. Both are addressed here.
- Focus spoofing (hook/focus_spoof): find the game's main window, subclass it
to rewrite/swallow WM_ACTIVATE/ACTIVATEAPP/NCACTIVATE/KILLFOCUS, and inline-
hook GetForegroundWindow/GetActiveWindow/GetFocus to always report the game
as active. The game keeps running and polling while unfocused.
- Status back-channel (protocol v2): the DLL reports attached/focus-spoof
flags, game pid/hwnd, a heartbeat, and a cumulative XInputGetState counter.
The host overlay turns the counter into a live poll rate, so "is the hook
working" is directly observable.
- Synthetic test-input toggle in the host: forwards a known automated pattern
(stick circle + periodic A) to prove forwarding independent of the physical
pad.
- hook_selftest extended to assert the status channel; passes.
Documented the windowed/borderless requirement and the new observable test
flow in the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The host can now inject coop_hook.dll into a running game and forward
controller state to it over shared memory, so the game reads the host's
(eventually the guest's) input and nothing else.
- hook/: coop_hook.dll. DllMain spawns a worker that opens the shared-memory
channel (named by the game's pid) and installs SafetyHook inline hooks on
XInputGetState/GetStateEx/GetCapabilities/SetState. Detours synthesize state
from shared memory; unmanaged slots report disconnected, hiding physical pads.
- host/: process picker (Toolhelp32), CreateRemoteThread(LoadLibraryW) injector
with an IsWow64Process2 bitness guard, IPC server publishing pads each frame,
and an ImGui Injection panel wiring it together.
- tests/: hook_selftest exercises the IPC seqlock + hook detours in-process
(no game/controller needed); passes.
Build: SafetyHook wired in (COOP_BUILD_HOOK=ON), Zydis via FetchContent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold CoopAllTheThings: Remote Play Together for any XInput game via a
mirror app under a donor appid (real game keeps its own appid, so DRM,
achievements, and playtime stay intact).
- Build: CMake skeleton, ImGui + SafetyHook submodules (no vcpkg)
- common/: host<->hook IPC contract (seqlock pad state, shared-memory RAII)
- host/: borderless D3D11 window + ImGui overlay listing visible XInput pads,
behind an InputSource interface (Steam Input slots in later)
- README documents the Phase 0 donor-launch validation procedure, anti-cheat
limitation, and XInput/bitness constraints
Phase 0 validates the riskiest assumption (Steam RPT streams an arbitrary
window under a donor appid and routes guest input to it) before capture and
injection are built.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>