Hook registry: list installed hooks + call counts in Injection panel

Add a process-wide hook registry (hook/src/hook_registry) that every hook
module registers its hooks with and bumps a counter from each detour. The
XInput, focus-spoof, and audio render-hooks now register their individual
hooks (XInputGetState/Ex/Caps/SetState; GetForegroundWindow/GetActiveWindow/
GetFocus/WndProc guard; IMMDevice::Activate, IAudioClient::Initialize/
GetService, IAudioRenderClient::GetBuffer/ReleaseBuffer) and count calls.

The worker publishes the table to the host each tick over a new HookStatus
field (protocol v4 -> v5: HookEntry[] + count). The Injection panel shows it
as a collapsible table grouped by subsystem with an installed flag and call
count per hook; coop_audio_probe prints the same table headless.

Verified against Phantom Brave: 13 hooks listed with live counts (focus APIs
polled heavily, GetBuffer/ReleaseBuffer ticking with the audio render loop).
All four tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 20:09:39 +02:00
parent 565934e8cf
commit 1f905940ef
16 changed files with 371 additions and 14 deletions

View File

@@ -14,6 +14,7 @@
#include <safetyhook.hpp>
#include "debug_log.hpp"
#include "hook_registry.hpp"
namespace coop::hook
{
@@ -55,6 +56,13 @@ safetyhook::InlineHook g_hk_getbuffer;
safetyhook::InlineHook g_hk_releasebuffer;
bool g_audioclient_hooked = false;
// Registry ids for the hook list.
int g_id_activate = -1;
int g_id_initialize = -1;
int g_id_getservice = -1;
int g_id_getbuffer = -1;
int g_id_releasebuffer = -1;
// Our own probe COM objects, created at anchor time purely to read the shared
// IAudioClient / IAudioRenderClient vtables and hook GetBuffer/ReleaseBuffer
// *proactively* — so render clients the game created before we injected (the
@@ -142,6 +150,7 @@ void try_register_lazy(IAudioRenderClient* rc);
HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_frames, BYTE** data)
{
hook_note_call(g_id_getbuffer);
const HRESULT hr = g_hk_getbuffer.call<HRESULT>(self, num_frames, data);
if (SUCCEEDED(hr) && data != nullptr)
{
@@ -154,6 +163,7 @@ HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_fram
HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_frames, DWORD flags)
{
hook_note_call(g_id_releasebuffer);
// A render client we've never seen actively rendering is almost certainly one
// the game created before we injected; adopt it now (the first becomes the
// primary we capture). Skip our own silent probe client.
@@ -300,6 +310,7 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX* format, LPCGUID session)
{
hook_note_call(g_id_initialize);
const HRESULT hr =
g_hk_initialize.call<HRESULT>(self, mode, flags, buffer_duration, periodicity, format, session);
logf("hk_Initialize: client=%p mode=%d flags=0x%lX hr=0x%08lX fmt=%s", self, mode,
@@ -314,6 +325,7 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void** ppv)
{
hook_note_call(g_id_getservice);
const HRESULT hr = g_hk_getservice.call<HRESULT>(self, riid, ppv);
const bool is_render = (riid == __uuidof(IAudioRenderClient));
logf("hk_GetService: client=%p hr=0x%08lX render_client=%d", self, static_cast<unsigned long>(hr),
@@ -371,6 +383,7 @@ void install_audioclient_hooks(IAudioClient* ac)
HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ctx, PROPVARIANT* params,
void** ppv)
{
hook_note_call(g_id_activate);
const HRESULT hr = g_hk_activate.call<HRESULT>(self, riid, cls_ctx, params, ppv);
const bool is_audioclient = (riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) ||
riid == __uuidof(IAudioClient3));
@@ -395,6 +408,12 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
return true; // anchor already installed
}
g_id_activate = hook_register("IMMDevice::Activate", HookSubsys_Audio);
g_id_initialize = hook_register("IAudioClient::Initialize", HookSubsys_Audio);
g_id_getservice = hook_register("IAudioClient::GetService", HookSubsys_Audio);
g_id_getbuffer = hook_register("IAudioRenderClient::GetBuffer", HookSubsys_Audio);
g_id_releasebuffer = hook_register("IAudioRenderClient::ReleaseBuffer", HookSubsys_Audio);
// Anchor: instantiate our own enumerator + default render device purely to
// read the shared IMMDevice vtable and hook Activate. Every IMMDevice in the
// process shares this vtable, so the game's Activate calls are intercepted.
@@ -471,6 +490,12 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
// stay valid; they're released in remove_audio_hooks.
}
hook_set_installed(g_id_activate, static_cast<bool>(g_hk_activate));
hook_set_installed(g_id_initialize, static_cast<bool>(g_hk_initialize));
hook_set_installed(g_id_getservice, static_cast<bool>(g_hk_getservice));
hook_set_installed(g_id_getbuffer, static_cast<bool>(g_hk_getbuffer));
hook_set_installed(g_id_releasebuffer, static_cast<bool>(g_hk_releasebuffer));
logf("install_audio_hooks: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d (device=%p)",
static_cast<bool>(g_hk_activate) ? 1 : 0, static_cast<bool>(g_hk_initialize) ? 1 : 0,
static_cast<bool>(g_hk_getservice) ? 1 : 0, static_cast<bool>(g_hk_getbuffer) ? 1 : 0,
@@ -522,6 +547,11 @@ void remove_audio_hooks()
g_hk_initialize = {};
g_hk_activate = {};
g_audioclient_hooked = false;
hook_set_installed(g_id_activate, false);
hook_set_installed(g_id_initialize, false);
hook_set_installed(g_id_getservice, false);
hook_set_installed(g_id_getbuffer, false);
hook_set_installed(g_id_releasebuffer, false);
// Hooks are gone; safe to drop the probe objects that held the vtables.
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel))