Fix audio render-hook missing already-playing streams (late injection)

The render-hook only installed IAudioClient/IAudioRenderClient hooks
reactively, when it saw the game call IMMDevice::Activate -> GetService.
But we attach to a game that is already running and playing audio, so its
render client was created before injection: those calls never fire again,
no primary stream is ever registered, nothing is captured, and the host
always falls back to process loopback (the echo). Every game tested did so.

Fix: at anchor time, build our own probe IAudioClient + IAudioRenderClient
with raw calls and hook GetBuffer/ReleaseBuffer (plus Initialize/GetService)
on their vtables. Every instance of a COM coclass shares one vtable, so 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 primary on the audio thread (try-lock, one-time) using the device
mix format as its assumed format (we never saw its Initialize). Streams
created after injection still register via the reactive path with their real
format.

Also fixes a self-deadlock: installing the Activate hook before the probe's
own device->Activate call re-entered hk_Activate -> install_audioclient_hooks,
which blocked on the setup mutex the installer already held, freezing the
worker (and any game thread that later called Activate -> crash). The probe
objects are now created raw, before any hook is installed.

Validated against Phantom Brave (injected while already playing): the
pre-existing 48 kHz/2ch/float render client is detected and registered as
primary, real non-silent audio reaches the ring (peak tracks the game's
levels), and a draining consumer sees zero overruns.

Tooling for iterating on real games without Steam/RPT/the host UI:
- tools/audio_probe: creates the IPC block + audio ring, injects the hook,
  drains the ring and prints stream/format/peak/overrun diagnostics by pid.
- hook/src/debug_log: opt-in file trace (%TEMP%\coop_hook.log), enabled by
  the COOP_HOOK_LOG env var or the %TEMP%\coop_hook.log.on sentinel the probe
  drops; off in normal use.

All four tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 14:53:52 +02:00
parent 2f1c036320
commit c7be4eeb9a
9 changed files with 493 additions and 20 deletions

86
hook/src/debug_log.cpp Normal file
View File

@@ -0,0 +1,86 @@
#define _CRT_SECURE_NO_WARNINGS
#include "debug_log.hpp"
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <windows.h>
namespace coop::hook
{
namespace
{
std::mutex g_log_mutex;
FILE* g_log_file = nullptr;
bool g_log_tried = false;
// Logging is opt-in so an injected DLL doesn't write to disk in normal use.
// Enable it by setting the COOP_HOOK_LOG environment variable for the target, or
// (more practical for a Steam-launched game we can't set env on) by creating the
// sentinel file %TEMP%\coop_hook.log.on — every process sees the same %TEMP%, so
// the probe / debugger can flip it without touching the game's environment.
bool logging_enabled()
{
wchar_t buf[8] = {};
if (GetEnvironmentVariableW(L"COOP_HOOK_LOG", buf, 8) > 0)
{
return true;
}
wchar_t dir[MAX_PATH] = {};
const DWORD n = GetTempPathW(MAX_PATH, dir);
if (n != 0 && n < MAX_PATH)
{
const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on";
return GetFileAttributesW(sentinel.c_str()) != INVALID_FILE_ATTRIBUTES;
}
return false;
}
FILE* log_file_locked()
{
if (!g_log_tried)
{
g_log_tried = true;
if (logging_enabled())
{
wchar_t dir[MAX_PATH] = {};
const DWORD n = GetTempPathW(MAX_PATH, dir);
if (n != 0 && n < MAX_PATH)
{
std::wstring path = std::wstring(dir) + L"coop_hook.log";
g_log_file = _wfopen(path.c_str(), L"a");
}
}
}
return g_log_file;
}
} // namespace
void logf(const char* fmt, ...)
{
std::scoped_lock lock(g_log_mutex);
FILE* f = log_file_locked();
if (f == nullptr)
{
return;
}
SYSTEMTIME st;
GetLocalTime(&st);
std::fprintf(f, "[%02u:%02u:%02u.%03u pid=%lu] ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
GetCurrentProcessId());
va_list args;
va_start(args, fmt);
std::vfprintf(f, fmt, args);
va_end(args);
std::fputc('\n', f);
std::fflush(f);
}
} // namespace coop::hook