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>
133 lines
3.7 KiB
C++
133 lines
3.7 KiB
C++
// coop_hook.dll -- injected into the target game by the host.
|
|
//
|
|
// On load it opens the host's shared-memory channel (named by this process's
|
|
// pid), hooks XInput so the game reads the forwarded controller state, and
|
|
// spoofs focus so the game keeps running while the tool holds the real OS focus.
|
|
// All real work happens on a worker thread; DllMain only kicks it off to stay
|
|
// clear of the loader lock.
|
|
|
|
#include <atomic>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <objbase.h>
|
|
|
|
#include "audio_hook.hpp"
|
|
#include "coop/audio_ring.hpp"
|
|
#include "coop/shared_memory.hpp"
|
|
#include "debug_log.hpp"
|
|
#include "focus_spoof.hpp"
|
|
#include "ipc_client.hpp"
|
|
#include "xinput_hook.hpp"
|
|
|
|
namespace
|
|
{
|
|
|
|
coop::hook::IpcClient g_ipc;
|
|
std::atomic<bool> g_running{true};
|
|
coop::SharedMemory g_audio_shm; // the host's audio ring, opened when present
|
|
|
|
DWORD WINAPI worker_thread(LPVOID)
|
|
{
|
|
coop::hook::logf("worker_thread: started");
|
|
|
|
// The host creates the mapping around injection time; give it a few seconds.
|
|
if (!g_ipc.connect(/*attempts=*/200, /*delay_ms=*/25))
|
|
{
|
|
coop::hook::logf("worker_thread: IPC connect FAILED (no host mapping); exiting");
|
|
return 0;
|
|
}
|
|
coop::hook::logf("worker_thread: IPC connected");
|
|
|
|
// The audio render-hook instantiates a COM enumerator on this thread.
|
|
const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
|
|
coop::hook::logf("worker_thread: CoInitializeEx com_ok=%d", com_ok ? 1 : 0);
|
|
|
|
bool xinput_installed = false;
|
|
bool focus_installed = false;
|
|
bool audio_installed = false;
|
|
bool audio_ring_open = false;
|
|
|
|
// Keep retrying the installs (XInput and the game window may both appear
|
|
// lazily) and beat a heartbeat so the host can show the hook is alive.
|
|
while (g_running.load(std::memory_order_relaxed))
|
|
{
|
|
if (!xinput_installed)
|
|
{
|
|
xinput_installed = coop::hook::install_xinput_hooks(g_ipc);
|
|
}
|
|
if (!focus_installed)
|
|
{
|
|
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
|
}
|
|
// Install the audio render-hook even before the host's ring exists, so
|
|
// render streams are counted for the debug view regardless; attach the
|
|
// ring (enabling capture+silence) once the host creates it.
|
|
if (com_ok && !audio_installed)
|
|
{
|
|
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
|
|
if (audio_installed)
|
|
{
|
|
coop::hook::logf("worker_thread: audio hooks installed");
|
|
}
|
|
}
|
|
if (audio_installed && !audio_ring_open)
|
|
{
|
|
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
|
|
if (g_audio_shm.open(name, coop::audio_ring_total_size(coop::kAudioRingCapacity)))
|
|
{
|
|
auto* ring = g_audio_shm.as<coop::AudioRingHeader>();
|
|
if (coop::audio_ring_valid(*ring))
|
|
{
|
|
coop::hook::set_audio_ring(ring);
|
|
audio_ring_open = true;
|
|
coop::hook::logf("worker_thread: audio ring opened");
|
|
}
|
|
else
|
|
{
|
|
g_audio_shm.reset(); // present but not our contract; retry
|
|
}
|
|
}
|
|
}
|
|
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
|
g_ipc.heartbeat();
|
|
Sleep(250);
|
|
}
|
|
|
|
if (com_ok)
|
|
{
|
|
CoUninitialize();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
|
{
|
|
switch (reason)
|
|
{
|
|
case DLL_PROCESS_ATTACH:
|
|
DisableThreadLibraryCalls(module);
|
|
if (HANDLE thread = CreateThread(nullptr, 0, &worker_thread, nullptr, 0, nullptr))
|
|
{
|
|
CloseHandle(thread);
|
|
}
|
|
break;
|
|
case DLL_PROCESS_DETACH:
|
|
// Skip cleanup when the process is tearing down (reserved != null): the
|
|
// loader is already unwinding and touching other modules is unsafe.
|
|
if (reserved == nullptr)
|
|
{
|
|
g_running.store(false, std::memory_order_relaxed);
|
|
coop::hook::remove_focus_spoof();
|
|
coop::hook::remove_xinput_hooks();
|
|
coop::hook::remove_audio_hooks();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return TRUE;
|
|
}
|