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

103
hook/src/hook_registry.cpp Normal file
View File

@@ -0,0 +1,103 @@
#include "hook_registry.hpp"
#include <atomic>
#include <cstring>
#include <mutex>
namespace coop::hook
{
namespace
{
struct Slot
{
char name[40] = {};
std::atomic<std::uint32_t> subsystem{0};
std::atomic<std::uint32_t> installed{0};
std::atomic<std::uint64_t> calls{0};
std::atomic<std::uint32_t> used{0};
};
Slot g_slots[kMaxHookEntries];
std::atomic<std::uint32_t> g_count{0}; // high-water mark of allocated slots
std::mutex g_register_mutex; // registration only (rare); calls are lock-free
} // namespace
int hook_register(const char* name, std::uint32_t subsystem)
{
std::scoped_lock lock(g_register_mutex);
const std::uint32_t count = g_count.load(std::memory_order_relaxed);
for (std::uint32_t i = 0; i < count; ++i)
{
if (g_slots[i].used.load(std::memory_order_relaxed) && std::strcmp(g_slots[i].name, name) == 0)
{
return static_cast<int>(i); // already registered
}
}
if (count >= kMaxHookEntries)
{
return -1; // table full
}
Slot& s = g_slots[count];
std::strncpy(s.name, name, sizeof(s.name) - 1);
s.name[sizeof(s.name) - 1] = '\0';
s.subsystem.store(subsystem, std::memory_order_relaxed);
s.installed.store(0, std::memory_order_relaxed);
s.calls.store(0, std::memory_order_relaxed);
s.used.store(1, std::memory_order_release);
g_count.store(count + 1, std::memory_order_release);
return static_cast<int>(count);
}
void hook_set_installed(int id, bool installed)
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
{
g_slots[id].installed.store(installed ? 1u : 0u, std::memory_order_relaxed);
}
}
void hook_note_call(int id)
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
{
g_slots[id].calls.fetch_add(1, std::memory_order_relaxed);
}
}
void hook_publish(IpcClient& ipc)
{
const std::uint32_t count = g_count.load(std::memory_order_acquire);
HookEntry entries[kMaxHookEntries];
std::uint32_t n = 0;
for (std::uint32_t i = 0; i < count && i < kMaxHookEntries; ++i)
{
if (!g_slots[i].used.load(std::memory_order_acquire))
{
continue;
}
HookEntry& e = entries[n];
std::memcpy(e.name, g_slots[i].name, sizeof(e.name));
e.subsystem = g_slots[i].subsystem.load(std::memory_order_relaxed);
e.installed = g_slots[i].installed.load(std::memory_order_relaxed);
e.calls = g_slots[i].calls.load(std::memory_order_relaxed);
++n;
}
ipc.publish_hook_entries(entries, n);
}
void hook_registry_reset()
{
std::scoped_lock lock(g_register_mutex);
for (auto& s : g_slots)
{
s.used.store(0, std::memory_order_relaxed);
s.installed.store(0, std::memory_order_relaxed);
s.calls.store(0, std::memory_order_relaxed);
}
g_count.store(0, std::memory_order_release);
}
} // namespace coop::hook