docs: record the late-injection render-hook fix + probe tool
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>
This commit is contained in:
11
README.md
11
README.md
@@ -142,6 +142,17 @@ ctest --test-dir build -C Debug --output-on-failure
|
||||
shipping process-loopback capture (the fallback path) receives its audio by
|
||||
PID. Skips cleanly if the machine has no audio endpoint.
|
||||
|
||||
### Debugging the render-hook against a real game
|
||||
|
||||
[`tools/audio_probe`](tools/audio_probe) (`coop_audio_probe.exe <pid> [seconds]`)
|
||||
brings up the audio render-hook without Steam / RPT / the host UI: it creates the
|
||||
IPC block + audio ring the hook expects, injects `coop_hook.dll` into the target
|
||||
game, then drains the ring and prints per-stream format, captured-frame counts,
|
||||
peak amplitude (proves the audio is real, not silence), and overruns. It enables
|
||||
the hook's file trace (`%TEMP%\coop_hook.log`) for the run. Run it from
|
||||
`bin/<config>/`. **Kill the game between runs** — the loaded DLL locks
|
||||
`coop_hook.dll` against the next rebuild.
|
||||
|
||||
## Running the tool (manual, end-to-end)
|
||||
|
||||
This needs Steam, a donor game that supports Remote Play Together, and a second
|
||||
|
||||
@@ -1,12 +1,44 @@
|
||||
# Plan: fix the local audio echo via an injection render-hook (Option B)
|
||||
|
||||
Status: **implemented (M1–M4), pending manual end-to-end (M5).** Milestones 1–4
|
||||
below are built, committed, and covered by `audio_ring_test` + `audio_hook_test`
|
||||
(in-process). The remaining step is M5: confirm in a real game with a guest that
|
||||
there's no local echo, the guest still hears audio, and the stream count reads
|
||||
correctly. This document is the green-lit design; the only correction applied
|
||||
during implementation was the `IAudioClient::GetService` vtable index (14, not
|
||||
13 — `SetEventHandle` is 13).
|
||||
Status: **implemented and validated against a real already-playing game.** M1–M4
|
||||
are built and covered by `audio_ring_test` + `audio_hook_test`. The render-hook
|
||||
now correctly captures a game's audio when injected into an already-running,
|
||||
already-playing process (verified with Phantom Brave, `coop_audio_probe`: the
|
||||
pre-existing 48 kHz/2ch/float render client is detected, real non-silent audio
|
||||
reaches the ring, zero overruns when consumed). The remaining step is the human
|
||||
end-to-end with a guest over RPT: no local echo, guest hears audio.
|
||||
|
||||
Two corrections were applied during/after implementation: the
|
||||
`IAudioClient::GetService` vtable index is **14**, not 13 (`SetEventHandle` is
|
||||
13); and the inner hooks must be installed **proactively** (see below), not only
|
||||
reactively — the original reactive-only design captured nothing on late injection,
|
||||
which is the normal case.
|
||||
|
||||
## Lesson: reactive-only hooking fails on late injection (the bug that broke it)
|
||||
|
||||
The first cut installed the IAudioClient / IAudioRenderClient hooks only when the
|
||||
*game* called `IMMDevice::Activate` → `GetService`. But the tool injects into a
|
||||
game that is already running and already playing — its render client was created
|
||||
before we attached, so those calls never fire again. Result: no stream is ever
|
||||
registered, nothing is captured, and the host always falls back to process
|
||||
loopback (the echo). Every game tested fell back.
|
||||
|
||||
Fix (commit `c7be4ee`): at anchor time, build our **own** probe `IAudioClient` +
|
||||
`IAudioRenderClient` with raw calls and hook `GetBuffer`/`ReleaseBuffer` (plus
|
||||
`Initialize`/`GetService`) on *their* vtables. Because every instance of a COM
|
||||
coclass shares one vtable, this patches the shared vtables and intercepts the
|
||||
game's pre-existing render client too. The first render client seen actively
|
||||
releasing buffers is adopted as the primary on the audio thread (try-lock,
|
||||
one-time) using the device **mix format** as its assumed format — we never saw its
|
||||
`Initialize`, and shared-mode clients overwhelmingly use the mix format. Streams
|
||||
created *after* injection still register via the reactive path with their real
|
||||
format.
|
||||
|
||||
Sub-lesson — ordering / self-deadlock: create the probe objects *before*
|
||||
installing the `Activate` hook. If `Activate` is hooked first, the probe's own
|
||||
`device->Activate` re-enters `hk_Activate` → `install_audioclient_hooks`, which
|
||||
blocks on the setup mutex the installer already holds — freezing the worker thread
|
||||
(and any game thread that later calls `Activate`, which crashed the game).
|
||||
|
||||
## Problem
|
||||
|
||||
|
||||
Reference in New Issue
Block a user