7 Commits

Author SHA1 Message Date
51e2c4f4cd Preserve SafetyHook concurrency finding as a committed upstream repro
A multithreaded test that tight-looped InlineHook enable()/disable() while
other threads called the hooked function flaked ~1/10. Isolation proved this is
a SafetyHook limitation, not our code: with the hook created once (no install
race, no trampoline UAF), tight-loop toggling AVs ~1/3 of runs in Debug
(0xC0000005, faulting RIP in the target body), while a no-toggle control is
clean at ~60M calls. enable()/disable() re-patch the prologue in place under a
VEH page-trap that only relocates a thread parked ON the prologue; a thread in
the function body faults on the briefly-non-exec page and relies on instruction
retry, which under rapid toggling races a half-rewritten prologue.

Rather than silently drop the flaky test, preserve the finding:
- tools/sh_concurrency_repro/: minimal, committed, non-CI reproducer
  (coop_sh_concurrency_repro; --callonly is the control). Surfaces 5/16 AVs.
- docs/safetyhook-concurrency.md: upstream-ready write-up (mechanism + fix
  directions + why it does not affect us).
- README lessons-learned + memory updated; tests/CMakeLists cross-references it.

Our code stays in SafetyHook's safe envelope (install/remove reconciled from a
single tick-bounded worker thread, never a tight loop), so the mock_game_test
storm is reliably green; the persistent-trampoline contract is covered
deterministically by hook_install_test + detour_gate_test. Removes the temp
_sh_probe wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:23:06 +02:00
2432e49784 Condense README lessons; drop implemented audio-render-hook plan doc
The docs/audio-render-hook-plan.md was a fully-implemented, validated design
doc; its still-relevant gotchas (GetService idx 14, agile completion handler,
loopback doesn't mute) already live in the README. Tighten the Lessons learned
section and merge the two x86 SafetyHook traps into one bullet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:05:20 +02:00
4e076420bf Fix audio falling back to echo in the full app (format not published)
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>
2026-06-19 15:23:30 +02:00
365043c3da 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>
2026-06-19 14:55:11 +02:00
2f1c036320 Audio render-hook: document the echo fix (M1-M4 done, M5 manual)
- 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>
2026-06-19 14:21:38 +02:00
4e3814a072 Audio render-hook M2: render hook + in-process self-test (concept proven)
Implements the WASAPI render-hook (hook/src/audio_hook.{hpp,cpp}) and an
in-process self-test that proves COM vtable discovery and GetBuffer/ReleaseBuffer
interception with no game and no second Steam account.

- audio_hook.cpp: anchors on IMMDevice::Activate (idx 3) off our own default
  endpoint (shared vtable), then hooks IAudioClient::Initialize (3) /
  GetService (14) and IAudioRenderClient::GetBuffer (3) / ReleaseBuffer (4) off
  live game pointers. Copies primary-stream frames into the audio ring and
  releases with AUDCLNT_BUFFERFLAGS_SILENT (+ memset belt-and-suspenders), only
  while the host-owned capture_enabled flag is set. Stream counting runs always;
  on a ring overrun it keeps playing locally rather than going silent.
- ipc_client.hpp: publish_audio_stream / note_audio_frames /
  set_audio_streams_seen write the render-stream debug fields into HookStatus.
- tests/audio_hook_test.cpp: installs the hooks, renders a tone through WASAPI
  in-process, and asserts exactly one stream, frames pushed to the ring, the
  ring carries the non-silent tone, and the primary was silenced. PASS:
  streams_seen=1, frames_captured=32640.
- plan doc: correct GetService vtable index 13 -> 14 (SetEventHandle is 13).

coop_hook DLL wiring + host consumer/fallback come next (M3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:57:28 +02:00
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