Apply clang-format across the whole tree

Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs,
Allman functions) over every source file so the tree is formatter-clean.
Whitespace only -- no behavior change; full x64 + x86 suites pass.

Also set SortIncludes: false in .clang-format. Windows include order is
load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h /
dinput.h; winsock2.h must precede windows.h), and the default
alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build
break. Leaving order alone keeps the manual, correct grouping.
This commit is contained in:
2026-07-12 11:52:53 +02:00
parent c684a15fb9
commit 30eccf749d
155 changed files with 3333 additions and 6171 deletions

View File

@@ -17,11 +17,9 @@
#include "rate_estimator.hpp"
#include "vtable_hook.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
// COM vtable indices (frozen ABI). IUnknown occupies 0..2.
// IMMDevice: Activate = 3
@@ -50,8 +48,7 @@ using ReleaseBufferFn = HRESULT(STDMETHODCALLTYPE*)(IAudioRenderClient*, UINT32,
// Swapping the slot leaves the original code untouched.
// The scalar audio format we forward; resolved from the game's WAVEFORMATEX.
struct CapturedFormat
{
struct CapturedFormat {
std::uint32_t rate = 0;
std::uint32_t channels = 0;
std::uint32_t bits = 0;
@@ -126,15 +123,14 @@ CapturedFormat g_stream_formats[kMaxAudioStreams];
std::unordered_map<IAudioClient*, CapturedFormat> g_client_formats;
// Streams we track (frame counting + per-stream capture). Index 0 is primary.
struct TrackedStream
{
struct TrackedStream {
std::atomic<IAudioRenderClient*> client{nullptr};
std::atomic<std::uint64_t> frames{0};
std::atomic<std::uint32_t> block_align{0}; // hot-path frame size for this stream
std::atomic<std::uint32_t> assumed_format{0}; // 1 = channels/bits guessed -> clamp copies safely
std::atomic<std::uint32_t> block_align{0}; // hot-path frame size for this stream
std::atomic<std::uint32_t> assumed_format{0}; // 1 = channels/bits guessed -> clamp copies safely
};
TrackedStream g_streams[kMaxAudioStreams];
std::uint32_t g_registered = 0; // slots filled (<= kMaxAudioStreams), under mutex
std::uint32_t g_registered = 0; // slots filled (<= kMaxAudioStreams), under mutex
std::atomic<std::uint32_t> g_streams_seen{0}; // total distinct clients ever seen
std::atomic<std::uint64_t> g_frames_captured{0}; // total frames captured across streams
@@ -183,20 +179,15 @@ CapturedFormat capture_format(const WAVEFORMATEX* wfx)
cf.bits = wfx->wBitsPerSample;
cf.block_align = wfx->nBlockAlign;
cf.tag = wfx->wFormatTag;
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22)
{
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22) {
const auto* ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(wfx);
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
{
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
cf.tag = WAVE_FORMAT_IEEE_FLOAT;
}
else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
{
} else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
cf.tag = WAVE_FORMAT_PCM;
}
}
if (cf.block_align == 0)
{
if (cf.block_align == 0) {
cf.block_align = cf.channels * (cf.bits / 8);
}
return cf;
@@ -214,13 +205,10 @@ void try_register_lazy(IAudioRenderClient* rc);
std::uint32_t readable_bytes(const void* ptr, std::uint32_t want)
{
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == sizeof(mbi) && mbi.State == MEM_COMMIT)
{
if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == sizeof(mbi) && mbi.State == MEM_COMMIT) {
const auto* base = static_cast<const std::uint8_t*>(mbi.BaseAddress);
const auto avail = static_cast<std::uintptr_t>((base + mbi.RegionSize) -
static_cast<const std::uint8_t*>(ptr));
if (avail < want)
{
const auto avail = static_cast<std::uintptr_t>((base + mbi.RegionSize) - static_cast<const std::uint8_t*>(ptr));
if (avail < want) {
return static_cast<std::uint32_t>(avail);
}
}
@@ -232,8 +220,7 @@ HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_fram
DetourGate::Guard guard(g_gate); // in-flight until return (drained before an unhook tears down)
hook_note_call(g_id_getbuffer);
const HRESULT hr = g_vh_getbuffer.original<GetBufferFn>()(self, num_frames, data);
if (SUCCEEDED(hr) && data != nullptr)
{
if (SUCCEEDED(hr) && data != nullptr) {
t_gb_client = self;
t_gb_data = *data;
t_gb_frames = num_frames;
@@ -249,37 +236,32 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// 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.
if (self != g_self_render.load(std::memory_order_acquire) && num_frames > 0 && !stream_tracked(self))
{
if (self != g_self_render.load(std::memory_order_acquire) && num_frames > 0 && !stream_tracked(self)) {
try_register_lazy(self);
}
// Per tracked stream: count frames (debug view) and, into the stream's own ring,
// capture + silence its buffer while capture is enabled. Every stream is captured
// into its own ring; the host mixes them.
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
{
if (g_streams[i].client.load(std::memory_order_acquire) != self)
{
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
if (g_streams[i].client.load(std::memory_order_acquire) != self) {
continue;
}
const std::uint64_t total =
g_streams[i].frames.fetch_add(num_frames, std::memory_order_relaxed) + num_frames;
const std::uint64_t total = g_streams[i].frames.fetch_add(num_frames, std::memory_order_relaxed) + num_frames;
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire)) // load once (unhook may null it)
{
ipc->note_audio_frames(i, total);
}
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0)
{
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
// Only capture once the format is published -- for a guessed-rate stream that's
// after the true rate is measured, so we never capture/silence audio we'd
// mis-rate (and don't build a backlog while measuring; the game stays audible).
if (ring != nullptr && ring->capture_enabled.load(std::memory_order_relaxed) != 0 &&
audio_ring_format_ready(*ring) && t_gb_client == self && t_gb_data != nullptr &&
t_gb_frames == num_frames &&
t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) // same hooked epoch as the GetBuffer
if (ring != nullptr && ring->capture_enabled.load(std::memory_order_relaxed) != 0
&& audio_ring_format_ready(*ring) && t_gb_client == self && t_gb_data != nullptr
&& t_gb_frames == num_frames
&& t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) // same hooked epoch as the GetBuffer
{
const bool guessed = g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0;
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
@@ -287,15 +269,13 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// A guessed (pre-existing client) stream's block may be larger than the real
// per-frame size, so clamp the COPY to what's actually readable -- never over-read
// the game's buffer (no-op when the guess is right).
if (guessed)
{
if (guessed) {
bytes = readable_bytes(t_gb_data, bytes);
}
// Only silence if the frames made it into the ring; if the host has
// stalled (ring full) keep playing locally rather than going dead
// silent — degrades to today's echo, never to silence.
if (block != 0 && audio_ring_push(*ring, t_gb_data, bytes, num_frames))
{
if (block != 0 && audio_ring_push(*ring, t_gb_data, bytes, num_frames)) {
g_frames_captured.fetch_add(num_frames, std::memory_order_relaxed);
// Mute the game's local playback so the only audio is the host's re-render.
// Otherwise the game plays locally AND the mirror re-renders the same audio a
@@ -306,13 +286,12 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// format we additionally zero the buffer (belt-and-suspenders; `block` is the
// real frame size there, so it stays in-bounds). Only mutes once the frames made
// the ring (above) -- a stalled host degrades to echo, never to dead silence.
if (!guessed)
{
if (!guessed) {
std::memset(t_gb_data, 0, bytes);
}
g_frames_silenced.fetch_add(num_frames, std::memory_order_relaxed);
return g_vh_releasebuffer.original<ReleaseBufferFn>()(
self, num_frames, flags | AUDCLNT_BUFFERFLAGS_SILENT);
return g_vh_releasebuffer.original<ReleaseBufferFn>()(self, num_frames,
flags | AUDCLNT_BUFFERFLAGS_SILENT);
}
}
}
@@ -323,18 +302,14 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// audio and cross-correlate them to recover the true format from ground truth. The game
// stays audible (the host runs loopback during the measurement window anyway), and the
// shipping no-echo capture/silence path above is left completely untouched.
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0)
{
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
AudioRingHeader* vring = g_rings[i].load(std::memory_order_acquire);
if (vring != nullptr && vring->verify_capture.load(std::memory_order_relaxed) != 0 &&
!audio_ring_format_ready(*vring) &&
g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0 && t_gb_client == self &&
t_gb_data != nullptr && t_gb_frames == num_frames &&
t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire))
{
if (vring != nullptr && vring->verify_capture.load(std::memory_order_relaxed) != 0
&& !audio_ring_format_ready(*vring) && g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0
&& t_gb_client == self && t_gb_data != nullptr && t_gb_frames == num_frames
&& t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) {
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
if (block != 0)
{
if (block != 0) {
// Self-describing chunk: [u32 frame-count][num_frames*block bytes]. The host can't
// know the real frame size of a guessed stream, so it recovers the layout by
// trying candidate de-interleavings -- but it needs the frame count to strip the
@@ -342,9 +317,8 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// channels/bits). Push both parts only if both fit and the payload is fully
// readable, so a full ring or a short buffer can never tear the framing.
const std::uint32_t want = num_frames * block;
if (readable_bytes(t_gb_data, want) == want &&
audio_ring_free_space(*vring) >= static_cast<std::uint32_t>(sizeof(num_frames)) + want)
{
if (readable_bytes(t_gb_data, want) == want
&& audio_ring_free_space(*vring) >= static_cast<std::uint32_t>(sizeof(num_frames)) + want) {
audio_ring_push(*vring, &num_frames, sizeof(num_frames), 0);
audio_ring_push(*vring, t_gb_data, want, num_frames);
}
@@ -358,12 +332,10 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
// Publish a stream's format + state to the host's per-stream debug channel. Caller holds
// g_setup_mutex.
void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, std::uint32_t state,
std::uint64_t frames)
void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, std::uint32_t state, std::uint64_t frames)
{
IpcClient* ipc = g_ipc.load(std::memory_order_acquire);
if (ipc == nullptr)
{
if (ipc == nullptr) {
return;
}
AudioStreamInfo info{};
@@ -384,35 +356,28 @@ void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, st
bool publish_stream_format_locked(std::uint32_t slot)
{
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
if (ring == nullptr || g_stream_formats[slot].rate == 0)
{
if (ring == nullptr || g_stream_formats[slot].rate == 0) {
return false; // no ring attached yet, or no stream in this slot
}
if (audio_ring_format_ready(*ring))
{
if (audio_ring_format_ready(*ring)) {
return true; // already published
}
CapturedFormat cf = g_stream_formats[slot];
if (g_stream_rate_guess[slot])
{
if (g_stream_rate_guess[slot]) {
// Feed this tick's render cadence to the estimator; it only commits on consensus
// across standard-rate windows, or a low-confidence fallback after enough attempts.
LARGE_INTEGER now{}, freq{};
QueryPerformanceCounter(&now);
QueryPerformanceFrequency(&freq);
const RateEstimate est = g_rate_estimator[slot].feed(
g_streams[slot].frames.load(std::memory_order_relaxed), now.QuadPart, freq.QuadPart);
if (!est.done)
{
const RateEstimate est = g_rate_estimator[slot].feed(g_streams[slot].frames.load(std::memory_order_relaxed),
now.QuadPart, freq.QuadPart);
if (!est.done) {
return false; // still measuring; caller retries next tick
}
const std::uint32_t state = est.confident ? AudioFormat_Measured : AudioFormat_LowConfidence;
if (est.confident)
{
if (est.confident) {
logf("audio stream %u: measured rate %uHz (was guessing %uHz)", slot, est.rate, cf.rate);
}
else
{
} else {
logw("audio stream %u: rate %uHz is a LOW-CONFIDENCE estimate (no consensus) -- verify or "
"override",
slot, est.rate);
@@ -435,12 +400,10 @@ bool publish_stream_format_locked(std::uint32_t slot)
void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
{
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
if (ring == nullptr || g_stream_formats[slot].rate == 0)
{
if (ring == nullptr || g_stream_formats[slot].rate == 0) {
return; // no ring / no stream in this slot
}
if (cmd.kind == AudioRingOp_Remeasure)
{
if (cmd.kind == AudioRingOp_Remeasure) {
logw("audio stream %u: operator requested re-measure", slot);
g_stream_rate_guess[slot] = true;
g_rate_estimator[slot] = RateEstimator{};
@@ -450,23 +413,19 @@ void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
ring->format_valid.store(0, std::memory_order_release); // force re-publish after measuring
publish_stream_info_locked(slot, g_stream_formats[slot], AudioFormat_Measuring,
g_streams[slot].frames.load(std::memory_order_relaxed));
}
else if (cmd.kind == AudioRingOp_Override)
{
} else if (cmd.kind == AudioRingOp_Override) {
CapturedFormat cf;
cf.rate = cmd.rate;
cf.channels = cmd.channels;
cf.bits = cmd.bits;
cf.tag = cmd.format_tag ? cmd.format_tag : WAVE_FORMAT_PCM;
cf.block_align = cmd.channels * (cmd.bits / 8);
if (cf.rate == 0 || cf.channels == 0 || cf.block_align == 0)
{
logw("audio stream %u: ignoring invalid override %uHz/%uch/%ubit", slot, cf.rate, cf.channels,
cf.bits);
if (cf.rate == 0 || cf.channels == 0 || cf.block_align == 0) {
logw("audio stream %u: ignoring invalid override %uHz/%uch/%ubit", slot, cf.rate, cf.channels, cf.bits);
return;
}
logw("audio stream %u: operator override -> %uHz/%uch/%ubit tag=%u", slot, cf.rate, cf.channels,
cf.bits, cf.tag);
logw("audio stream %u: operator override -> %uHz/%uch/%ubit tag=%u", slot, cf.rate, cf.channels, cf.bits,
cf.tag);
g_stream_formats[slot] = cf;
g_stream_rate_guess[slot] = false;
g_stream_format_state[slot] = AudioFormat_Override;
@@ -487,25 +446,21 @@ void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
// the format is published). Caller holds g_setup_mutex.
void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat& cf, bool rate_is_guess)
{
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
{
if (g_streams[i].client.load(std::memory_order_relaxed) == rc)
{
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
if (g_streams[i].client.load(std::memory_order_relaxed) == rc) {
return; // already tracked
}
}
const std::uint32_t seen = g_streams_seen.fetch_add(1, std::memory_order_relaxed) + 1;
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire))
{
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire)) {
ipc->set_audio_streams_seen(seen);
}
logf("register_render_client: rc=%p seen=%u fmt=%uHz/%uch/%ubit tag=%u block=%u", rc, seen, cf.rate,
cf.channels, cf.bits, cf.tag, cf.block_align);
logf("register_render_client: rc=%p seen=%u fmt=%uHz/%uch/%ubit tag=%u block=%u", rc, seen, cf.rate, cf.channels,
cf.bits, cf.tag, cf.block_align);
const std::uint32_t slot = g_registered;
if (slot >= kMaxAudioStreams)
{
if (slot >= kMaxAudioStreams) {
return; // more streams than debug slots; counted above, not detailed
}
g_registered = slot + 1;
@@ -520,16 +475,13 @@ void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat&
g_streams[slot].block_align.store(cf.block_align, std::memory_order_relaxed); // before client (hot path)
g_streams[slot].client.store(rc, std::memory_order_release);
if (rate_is_guess)
{
if (rate_is_guess) {
logf("audio stream %u: format unknown (pre-existing client) -> assuming device mix %uHz/%uch/%ubit; "
"measuring true rate; channels/bits assumed (verified byte-compatible before capture)",
slot, cf.rate, cf.channels, cf.bits);
}
else
{
logf("audio stream %u: exact format %uHz/%uch/%ubit from the game's Initialize", slot, cf.rate,
cf.channels, cf.bits);
} else {
logf("audio stream %u: exact format %uHz/%uch/%ubit from the game's Initialize", slot, cf.rate, cf.channels,
cf.bits);
}
publish_stream_info_locked(slot, cf, state, 0);
@@ -544,10 +496,8 @@ void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat&
// True if `rc` already occupies a tracked debug slot (lock-free scan).
bool stream_tracked(IAudioRenderClient* rc)
{
for (auto& s : g_streams)
{
if (s.client.load(std::memory_order_acquire) == rc)
{
for (auto& s : g_streams) {
if (s.client.load(std::memory_order_acquire) == rc) {
return true;
}
}
@@ -559,17 +509,14 @@ bool stream_tracked(IAudioRenderClient* rc)
// as a best guess. Non-blocking: if setup is momentarily busy, retry next call.
void try_register_lazy(IAudioRenderClient* rc)
{
if (g_have_mix_format.load(std::memory_order_acquire) == 0)
{
if (g_have_mix_format.load(std::memory_order_acquire) == 0) {
return;
}
std::unique_lock<std::mutex> lock(g_setup_mutex, std::try_to_lock);
if (!lock.owns_lock())
{
if (!lock.owns_lock()) {
return; // another thread is in setup; try again on the next buffer
}
if (stream_tracked(rc))
{
if (stream_tracked(rc)) {
return; // a concurrent path registered it first
}
logf("try_register_lazy: discovered pre-existing render client rc=%p", rc);
@@ -581,12 +528,11 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
const WAVEFORMATEX* format, LPCGUID session)
{
hook_note_call(g_id_initialize);
const HRESULT hr = g_vh_initialize.original<InitializeFn>()(self, mode, flags, buffer_duration,
periodicity, format, session);
const HRESULT hr =
g_vh_initialize.original<InitializeFn>()(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,
static_cast<unsigned long>(flags), static_cast<unsigned long>(hr), format ? "yes" : "null");
if (SUCCEEDED(hr) && format != nullptr)
{
if (SUCCEEDED(hr) && format != nullptr) {
std::scoped_lock lock(g_setup_mutex);
g_client_formats[self] = capture_format(format);
}
@@ -600,33 +546,28 @@ HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void**
const bool is_render = (riid == __uuidof(IAudioRenderClient));
logf("hk_GetService: client=%p hr=0x%08lX render_client=%d", self, static_cast<unsigned long>(hr),
is_render ? 1 : 0);
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && riid == __uuidof(IAudioRenderClient))
{
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && riid == __uuidof(IAudioRenderClient)) {
CapturedFormat cf;
bool have = false;
{
std::scoped_lock lock(g_setup_mutex);
auto it = g_client_formats.find(self);
if (it != g_client_formats.end())
{
if (it != g_client_formats.end()) {
cf = it->second;
have = true;
}
}
// Fallback for IAudioClient3::InitializeSharedAudioStream (no Initialize
// format): the shared-mode format is the device mix format.
if (!have)
{
if (!have) {
WAVEFORMATEX* mix = nullptr;
if (SUCCEEDED(self->GetMixFormat(&mix)) && mix != nullptr)
{
if (SUCCEEDED(self->GetMixFormat(&mix)) && mix != nullptr) {
cf = capture_format(mix);
have = true;
CoTaskMemFree(mix);
}
}
if (have)
{
if (have) {
std::scoped_lock lock(g_setup_mutex);
// We saw this client's Initialize (or its shared-mode mix format), so the rate
// is exact, not a guess.
@@ -639,28 +580,25 @@ HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void**
void install_audioclient_hooks(IAudioClient* ac)
{
std::scoped_lock lock(g_setup_mutex);
if (g_audioclient_hooked)
{
if (g_audioclient_hooked) {
return; // shared vtable: hook the first IAudioClient we see, covers all
}
g_vh_initialize.install(ac, kIdx_IAudioClient_Initialize, reinterpret_cast<void*>(&hk_Initialize));
g_vh_getservice.install(ac, kIdx_IAudioClient_GetService, reinterpret_cast<void*>(&hk_GetService));
g_audioclient_hooked = (static_cast<bool>(g_vh_initialize) && static_cast<bool>(g_vh_getservice));
logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac,
static_cast<bool>(g_vh_initialize) ? 1 : 0, static_cast<bool>(g_vh_getservice) ? 1 : 0);
logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac, static_cast<bool>(g_vh_initialize) ? 1 : 0,
static_cast<bool>(g_vh_getservice) ? 1 : 0);
}
HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ctx, PROPVARIANT* params,
void** ppv)
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_vh_activate.original<ActivateFn>()(self, riid, cls_ctx, params, ppv);
const bool is_audioclient = (riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) ||
riid == __uuidof(IAudioClient3));
const bool is_audioclient =
(riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) || riid == __uuidof(IAudioClient3));
logf("hk_Activate: device=%p hr=0x%08lX audioclient=%d", self, static_cast<unsigned long>(hr),
is_audioclient ? 1 : 0);
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && is_audioclient)
{
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && is_audioclient) {
install_audioclient_hooks(static_cast<IAudioClient*>(*ppv));
}
return hr;
@@ -668,62 +606,51 @@ HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ct
} // namespace
namespace
{
namespace {
// Build the probe COM objects (enumerator -> device -> client -> render) and capture the
// device mix format. Created ONCE and kept for the DLL's lifetime: every instance of a
// coclass shares one vtable, so a toggle then only re-swaps vtable slots on these kept
// objects -- no COM create/destroy churn (which races AudioSes). Caller holds g_setup_mutex.
bool build_probe_locked()
{
if (g_self_device != nullptr)
{
if (g_self_device != nullptr) {
return true; // already built
}
IMMDeviceEnumerator* enumerator = nullptr;
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator))))
{
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
reinterpret_cast<void**>(&enumerator)))) {
return false;
}
IMMDevice* device = nullptr;
const HRESULT hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
enumerator->Release(); // only needed to reach the device
if (FAILED(hr) || device == nullptr)
{
if (FAILED(hr) || device == nullptr) {
return false;
}
g_self_device = device; // kept alive (Activate hook re-installs from its vtable)
IAudioRenderClient* self_render = nullptr;
HRESULT ah = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
reinterpret_cast<void**>(&g_self_client));
if (SUCCEEDED(ah) && g_self_client != nullptr)
{
HRESULT ah =
device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&g_self_client));
if (SUCCEEDED(ah) && g_self_client != nullptr) {
WAVEFORMATEX* mix = nullptr;
if (SUCCEEDED(g_self_client->GetMixFormat(&mix)) && mix != nullptr)
{
if (SUCCEEDED(g_self_client->GetMixFormat(&mix)) && mix != nullptr) {
g_mix_format = capture_format(mix);
g_have_mix_format.store(1, std::memory_order_release);
constexpr REFERENCE_TIME kBuf = 10 * 10000; // 10 ms; never started
HRESULT ih = g_self_client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, kBuf, 0, mix, nullptr);
if (SUCCEEDED(ih))
{
ih = g_self_client->GetService(__uuidof(IAudioRenderClient),
reinterpret_cast<void**>(&self_render));
if (SUCCEEDED(ih)) {
ih = g_self_client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&self_render));
}
logf("build_probe: client init=0x%08lX render=%p mix=%uHz/%uch/%ubit tag=%u",
static_cast<unsigned long>(ih), self_render, g_mix_format.rate, g_mix_format.channels,
g_mix_format.bits, g_mix_format.tag);
CoTaskMemFree(mix);
}
}
else
{
} else {
logf("build_probe: Activate(IAudioClient) failed hr=0x%08lX", static_cast<unsigned long>(ah));
}
if (self_render != nullptr)
{
if (self_render != nullptr) {
g_self_render.store(self_render, std::memory_order_release);
}
return true; // device built; render may be null on odd setups (Activate hook still works)
@@ -733,18 +660,14 @@ bool build_probe_locked()
// g_setup_mutex.
void install_detours_locked()
{
if (g_self_device == nullptr)
{
if (g_self_device == nullptr) {
return;
}
g_hook_epoch.fetch_add(1, std::memory_order_release); // new epoch: invalidate any straddling GetBuffer
g_vh_activate.install(g_self_device, kIdx_IMMDevice_Activate, reinterpret_cast<void*>(&hk_Activate));
if (IAudioRenderClient* sr = g_self_render.load(std::memory_order_acquire))
{
g_vh_initialize.install(g_self_client, kIdx_IAudioClient_Initialize,
reinterpret_cast<void*>(&hk_Initialize));
g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService,
reinterpret_cast<void*>(&hk_GetService));
if (IAudioRenderClient* sr = g_self_render.load(std::memory_order_acquire)) {
g_vh_initialize.install(g_self_client, kIdx_IAudioClient_Initialize, reinterpret_cast<void*>(&hk_Initialize));
g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService, reinterpret_cast<void*>(&hk_GetService));
g_vh_getbuffer.install(sr, kIdx_IAudioRenderClient_GetBuffer, reinterpret_cast<void*>(&hk_GetBuffer));
g_vh_releasebuffer.install(sr, kIdx_IAudioRenderClient_ReleaseBuffer,
reinterpret_cast<void*>(&hk_ReleaseBuffer));
@@ -755,10 +678,9 @@ void install_detours_locked()
hook_set_installed(g_id_getservice, static_cast<bool>(g_vh_getservice));
hook_set_installed(g_id_getbuffer, static_cast<bool>(g_vh_getbuffer));
hook_set_installed(g_id_releasebuffer, static_cast<bool>(g_vh_releasebuffer));
logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d",
static_cast<bool>(g_vh_activate) ? 1 : 0, static_cast<bool>(g_vh_initialize) ? 1 : 0,
static_cast<bool>(g_vh_getservice) ? 1 : 0, static_cast<bool>(g_vh_getbuffer) ? 1 : 0,
static_cast<bool>(g_vh_releasebuffer) ? 1 : 0);
logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d", static_cast<bool>(g_vh_activate) ? 1 : 0,
static_cast<bool>(g_vh_initialize) ? 1 : 0, static_cast<bool>(g_vh_getservice) ? 1 : 0,
static_cast<bool>(g_vh_getbuffer) ? 1 : 0, static_cast<bool>(g_vh_releasebuffer) ? 1 : 0);
}
} // namespace
@@ -767,8 +689,7 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
std::scoped_lock lock(g_setup_mutex);
g_ipc.store(&ipc, std::memory_order_release);
g_rings[0].store(ring, std::memory_order_release);
if (g_vh_activate)
{
if (g_vh_activate) {
return true; // detours already installed
}
if (g_id_activate < 0) // register the hook-list ids once
@@ -790,17 +711,14 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
void republish_audio_format()
{
std::scoped_lock lock(g_setup_mutex);
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
{
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
if (ring == nullptr)
{
if (ring == nullptr) {
continue;
}
// Apply any operator command (re-measure / override) the host posted on this ring.
AudioRingOpCmd cmd;
if (audio_ring_poll_op(*ring, g_last_op_seq[i], cmd) != AudioRingOp_None)
{
if (audio_ring_poll_op(*ring, g_last_op_seq[i], cmd) != AudioRingOp_None) {
apply_audio_op_locked(i, cmd);
}
// Publishes an exact format immediately; a guessed rate is measured first and
@@ -811,15 +729,13 @@ void republish_audio_format()
void set_audio_ring(unsigned index, AudioRingHeader* ring)
{
if (index >= kMaxAudioStreams)
{
if (index >= kMaxAudioStreams) {
return;
}
// The worker thread re-attaches every tick (idempotent); only log when the ring
// pointer actually changes so the log isn't flooded with identical lines.
AudioRingHeader* const prev = g_rings[index].exchange(ring, std::memory_order_acq_rel);
if (prev != ring)
{
if (prev != ring) {
logf("set_audio_ring: index=%u ring=%p capture_enabled=%u", index, ring,
ring ? ring->capture_enabled.load(std::memory_order_relaxed) : 0u);
}
@@ -836,7 +752,8 @@ void remove_audio_hooks()
// objects alive -- a re-enable just re-swaps the slots, no COM churn. The probe is only
// released on detach (shutdown_audio_hooks). m_original stays valid (see VtableHook), so
// an in-flight detour on the audio thread completes safely after the restore.
g_hook_epoch.fetch_add(1, std::memory_order_release); // new epoch: a later capture won't trust a pre-toggle GetBuffer
g_hook_epoch.fetch_add(1,
std::memory_order_release); // new epoch: a later capture won't trust a pre-toggle GetBuffer
g_vh_releasebuffer.remove();
g_vh_getbuffer.remove();
g_vh_getservice.remove();
@@ -858,8 +775,7 @@ void remove_audio_hooks()
g_streams_seen.store(0, std::memory_order_relaxed);
g_frames_captured.store(0, std::memory_order_relaxed);
g_frames_silenced.store(0, std::memory_order_relaxed);
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
{
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
g_streams[i].client.store(nullptr, std::memory_order_relaxed);
g_streams[i].frames.store(0, std::memory_order_relaxed);
g_streams[i].block_align.store(0, std::memory_order_relaxed);
@@ -880,17 +796,14 @@ void shutdown_audio_hooks()
remove_audio_hooks(); // restore vtables + clear state (takes the lock)
// Now safe to release the kept probe objects (called only on DLL detach).
std::scoped_lock lock(g_setup_mutex);
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel))
{
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel)) {
sr->Release();
}
if (g_self_client != nullptr)
{
if (g_self_client != nullptr) {
g_self_client->Release();
g_self_client = nullptr;
}
if (g_self_device != nullptr)
{
if (g_self_device != nullptr) {
g_self_device->Release();
g_self_device = nullptr;
}

View File

@@ -13,8 +13,7 @@
#include "coop/audio_ring.hpp"
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Installs the render-path hooks. `ipc` must outlive the hooks (used for the
// stream-count diagnostics in HookStatus). `ring` may be null — counting still

View File

@@ -19,11 +19,9 @@
#include "shared_video_texture.hpp"
#include "vtable_hook.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight Present detours before remove frees the shared D3D state
@@ -65,14 +63,12 @@ thread_local bool t_in_present = false;
bool ensure_device()
{
if (g_device != nullptr)
{
if (g_device != nullptr) {
return true;
}
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, &g_device, nullptr, &g_ctx);
if (FAILED(hr) || g_device == nullptr)
{
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
&g_device, nullptr, &g_ctx);
if (FAILED(hr) || g_device == nullptr) {
logf("d3d9: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
return false;
}
@@ -81,13 +77,11 @@ bool ensure_device()
void release_sysmem()
{
if (g_sysmem != nullptr)
{
if (g_sysmem != nullptr) {
g_sysmem->Release();
g_sysmem = nullptr;
}
if (g_sysmem_dev != nullptr)
{
if (g_sysmem_dev != nullptr) {
g_sysmem_dev->Release();
g_sysmem_dev = nullptr;
}
@@ -99,8 +93,7 @@ void release_sysmem()
void capture_d3d9(IDirect3DDevice9* dev)
{
IDirect3DSurface9* back = nullptr;
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back)) || back == nullptr)
{
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back)) || back == nullptr) {
return;
}
D3DSURFACE_DESC d{};
@@ -108,10 +101,8 @@ void capture_d3d9(IDirect3DDevice9* dev)
const UINT w = d.Width;
const UINT h = d.Height;
// We only handle the standard 32-bit BGRX/BGRA back buffers (the common D3D9 case).
if ((d.Format != D3DFMT_X8R8G8B8 && d.Format != D3DFMT_A8R8G8B8) || w == 0 || h == 0)
{
if (!g_unsupported_logged)
{
if ((d.Format != D3DFMT_X8R8G8B8 && d.Format != D3DFMT_A8R8G8B8) || w == 0 || h == 0) {
if (!g_unsupported_logged) {
logf("d3d9: unsupported backbuffer format=%d (only X8R8G8B8 / A8R8G8B8); idle", static_cast<int>(d.Format));
g_unsupported_logged = true;
}
@@ -120,12 +111,11 @@ void capture_d3d9(IDirect3DDevice9* dev)
}
// (Re)create the system-memory read-back surface on the game's device.
if (!(g_sysmem != nullptr && g_sysmem_dev == dev && g_sysmem_w == w && g_sysmem_h == h && g_sysmem_fmt == d.Format))
{
if (!(g_sysmem != nullptr && g_sysmem_dev == dev && g_sysmem_w == w && g_sysmem_h == h
&& g_sysmem_fmt == d.Format)) {
release_sysmem();
if (SUCCEEDED(dev->CreateOffscreenPlainSurface(w, h, d.Format, D3DPOOL_SYSTEMMEM, &g_sysmem, nullptr)) &&
g_sysmem != nullptr)
{
if (SUCCEEDED(dev->CreateOffscreenPlainSurface(w, h, d.Format, D3DPOOL_SYSTEMMEM, &g_sysmem, nullptr))
&& g_sysmem != nullptr) {
g_sysmem_dev = dev;
dev->AddRef();
g_sysmem_w = w;
@@ -138,21 +128,18 @@ void capture_d3d9(IDirect3DDevice9* dev)
if (g_sysmem != nullptr && SUCCEEDED(dev->GetRenderTargetData(back, g_sysmem))) // GPU->sysmem, blocks
{
D3DLOCKED_RECT lr{};
if (SUCCEEDED(g_sysmem->LockRect(&lr, nullptr, D3DLOCK_READONLY)) && lr.pBits != nullptr)
{
if (SUCCEEDED(g_sysmem->LockRect(&lr, nullptr, D3DLOCK_READONLY)) && lr.pBits != nullptr) {
const size_t dst_row = static_cast<size_t>(w) * 4;
if (g_rgba.size() != dst_row * h)
{
if (g_rgba.size() != dst_row * h) {
g_rgba.resize(dst_row * h);
}
// X8R8G8B8 / A8R8G8B8 store as little-endian 0xAARRGGBB -> bytes B,G,R,A. Swizzle to
// R,G,B,A and force opaque alpha so the host's RGBA decode matches the other backends.
for (UINT y = 0; y < h; ++y)
{
const unsigned char* src = static_cast<const unsigned char*>(lr.pBits) + static_cast<size_t>(y) * lr.Pitch;
for (UINT y = 0; y < h; ++y) {
const unsigned char* src =
static_cast<const unsigned char*>(lr.pBits) + static_cast<size_t>(y) * lr.Pitch;
unsigned char* out = g_rgba.data() + static_cast<size_t>(y) * dst_row;
for (UINT x = 0; x < w; ++x)
{
for (UINT x = 0; x < w; ++x) {
out[x * 4 + 0] = src[x * 4 + 2]; // R
out[x * 4 + 1] = src[x * 4 + 1]; // G
out[x * 4 + 2] = src[x * 4 + 0]; // B
@@ -162,10 +149,8 @@ void capture_d3d9(IDirect3DDevice9* dev)
g_sysmem->UnlockRect();
// DXGI_FORMAT_R8G8B8A8_UNORM: we swizzle the D3D9 BGRA backbuffer to RGBA above.
if (ensure_device() &&
g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "d3d9") &&
g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
if (ensure_device() && g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "d3d9")
&& g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_rgba.data(), static_cast<UINT>(dst_row), 0);
g_ctx->Flush();
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
@@ -174,11 +159,9 @@ void capture_d3d9(IDirect3DDevice9* dev)
}
}
if (shared)
{
if (shared) {
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
}
}
@@ -191,12 +174,10 @@ HRESULT STDMETHODCALLTYPE hk_Present9(IDirect3DDevice9* dev, const RECT* src, co
DetourGate::Guard guard(g_gate); // keep the shared D3D state alive for this whole detour
hook_note_call(g_id_present9);
g_presents.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_present();
}
if (!t_in_present)
{
if (!t_in_present) {
t_in_present = true;
capture_d3d9(dev);
t_in_present = false;
@@ -213,19 +194,16 @@ HRESULT STDMETHODCALLTYPE hk_Present9(IDirect3DDevice9* dev, const RECT* src, co
void* grab_present9_address()
{
HMODULE d3d9 = GetModuleHandleW(L"d3d9.dll");
if (d3d9 == nullptr)
{
if (d3d9 == nullptr) {
return nullptr; // not a D3D9 game
}
using PFN_Direct3DCreate9 = IDirect3D9*(WINAPI*)(UINT);
auto create = reinterpret_cast<PFN_Direct3DCreate9>(GetProcAddress(d3d9, "Direct3DCreate9"));
if (create == nullptr)
{
if (create == nullptr) {
return nullptr;
}
IDirect3D9* d3d = create(D3D_SDK_VERSION);
if (d3d == nullptr)
{
if (d3d == nullptr) {
return nullptr;
}
@@ -239,8 +217,7 @@ void* grab_present9_address()
wc.hInstance, nullptr);
void* present = nullptr;
if (hwnd != nullptr)
{
if (hwnd != nullptr) {
D3DPRESENT_PARAMETERS pp{};
pp.BackBufferWidth = 8;
pp.BackBufferHeight = 8;
@@ -251,9 +228,8 @@ void* grab_present9_address()
pp.Windowed = TRUE;
IDirect3DDevice9* dev = nullptr;
if (SUCCEEDED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &dev)) &&
dev != nullptr)
{
D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &dev))
&& dev != nullptr) {
present = vtable_method(dev, kIdx_IDirect3DDevice9_Present);
dev->Release();
}
@@ -270,16 +246,14 @@ bool install_d3d9_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_present9.enabled())
{
if (g_hk_present9.enabled()) {
return true; // already installed (persistent hook; re-install below re-enables it)
}
g_id_present9 = hook_register("IDirect3DDevice9::Present", HookSubsys_Video);
g_unsupported_logged = false;
void* present = grab_present9_address();
if (present == nullptr)
{
if (present == nullptr) {
hook_set_installed(g_id_present9, false); // not a D3D9 game (or no probe device)
return false;
}
@@ -301,13 +275,11 @@ void remove_d3d9_hooks()
g_gate.drain();
g_shared.release();
release_sysmem();
if (g_ctx != nullptr)
{
if (g_ctx != nullptr) {
g_ctx->Release();
g_ctx = nullptr;
}
if (g_device != nullptr)
{
if (g_device != nullptr) {
g_device->Release();
g_device = nullptr;
}

View File

@@ -12,8 +12,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Installs the D3D9 Present hook. `ipc` must outlive the hook. Returns true if Present was
// hooked (i.e. d3d9.dll is present and a probe device came up). Safe to call repeatedly.

View File

@@ -10,11 +10,9 @@
#include "coop/log_ring.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
std::mutex g_log_mutex;
FILE* g_log_file = nullptr;
@@ -29,14 +27,12 @@ std::atomic<coop::LogRing*> g_log_ring{nullptr};
bool logging_enabled()
{
wchar_t buf[8] = {};
if (GetEnvironmentVariableW(L"COOP_HOOK_LOG", buf, 8) > 0)
{
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)
{
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;
}
@@ -45,15 +41,12 @@ bool logging_enabled()
FILE* log_file_locked()
{
if (!g_log_tried)
{
if (!g_log_tried) {
g_log_tried = true;
if (logging_enabled())
{
if (logging_enabled()) {
wchar_t dir[MAX_PATH] = {};
const DWORD n = GetTempPathW(MAX_PATH, dir);
if (n != 0 && n < MAX_PATH)
{
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");
}
@@ -69,12 +62,10 @@ void set_log_ring(coop::LogRing* ring)
g_log_ring.store(ring, std::memory_order_release);
}
namespace
{
namespace {
const char* level_tag(std::uint32_t level)
{
switch (level)
{
switch (level) {
case coop::LogLevel_Warn:
return "WARN ";
case coop::LogLevel_Error:
@@ -91,20 +82,18 @@ void vlog(std::uint32_t level, const char* fmt, va_list args)
std::vsnprintf(line, sizeof(line), fmt, args);
// Stream to the host's Log window over the shared ring (the primary sink).
if (coop::LogRing* ring = g_log_ring.load(std::memory_order_acquire))
{
if (coop::LogRing* ring = g_log_ring.load(std::memory_order_acquire)) {
coop::log_ring_push(*ring, GetCurrentProcessId(), level, GetTickCount64(), line);
}
// Also mirror to the file when the opt-in trace is enabled.
std::scoped_lock lock(g_log_mutex);
FILE* f = log_file_locked();
if (f != nullptr)
{
if (f != nullptr) {
SYSTEMTIME st;
GetLocalTime(&st);
std::fprintf(f, "[%02u:%02u:%02u.%03u pid=%lu %s] %s\n", st.wHour, st.wMinute, st.wSecond,
st.wMilliseconds, GetCurrentProcessId(), level_tag(level), line);
std::fprintf(f, "[%02u:%02u:%02u.%03u pid=%lu %s] %s\n", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
GetCurrentProcessId(), level_tag(level), line);
std::fflush(f);
}
}

View File

@@ -4,13 +4,11 @@
// see debug_log.cpp). Thread-safe; cheap enough to leave compiled in.
#pragma once
namespace coop
{
namespace coop {
struct LogRing;
}
namespace coop::hook
{
namespace coop::hook {
// Append a printf-style line to the log ring (if attached) and the file (if on).
// logf = info, logw = warning, loge = error; the host colours the Log window by level.

View File

@@ -27,8 +27,7 @@
#include "vk_hook.hpp"
#include "xinput_hook.hpp"
namespace
{
namespace {
coop::hook::IpcClient g_ipc;
std::atomic<bool> g_running{true};
@@ -40,8 +39,7 @@ 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))
{
if (!g_ipc.connect(/*attempts=*/200, /*delay_ms=*/25)) {
coop::hook::logf("worker_thread: IPC connect FAILED (no host mapping); exiting");
return 0;
}
@@ -49,15 +47,11 @@ DWORD WINAPI worker_thread(LPVOID)
// window. The host creates it at injection time; it's normally already there.
{
const std::wstring log_name = coop::log_ring_name(GetCurrentProcessId());
if (g_log_shm.open(log_name, coop::log_ring_total_size(coop::kLogCapacity)))
{
if (g_log_shm.open(log_name, coop::log_ring_total_size(coop::kLogCapacity))) {
auto* lr = g_log_shm.as<coop::LogRing>();
if (coop::log_ring_valid(*lr))
{
if (coop::log_ring_valid(*lr)) {
coop::hook::set_log_ring(lr);
}
else
{
} else {
g_log_shm.reset();
}
}
@@ -81,28 +75,21 @@ DWORD WINAPI worker_thread(LPVOID)
// what's wanted but missing (modules / the game window may appear lazily) and
// remove what's no longer wanted (the host toggled it off). Beat a heartbeat so
// the host can see the hook is alive.
while (g_running.load(std::memory_order_relaxed))
{
while (g_running.load(std::memory_order_relaxed)) {
// --- Input (XInput) ---
const bool want_input = g_ipc.subsystem_install_requested(coop::HookSubsys_Input);
if (want_input && !xinput_installed)
{
if (want_input && !xinput_installed) {
xinput_installed = coop::hook::install_xinput_hooks(g_ipc);
}
else if (!want_input && xinput_installed)
{
} else if (!want_input && xinput_installed) {
coop::hook::remove_xinput_hooks();
xinput_installed = false;
}
// --- Focus spoof ---
const bool want_focus = g_ipc.subsystem_install_requested(coop::HookSubsys_Focus);
if (want_focus && !focus_installed)
{
if (want_focus && !focus_installed) {
focus_installed = coop::hook::install_focus_spoof(g_ipc);
}
else if (!want_focus && focus_installed)
{
} else if (!want_focus && focus_installed) {
coop::hook::remove_focus_spoof();
focus_installed = false;
}
@@ -111,17 +98,13 @@ DWORD WINAPI worker_thread(LPVOID)
// Install even before the host's ring exists so render streams are counted
// regardless; attach the ring (enabling capture+silence) once it appears.
const bool want_audio = com_ok && g_ipc.subsystem_install_requested(coop::HookSubsys_Audio);
if (want_audio && !audio_installed)
{
if (want_audio && !audio_installed) {
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
if (audio_installed)
{
if (audio_installed) {
coop::hook::logf("worker_thread: audio hooks installed");
audio_ring_open = false; // re-attach the ring below after a reinstall
}
}
else if (!want_audio && audio_installed)
{
} else if (!want_audio && audio_installed) {
coop::hook::remove_audio_hooks();
audio_installed = false;
audio_ring_open = false;
@@ -133,31 +116,25 @@ DWORD WINAPI worker_thread(LPVOID)
// Install both producers: DXGI games hit the Present hook, OpenGL games hit
// the SwapBuffers hook, whichever the game uses fills the shared texture.
const bool want_video = g_ipc.subsystem_install_requested(coop::HookSubsys_Video);
if (want_video && !video_installed)
{
if (want_video && !video_installed) {
const bool present_ok = coop::hook::install_present_hooks(g_ipc);
const bool gl_ok = coop::hook::install_opengl_hooks(g_ipc);
const bool d3d9_ok = coop::hook::install_d3d9_hooks(g_ipc);
video_installed = present_ok || gl_ok || d3d9_ok;
if (video_installed)
{
if (video_installed) {
coop::hook::logf("worker_thread: video hooks installed (present=%d opengl=%d d3d9=%d)",
present_ok ? 1 : 0, gl_ok ? 1 : 0, d3d9_ok ? 1 : 0);
}
}
// Vulkan separately: vulkan-1.dll loads lazily (volk dlopens it after start), so the DXGI/
// GL/D3D9 hooks above may install before it exists. Keep trying each tick until it appears.
if (want_video && !vk_installed)
{
if (want_video && !vk_installed) {
vk_installed = coop::hook::install_vk_hooks(g_ipc);
if (vk_installed)
{
if (vk_installed) {
video_installed = true; // a Vulkan-only game otherwise has no video hook installed
coop::hook::logf("worker_thread: vulkan video hook installed");
}
}
else if (!want_video && video_installed)
{
} else if (!want_video && video_installed) {
coop::hook::remove_present_hooks();
coop::hook::remove_opengl_hooks();
coop::hook::remove_d3d9_hooks();
@@ -171,16 +148,12 @@ DWORD WINAPI worker_thread(LPVOID)
// Opt-in. When on, the host streams MKB events into the shared ring; we post
// them to the game and synthesize polling state. Drained at high rate below.
const bool want_mkb = g_ipc.subsystem_install_requested(coop::HookSubsys_Mkb);
if (want_mkb && !mkb_installed)
{
if (want_mkb && !mkb_installed) {
mkb_installed = coop::hook::install_mkb_hooks(g_ipc);
if (mkb_installed)
{
if (mkb_installed) {
coop::hook::logf("worker_thread: MKB hooks installed");
}
}
else if (!want_mkb && mkb_installed)
{
} else if (!want_mkb && mkb_installed) {
coop::hook::remove_mkb_hooks();
mkb_installed = false;
coop::hook::logf("worker_thread: MKB hooks removed (host request)");
@@ -189,29 +162,21 @@ DWORD WINAPI worker_thread(LPVOID)
// Attach a ring per stream. The host creates up to kMaxAudioStreams rings
// (coop_audio_<pid>[_<index>]); we open each as it appears and (re)attach it so
// every stream is captured + silenced into its own ring for the host to mix.
if (audio_installed)
{
for (unsigned i = 0; i < coop::kMaxAudioStreams; ++i)
{
if (!g_audio_shm[i].valid())
{
if (audio_installed) {
for (unsigned i = 0; i < coop::kMaxAudioStreams; ++i) {
if (!g_audio_shm[i].valid()) {
g_audio_shm[i].open(coop::audio_ring_name(GetCurrentProcessId(), i),
coop::audio_ring_total_size(coop::kAudioRingCapacity));
}
if (g_audio_shm[i].valid())
{
if (g_audio_shm[i].valid()) {
auto* ring = g_audio_shm[i].as<coop::AudioRingHeader>();
if (coop::audio_ring_valid(*ring))
{
if (coop::audio_ring_valid(*ring)) {
coop::hook::set_audio_ring(i, ring); // idempotent re-attach
if (i == 0 && !audio_ring_open)
{
if (i == 0 && !audio_ring_open) {
audio_ring_open = true;
coop::hook::logf("worker_thread: audio ring 0 opened");
}
}
else
{
} else {
g_audio_shm[i].reset(); // present but not our contract; retry
}
}
@@ -220,8 +185,7 @@ DWORD WINAPI worker_thread(LPVOID)
// A stream is often registered before its ring is attached (or the host re-inits
// a ring on a mirror re-toggle, clearing its format); keep formats published so
// the host consumes the rings instead of falling back to loopback.
if (audio_ring_open)
{
if (audio_ring_open) {
coop::hook::republish_audio_format();
}
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
@@ -232,18 +196,15 @@ DWORD WINAPI worker_thread(LPVOID)
// Reconcile ~4x/s (50 slices x 5 ms), but drain MKB events every slice --
// input must stay responsive at a far higher rate than the reconcile.
for (int slice = 0; slice < 50 && g_running.load(std::memory_order_relaxed); ++slice)
{
if (mkb_installed)
{
for (int slice = 0; slice < 50 && g_running.load(std::memory_order_relaxed); ++slice) {
if (mkb_installed) {
coop::hook::mkb_pump(g_ipc);
}
Sleep(5);
}
}
if (com_ok)
{
if (com_ok) {
CoUninitialize();
}
return 0;
@@ -253,20 +214,17 @@ DWORD WINAPI worker_thread(LPVOID)
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
{
switch (reason)
{
switch (reason) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(module);
if (HANDLE thread = CreateThread(nullptr, 0, &worker_thread, nullptr, 0, nullptr))
{
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)
{
if (reserved == nullptr) {
g_running.store(false, std::memory_order_relaxed);
coop::hook::set_log_ring(nullptr);
coop::hook::remove_focus_spoof();

View File

@@ -5,13 +5,11 @@
#include <windows.h>
namespace coop::hook
{
namespace coop::hook {
inline HWND find_main_window(DWORD pid)
{
struct Ctx
{
struct Ctx {
DWORD pid;
HWND best;
long best_area;
@@ -22,18 +20,15 @@ inline HWND find_main_window(DWORD pid)
auto* c = reinterpret_cast<Ctx*>(lparam);
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
if (pid != c->pid || !IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr)
{
if (pid != c->pid || !IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr) {
return TRUE; // not ours, hidden, or an owned dialog -- keep looking
}
RECT rect = {};
if (!GetWindowRect(hwnd, &rect))
{
if (!GetWindowRect(hwnd, &rect)) {
return TRUE;
}
const long area = (rect.right - rect.left) * (rect.bottom - rect.top);
if (area > c->best_area)
{
if (area > c->best_area) {
c->best_area = area;
c->best = hwnd;
}

View File

@@ -11,11 +11,9 @@
#include "hook_install.hpp"
#include "hook_registry.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight focus / WNDPROC detours before remove nulls their state
@@ -40,11 +38,9 @@ safetyhook::InlineHook g_hk_setcursorpos;
LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
DetourGate::Guard guard(g_gate); // keep g_orig_proc / g_unicode valid for this whole dispatch
switch (msg)
{
switch (msg) {
case WM_ACTIVATE:
if (LOWORD(wparam) == WA_INACTIVE)
{
if (LOWORD(wparam) == WA_INACTIVE) {
wparam = MAKEWPARAM(WA_ACTIVE, HIWORD(wparam));
hook_note_call(g_id_wndproc);
}
@@ -66,8 +62,7 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
// Read g_orig_proc once; if the subclass is live but the original isn't published yet (the tiny
// install/remove window), fall back to DefWindowProc rather than call through a null pointer.
const WNDPROC orig = g_orig_proc;
if (orig == nullptr)
{
if (orig == nullptr) {
return g_unicode ? DefWindowProcW(hwnd, msg, wparam, lparam) : DefWindowProcA(hwnd, msg, wparam, lparam);
}
return g_unicode ? CallWindowProcW(orig, hwnd, msg, wparam, lparam)
@@ -78,8 +73,7 @@ HWND WINAPI hk_GetForegroundWindow()
{
DetourGate::Guard guard(g_gate);
hook_note_call(g_id_foreground);
if (g_focus_ipc != nullptr)
{
if (g_focus_ipc != nullptr) {
g_focus_ipc->note_focus_query(FocusApi_Foreground);
}
return g_game_hwnd;
@@ -89,8 +83,7 @@ HWND WINAPI hk_GetActiveWindow()
{
DetourGate::Guard guard(g_gate);
hook_note_call(g_id_active);
if (g_focus_ipc != nullptr)
{
if (g_focus_ipc != nullptr) {
g_focus_ipc->note_focus_query(FocusApi_Active);
}
return g_game_hwnd;
@@ -100,8 +93,7 @@ HWND WINAPI hk_GetFocus()
{
DetourGate::Guard guard(g_gate);
hook_note_call(g_id_focus);
if (g_focus_ipc != nullptr)
{
if (g_focus_ipc != nullptr) {
g_focus_ipc->note_focus_query(FocusApi_Focus);
}
return g_game_hwnd;
@@ -124,8 +116,7 @@ BOOL WINAPI hk_SetCursorPos(int x, int y)
DetourGate::Guard guard(g_gate);
hook_note_call(g_id_setcursorpos);
const bool allow = g_focus_ipc != nullptr && g_focus_ipc->cursor_clip_allowed();
if (!allow)
{
if (!allow) {
return TRUE;
}
return g_hk_setcursorpos.stdcall<BOOL>(x, y);
@@ -133,8 +124,7 @@ BOOL WINAPI hk_SetCursorPos(int x, int y)
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
{
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
{
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name))) {
g_focus_hooks.emplace_back();
install_inline(g_focus_hooks.back(), target, detour); // assign-then-enable (no install race)
hook_set_installed(registry_id, true);
@@ -146,8 +136,7 @@ void hook_export(HMODULE module, const char* name, void* detour, int registry_id
bool install_focus_spoof(IpcClient& ipc)
{
g_focus_ipc = &ipc;
if (g_game_hwnd != nullptr)
{
if (g_game_hwnd != nullptr) {
return true; // already active
}
@@ -159,8 +148,7 @@ bool install_focus_spoof(IpcClient& ipc)
g_id_setcursorpos = hook_register("SetCursorPos (cursor release)", HookSubsys_Focus);
HWND hwnd = find_main_window(GetCurrentProcessId());
if (hwnd == nullptr)
{
if (hwnd == nullptr) {
return false; // window not created yet; caller retries
}
@@ -173,38 +161,30 @@ bool install_focus_spoof(IpcClient& ipc)
// thread is safe (the new proc runs on the window's own thread); match A/W for CallWindowProc.
g_orig_proc = g_unicode ? reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC))
: reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hwnd, GWLP_WNDPROC));
if (g_unicode)
{
if (g_unicode) {
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
}
else
{
} else {
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
}
hook_set_installed(g_id_wndproc, true);
if (HMODULE user32 = GetModuleHandleW(L"user32.dll"))
{
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow),
g_id_foreground);
if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) {
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow), g_id_foreground);
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow), g_id_active);
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus), g_id_focus);
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor")))
{
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor"))) {
install_inline(g_hk_clipcursor, clip, &hk_ClipCursor);
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
}
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")))
{
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos"))) {
install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos);
hook_set_installed(g_id_setcursorpos, static_cast<bool>(g_hk_setcursorpos));
}
}
// Free any clip the game already set, so release takes effect immediately.
if (!ipc.cursor_clip_allowed())
{
if (!ipc.cursor_clip_allowed()) {
ClipCursor(nullptr);
}
@@ -221,20 +201,16 @@ void update_input_diagnostics(IpcClient& ipc)
bool raw_gamepad_sink = false;
UINT count = 0;
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0)
{
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0) {
std::vector<RAWINPUTDEVICE> devices(count);
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
if (got != static_cast<UINT>(-1))
{
if (got != static_cast<UINT>(-1)) {
raw_registered = got > 0;
for (UINT i = 0; i < got; ++i)
{
for (UINT i = 0; i < got; ++i) {
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
const bool is_pad =
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
if (is_pad)
{
if (is_pad) {
raw_gamepad = true;
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
}
@@ -248,22 +224,17 @@ void update_input_diagnostics(IpcClient& ipc)
void release_cursor_tick()
{
if (g_focus_ipc != nullptr && g_game_hwnd != nullptr && !g_focus_ipc->cursor_clip_allowed())
{
if (g_focus_ipc != nullptr && g_game_hwnd != nullptr && !g_focus_ipc->cursor_clip_allowed()) {
ClipCursor(nullptr); // routes through hk_ClipCursor -> frees the cursor
}
}
void remove_focus_spoof()
{
if (g_game_hwnd != nullptr && g_orig_proc != nullptr)
{
if (g_unicode)
{
if (g_game_hwnd != nullptr && g_orig_proc != nullptr) {
if (g_unicode) {
SetWindowLongPtrW(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
}
else
{
} else {
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
}
}
@@ -275,13 +246,12 @@ void remove_focus_spoof()
// patched bytes. The reverse of the enable order (GFW first) keeps the invariant "GetActiveWindow
// hooked => GetForegroundWindow hooked" across the whole install/remove cycle, so a call never
// lands in a half-patched shared region.
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it)
{
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it) {
disable_for_removal(*it);
}
disable_for_removal(g_hk_clipcursor);
disable_for_removal(g_hk_setcursorpos);
ClipCursor(nullptr); // leave the cursor free when the spoof is removed
ClipCursor(nullptr); // leave the cursor free when the spoof is removed
hook_set_installed(g_id_foreground, false);
hook_set_installed(g_id_active, false);
hook_set_installed(g_id_focus, false);
@@ -298,8 +268,7 @@ void remove_focus_spoof()
// DO call the trampoline, so keep them ALIVE (disabled) -- persistent, re-enabled on re-install
// (see hook_install.hpp) -- so a stale detour never hits a freed trampoline.
g_focus_hooks.clear();
if (g_focus_ipc != nullptr)
{
if (g_focus_ipc != nullptr) {
g_focus_ipc->mark_focus_spoof(false, 0);
}
g_game_hwnd = nullptr;

View File

@@ -6,8 +6,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Finds the game's main window, subclasses it to suppress deactivation messages,
// and hooks the focus-query APIs to always report the game as active. Returns

View File

@@ -26,28 +26,19 @@
#include <windows.h>
namespace coop::hook
{
namespace coop::hook {
class DetourGate
{
public:
class DetourGate {
public:
// RAII: marks a detour body as in-flight for as long as it's on the stack.
class Guard
{
public:
explicit Guard(DetourGate& gate) : m_gate(gate)
{
m_gate.m_active.fetch_add(1, std::memory_order_acq_rel);
}
~Guard()
{
m_gate.m_active.fetch_sub(1, std::memory_order_acq_rel);
}
class Guard {
public:
explicit Guard(DetourGate& gate) : m_gate(gate) { m_gate.m_active.fetch_add(1, std::memory_order_acq_rel); }
~Guard() { m_gate.m_active.fetch_sub(1, std::memory_order_acq_rel); }
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
private:
private:
DetourGate& m_gate;
};
@@ -64,22 +55,17 @@ public:
// reliably, so checking before the first sleep is not safe.
void drain()
{
for (int spins = 0; spins < 400; ++spins)
{
for (int spins = 0; spins < 400; ++spins) {
Sleep(1);
if (m_active.load(std::memory_order_acquire) == 0)
{
if (m_active.load(std::memory_order_acquire) == 0) {
return;
}
}
}
int active() const
{
return m_active.load(std::memory_order_acquire);
}
int active() const { return m_active.load(std::memory_order_acquire); }
private:
private:
std::atomic<int> m_active{0};
};
@@ -92,8 +78,7 @@ private:
template <class InlineHook>
void disable_for_removal(InlineHook& hook)
{
if (!hook.disable())
{
if (!hook.disable()) {
OutputDebugStringA("coop: SafetyHook InlineHook::disable() failed during removal -- unhook may be unsafe\n");
}
}

View File

@@ -23,8 +23,7 @@
#include <windows.h>
namespace coop::hook
{
namespace coop::hook {
// Arm `detour` over `target` in `dst`: create it once (StartDisabled) if empty, then enable. Calling
// this again after a remove just re-enables the SAME hook (no recreate -> the trampoline is never
@@ -36,8 +35,7 @@ inline void install_inline(safetyhook::InlineHook& dst, void* target, void* deto
{
dst = safetyhook::create_inline(target, detour, safetyhook::InlineHook::StartDisabled);
}
if (dst && !dst.enable())
{
if (dst && !dst.enable()) {
OutputDebugStringA("coop: SafetyHook InlineHook::enable() failed during install\n");
}
}

View File

@@ -4,14 +4,11 @@
#include <cstring>
#include <mutex>
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
struct Slot
{
struct Slot {
char name[40] = {};
std::atomic<std::uint32_t> subsystem{0};
std::atomic<std::uint32_t> installed{0};
@@ -29,15 +26,12 @@ 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)
{
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)
{
if (count >= kMaxHookEntries) {
return -1; // table full
}
Slot& s = g_slots[count];
@@ -53,16 +47,14 @@ int hook_register(const char* name, std::uint32_t subsystem)
void hook_set_installed(int id, bool installed)
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
{
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))
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries)) {
g_slots[id].calls.fetch_add(1, std::memory_order_relaxed);
}
}
@@ -72,10 +64,8 @@ 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))
{
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];
@@ -91,8 +81,7 @@ void hook_publish(IpcClient& ipc)
void hook_registry_reset()
{
std::scoped_lock lock(g_register_mutex);
for (auto& s : g_slots)
{
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);

View File

@@ -9,8 +9,7 @@
#include "coop/protocol.hpp"
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Find-or-create a registry slot for `name` in `subsystem`; returns a stable id
// (>= 0) used with the calls below, or -1 if the table is full. Idempotent: the

View File

@@ -11,24 +11,19 @@
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
namespace coop::hook
{
namespace coop::hook {
class IpcClient
{
public:
class IpcClient {
public:
// Tries to open the section a few times: the host may inject us slightly
// before (or after) it creates the mapping. Returns true once connected.
bool connect(int attempts, int delay_ms)
{
const std::wstring name = shared_memory_name(GetCurrentProcessId());
for (int i = 0; i < attempts; ++i)
{
if (shm_.open(name, sizeof(SharedBlock)))
{
for (int i = 0; i < attempts; ++i) {
if (shm_.open(name, sizeof(SharedBlock))) {
auto* block = shm_.as<SharedBlock>();
if (block->magic == kProtocolMagic && block->version == kProtocolVersion)
{
if (block->magic == kProtocolMagic && block->version == kProtocolVersion) {
block_ = block;
return true;
}
@@ -39,17 +34,13 @@ public:
return false;
}
[[nodiscard]] bool connected() const
{
return block_ != nullptr;
}
[[nodiscard]] bool connected() const { return block_ != nullptr; }
// Host-requested install state for a subsystem (default = install, since the
// mapping is zero-filled and 0 means "disabled flag clear" = install).
[[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const
{
if (block_ == nullptr || subsystem >= HookSubsys_Count)
{
if (block_ == nullptr || subsystem >= HookSubsys_Count) {
return true;
}
return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0;
@@ -66,8 +57,7 @@ public:
// was mid-write for the whole spin window (caller should reuse its cache).
bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const
{
if (block_ == nullptr)
{
if (block_ == nullptr) {
return false;
}
return read_pads(*block_, out, count);
@@ -78,32 +68,28 @@ public:
// Record that the game queried a controller slot via XInputGetState/Ex.
void note_state_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
if (block_ != nullptr && user_index < kMaxPads) {
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_caps_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
if (block_ != nullptr && user_index < kMaxPads) {
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_focus_query(FocusApi which)
{
if (block_ != nullptr && which < FocusApi_Count)
{
if (block_ != nullptr && which < FocusApi_Count) {
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
}
}
void mark_attached()
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.game_pid = GetCurrentProcessId();
block_->status.attached = 1;
}
@@ -113,16 +99,14 @@ public:
// the Controllers panel stops showing stale poll rates.
void mark_detached()
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.attached = 0;
}
}
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.focus_spoof = active ? 1u : 0u;
block_->status.game_hwnd = game_hwnd;
}
@@ -130,8 +114,7 @@ public:
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
@@ -141,16 +124,14 @@ public:
void heartbeat()
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
}
}
void set_vk_too_late(bool too_late)
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.vk_too_late = too_late ? 1u : 0u;
}
}
@@ -159,8 +140,7 @@ public:
// the guest's controller). Plain stores; the hook is the sole writer.
void note_rumble(std::uint32_t slot, std::uint16_t left, std::uint16_t right)
{
if (block_ != nullptr && slot < kMaxPads)
{
if (block_ != nullptr && slot < kMaxPads) {
block_->status.rumble_left[slot] = left;
block_->status.rumble_right[slot] = right;
}
@@ -169,8 +149,7 @@ public:
// Record the state the hook just returned to the game for a slot (round-trip view).
void note_read_state(std::uint32_t slot, const CoopPadState& state)
{
if (block_ != nullptr && slot < kMaxPads)
{
if (block_ != nullptr && slot < kMaxPads) {
block_->status.read_state[slot] = state;
}
}
@@ -180,8 +159,7 @@ public:
// Total distinct render streams the audio hook has observed.
void set_audio_streams_seen(std::uint32_t count)
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->status.audio_streams_seen = count;
}
}
@@ -189,8 +167,7 @@ public:
// Publish a tracked stream's format/role into its debug slot.
void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
if (block_ != nullptr && slot < kMaxAudioStreams) {
block_->status.audio_streams[slot] = info;
}
}
@@ -198,12 +175,12 @@ public:
// Update a tracked stream's cumulative frame count (host derives live/idle).
void note_audio_frames(std::uint32_t slot, std::uint64_t frames)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
if (block_ != nullptr && slot < kMaxAudioStreams) {
// atomic_ref so the host's cross-process read isn't torn (notably an x86 DLL -> x64 host,
// where a plain 64-bit store is two halves). The field stays plain POD so AudioStreamInfo
// remains trivially copyable for the wholesale publishes elsewhere.
std::atomic_ref(block_->status.audio_streams[slot].frames_rendered).store(frames, std::memory_order_relaxed);
std::atomic_ref(block_->status.audio_streams[slot].frames_rendered)
.store(frames, std::memory_order_relaxed);
}
}
@@ -212,8 +189,7 @@ public:
// Record that the game's Present() ran (diagnostic counter, hook is sole writer).
void note_present()
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
std::atomic_ref(block_->video.present_calls).fetch_add(1, std::memory_order_relaxed);
}
}
@@ -222,8 +198,7 @@ public:
// keyed mutex was held by the host (we skip rather than block the game's render thread).
void note_video_dropped()
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
std::atomic_ref(block_->video.frames_dropped).fetch_add(1, std::memory_order_relaxed);
}
}
@@ -233,8 +208,7 @@ public:
// polls. The texture itself is shared out-of-band by name, not through here.
void publish_video_frame(std::uint32_t width, std::uint32_t height, std::uint32_t format)
{
if (block_ != nullptr)
{
if (block_ != nullptr) {
block_->video.width = width;
block_->video.height = height;
block_->video.format = format;
@@ -249,32 +223,26 @@ public:
// The host's MKB event queue (nullptr if not connected). The MKB subsystem
// drains it; the host is the sole producer.
[[nodiscard]] MkbRing* mkb_ring()
{
return block_ != nullptr ? &block_->mkb : nullptr;
}
[[nodiscard]] MkbRing* mkb_ring() { return block_ != nullptr ? &block_->mkb : nullptr; }
// --- Hook registry -----------------------------------------------------
// Publish the installed-hooks table (name / subsystem / installed / calls).
void publish_hook_entries(const HookEntry* entries, std::uint32_t count)
{
if (block_ == nullptr)
{
if (block_ == nullptr) {
return;
}
if (count > kMaxHookEntries)
{
if (count > kMaxHookEntries) {
count = kMaxHookEntries;
}
for (std::uint32_t i = 0; i < count; ++i)
{
for (std::uint32_t i = 0; i < count; ++i) {
block_->status.hook_entries[i] = entries[i];
}
block_->status.hook_entry_count = count;
}
private:
private:
SharedMemory shm_;
SharedBlock* block_ = nullptr;
};

View File

@@ -15,22 +15,20 @@
#include "hook_registry.hpp"
#include "vtable_hook.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight polling detours before remove tears the hooks down
// Synthesized input state the polling hooks report. Written by the worker thread
// (mkb_pump), read by the game's thread inside the detours -> all atomic.
std::atomic<bool> g_active{false};
std::atomic<bool> g_key_down[256]; // by Win32 virtual-key (incl. VK_LBUTTON etc.)
std::atomic<long> g_cursor_x{0}; // last forwarded mouse position (game client px)
std::atomic<bool> g_key_down[256]; // by Win32 virtual-key (incl. VK_LBUTTON etc.)
std::atomic<long> g_cursor_x{0}; // last forwarded mouse position (game client px)
std::atomic<long> g_cursor_y{0};
std::atomic<bool> g_have_cursor{false}; // a mouse event has been forwarded at least once
std::atomic<void*> g_target{nullptr}; // game main window (HWND), resolved lazily
std::atomic<bool> g_have_cursor{false}; // a mouse event has been forwarded at least once
std::atomic<void*> g_target{nullptr}; // game main window (HWND), resolved lazily
safetyhook::InlineHook g_hk_async;
safetyhook::InlineHook g_hk_kbstate;
@@ -82,9 +80,8 @@ SHORT WINAPI hk_GetAsyncKeyState(int vkey)
{
DetourGate::Guard guard(g_gate);
const SHORT orig = g_hk_async.stdcall<SHORT>(vkey);
if (g_active.load(std::memory_order_relaxed) && vkey >= 0 && vkey < 256 &&
g_key_down[vkey].load(std::memory_order_relaxed))
{
if (g_active.load(std::memory_order_relaxed) && vkey >= 0 && vkey < 256
&& g_key_down[vkey].load(std::memory_order_relaxed)) {
return static_cast<SHORT>(0x8000) | (orig & 0x1);
}
return orig;
@@ -94,12 +91,9 @@ BOOL WINAPI hk_GetKeyboardState(PBYTE state)
{
DetourGate::Guard guard(g_gate);
const BOOL r = g_hk_kbstate.stdcall<BOOL>(state);
if (r && state != nullptr && g_active.load(std::memory_order_relaxed))
{
for (int vk = 0; vk < 256; ++vk)
{
if (g_key_down[vk].load(std::memory_order_relaxed))
{
if (r && state != nullptr && g_active.load(std::memory_order_relaxed)) {
for (int vk = 0; vk < 256; ++vk) {
if (g_key_down[vk].load(std::memory_order_relaxed)) {
state[vk] |= 0x80;
}
}
@@ -111,11 +105,9 @@ BOOL WINAPI hk_GetCursorPos(LPPOINT pt)
{
DetourGate::Guard guard(g_gate);
const BOOL r = g_hk_cursor.stdcall<BOOL>(pt);
if (g_active.load(std::memory_order_relaxed) && g_have_cursor.load(std::memory_order_relaxed) && pt != nullptr)
{
if (g_active.load(std::memory_order_relaxed) && g_have_cursor.load(std::memory_order_relaxed) && pt != nullptr) {
auto* hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
if (hwnd != nullptr)
{
if (hwnd != nullptr) {
POINT c{g_cursor_x.load(std::memory_order_relaxed), g_cursor_y.load(std::memory_order_relaxed)};
ClientToScreen(hwnd, &c); // synth state is game-client; GetCursorPos is screen-space
*pt = c;
@@ -140,31 +132,25 @@ HRESULT STDMETHODCALLTYPE hk_DI_GetDeviceState(IDirectInputDevice8W* self, DWORD
{
DetourGate::Guard guard(g_gate);
const HRESULT hr = g_vh_di_getstate.original<DI_GetDeviceStateFn>()(self, cb, data);
if (FAILED(hr) || data == nullptr || !g_active.load(std::memory_order_relaxed))
{
if (FAILED(hr) || data == nullptr || !g_active.load(std::memory_order_relaxed)) {
return hr;
}
hook_note_call(g_id_di_getstate);
if (cb == 256) // keyboard: BYTE[256] indexed by DIK (scan code); high bit = pressed
{
BYTE* keys = static_cast<BYTE*>(data);
for (int vk = 0; vk < 256; ++vk)
{
if (g_key_down[vk].load(std::memory_order_relaxed))
{
for (int vk = 0; vk < 256; ++vk) {
if (g_key_down[vk].load(std::memory_order_relaxed)) {
const BYTE dik = vk_to_dik(vk);
if (dik != 0)
{
if (dik != 0) {
keys[dik] |= 0x80;
}
}
}
}
else if (cb == sizeof(DIMOUSESTATE) || cb == sizeof(DIMOUSESTATE2)) // mouse (DIMOUSESTATE2 is a superset)
} else if (cb == sizeof(DIMOUSESTATE) || cb == sizeof(DIMOUSESTATE2)) // mouse (DIMOUSESTATE2 is a superset)
{
auto* m = static_cast<DIMOUSESTATE*>(data); // the shared lead fields (lX/lY/lZ/rgbButtons)
if (g_have_cursor.load(std::memory_order_relaxed))
{
if (g_have_cursor.load(std::memory_order_relaxed)) {
const long x = g_cursor_x.load(std::memory_order_relaxed);
const long y = g_cursor_y.load(std::memory_order_relaxed);
if (g_di_mouse_primed.load(std::memory_order_relaxed)) // relative delta from our cursor
@@ -176,16 +162,13 @@ HRESULT STDMETHODCALLTYPE hk_DI_GetDeviceState(IDirectInputDevice8W* self, DWORD
g_di_mouse_last_y.store(y, std::memory_order_relaxed);
g_di_mouse_primed.store(true, std::memory_order_relaxed);
}
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed)) {
m->rgbButtons[0] |= 0x80;
}
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed)) {
m->rgbButtons[1] |= 0x80;
}
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed)) {
m->rgbButtons[2] |= 0x80;
}
}
@@ -204,38 +187,31 @@ bool is_our_raw(HRAWINPUT h)
UINT WINAPI hk_GetRawInputData(HRAWINPUT hri, UINT cmd, LPVOID pData, PUINT pcbSize, UINT cbHeader)
{
DetourGate::Guard guard(g_gate);
if (g_active.load(std::memory_order_relaxed) && is_our_raw(hri))
{
if (g_active.load(std::memory_order_relaxed) && is_our_raw(hri)) {
hook_note_call(g_id_rawinput);
const RAWINPUT* ri = reinterpret_cast<const RAWINPUT*>(hri);
const UINT body = ri->header.dwType == RIM_TYPEMOUSE ? sizeof(RAWMOUSE) : sizeof(RAWKEYBOARD);
const UINT full = sizeof(RAWINPUTHEADER) + body;
if (pcbSize == nullptr)
{
if (pcbSize == nullptr) {
return static_cast<UINT>(-1);
}
if (cmd == RID_HEADER)
{
if (pData == nullptr)
{
if (cmd == RID_HEADER) {
if (pData == nullptr) {
*pcbSize = sizeof(RAWINPUTHEADER);
return 0;
}
if (*pcbSize < sizeof(RAWINPUTHEADER))
{
if (*pcbSize < sizeof(RAWINPUTHEADER)) {
return static_cast<UINT>(-1);
}
memcpy(pData, &ri->header, sizeof(RAWINPUTHEADER));
return sizeof(RAWINPUTHEADER);
}
// RID_INPUT: the full header + body.
if (pData == nullptr)
{
if (pData == nullptr) {
*pcbSize = full;
return 0;
}
if (*pcbSize < full)
{
if (*pcbSize < full) {
return static_cast<UINT>(-1);
}
memcpy(pData, ri, full);
@@ -247,8 +223,7 @@ UINT WINAPI hk_GetRawInputData(HRAWINPUT hri, UINT cmd, LPVOID pData, PUINT pcbS
// Post a synthetic Raw Input event to `hwnd` (a WM_INPUT carrying one of our g_raw_slots).
void post_raw_key(HWND hwnd, UINT vk, bool down)
{
if (hwnd == nullptr || !g_hk_getrawinputdata)
{
if (hwnd == nullptr || !g_hk_getrawinputdata) {
return;
}
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
@@ -264,8 +239,7 @@ void post_raw_key(HWND hwnd, UINT vk, bool down)
void post_raw_mouse(HWND hwnd, USHORT button_flags)
{
if (hwnd == nullptr || !g_hk_getrawinputdata)
{
if (hwnd == nullptr || !g_hk_getrawinputdata) {
return;
}
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
@@ -283,8 +257,7 @@ LPARAM key_lparam(UINT vk, bool key_up)
{
const UINT scan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC);
LPARAM lp = 1 | (static_cast<LPARAM>(scan) << 16); // repeat count 1 + scan code
if (key_up)
{
if (key_up) {
lp |= (LPARAM{1} << 30) | (LPARAM{1} << 31); // previous-down + transition (key released)
}
return lp;
@@ -292,8 +265,7 @@ LPARAM key_lparam(UINT vk, bool key_up)
void set_key(UINT vk, bool down)
{
if (vk < 256)
{
if (vk < 256) {
g_key_down[vk].store(down, std::memory_order_relaxed);
}
}
@@ -301,16 +273,13 @@ void set_key(UINT vk, bool down)
WPARAM mouse_button_wparam()
{
WPARAM w = 0;
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed)) {
w |= MK_LBUTTON;
}
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed)) {
w |= MK_RBUTTON;
}
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed))
{
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed)) {
w |= MK_MBUTTON;
}
return w;
@@ -324,36 +293,25 @@ void handle_mouse(const MkbEvent& ev, bool down, HWND hwnd)
const UINT vk = ev.code == 0 ? VK_LBUTTON : ev.code == 1 ? VK_RBUTTON : VK_MBUTTON;
set_key(vk, down);
if (hwnd == nullptr)
{
if (hwnd == nullptr) {
return;
}
UINT msg;
if (ev.code == 0)
{
if (ev.code == 0) {
msg = down ? WM_LBUTTONDOWN : WM_LBUTTONUP;
}
else if (ev.code == 1)
{
} else if (ev.code == 1) {
msg = down ? WM_RBUTTONDOWN : WM_RBUTTONUP;
}
else
{
} else {
msg = down ? WM_MBUTTONDOWN : WM_MBUTTONUP;
}
PostMessageW(hwnd, msg, mouse_button_wparam(), MAKELPARAM(ev.x, ev.y));
// Also feed Raw Input games (button event; relative move isn't in the MKB event stream).
USHORT rflags = 0;
if (ev.code == 0)
{
if (ev.code == 0) {
rflags = down ? RI_MOUSE_LEFT_BUTTON_DOWN : RI_MOUSE_LEFT_BUTTON_UP;
}
else if (ev.code == 1)
{
} else if (ev.code == 1) {
rflags = down ? RI_MOUSE_RIGHT_BUTTON_DOWN : RI_MOUSE_RIGHT_BUTTON_UP;
}
else
{
} else {
rflags = down ? RI_MOUSE_MIDDLE_BUTTON_DOWN : RI_MOUSE_MIDDLE_BUTTON_UP;
}
post_raw_mouse(hwnd, rflags);
@@ -364,8 +322,7 @@ void handle_wheel(const MkbEvent& ev, HWND hwnd)
g_cursor_x.store(ev.x, std::memory_order_relaxed);
g_cursor_y.store(ev.y, std::memory_order_relaxed);
g_have_cursor.store(true, std::memory_order_relaxed);
if (hwnd == nullptr)
{
if (hwnd == nullptr) {
return;
}
POINT pt{ev.x, ev.y};
@@ -376,15 +333,12 @@ void handle_wheel(const MkbEvent& ev, HWND hwnd)
void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyhook::InlineHook& slot, int id)
{
if (user32 == nullptr)
{
if (user32 == nullptr) {
return;
}
if (void* target = reinterpret_cast<void*>(GetProcAddress(user32, name)))
{
if (void* target = reinterpret_cast<void*>(GetProcAddress(user32, name))) {
install_inline(slot, target, detour); // StartDisabled -> assign -> enable (no install race)
if (slot)
{
if (slot) {
hook_set_installed(id, true);
}
}
@@ -396,35 +350,27 @@ void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyh
// on the calling thread (the worker thread is).
bool install_dinput_hook()
{
if (g_vh_di_getstate)
{
if (g_vh_di_getstate) {
return true;
}
HMODULE di = GetModuleHandleW(L"dinput8.dll");
if (di == nullptr)
{
if (di == nullptr) {
return false; // not a DirectInput game (yet)
}
using PFN_DI8Create = HRESULT(WINAPI*)(HINSTANCE, DWORD, REFIID, LPVOID*, LPUNKNOWN);
auto create = reinterpret_cast<PFN_DI8Create>(GetProcAddress(di, "DirectInput8Create"));
if (create == nullptr)
{
if (create == nullptr) {
return false;
}
if (g_di_probe == nullptr)
{
if (g_di_probe == nullptr) {
if (FAILED(create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
reinterpret_cast<void**>(&g_di_probe), nullptr)) ||
g_di_probe == nullptr)
{
reinterpret_cast<void**>(&g_di_probe), nullptr))
|| g_di_probe == nullptr) {
return false;
}
}
if (g_di_probe_kbd == nullptr)
{
if (FAILED(g_di_probe->CreateDevice(GUID_SysKeyboard, &g_di_probe_kbd, nullptr)) ||
g_di_probe_kbd == nullptr)
{
if (g_di_probe_kbd == nullptr) {
if (FAILED(g_di_probe->CreateDevice(GUID_SysKeyboard, &g_di_probe_kbd, nullptr)) || g_di_probe_kbd == nullptr) {
return false;
}
}
@@ -438,13 +384,11 @@ bool install_dinput_hook()
bool install_mkb_hooks(IpcClient& ipc)
{
if (g_installed)
{
if (g_installed) {
return true;
}
if (g_id_pump < 0)
{
if (g_id_pump < 0) {
g_id_pump = hook_register("MKB pump (PostMessage)", HookSubsys_Mkb);
g_id_async = hook_register("GetAsyncKeyState", HookSubsys_Mkb);
g_id_kbstate = hook_register("GetKeyboardState", HookSubsys_Mkb);
@@ -454,8 +398,7 @@ bool install_mkb_hooks(IpcClient& ipc)
}
// Fresh synthesized state so a previous session leaves no stuck keys.
for (int vk = 0; vk < 256; ++vk)
{
for (int vk = 0; vk < 256; ++vk) {
g_key_down[vk].store(false, std::memory_order_relaxed);
}
g_have_cursor.store(false, std::memory_order_relaxed);
@@ -469,8 +412,8 @@ bool install_mkb_hooks(IpcClient& ipc)
install_user32_hook(user32, "GetCursorPos", reinterpret_cast<void*>(&hk_GetCursorPos), g_hk_cursor, g_id_cursor);
// Raw Input: synthesize WM_INPUT (in mkb_pump) + serve it from this hook, for games that read
// keyboard/mouse via GetRawInputData. GetRawInputData has a clean prologue -> inline hook is OK.
install_user32_hook(user32, "GetRawInputData", reinterpret_cast<void*>(&hk_GetRawInputData),
g_hk_getrawinputdata, g_id_rawinput);
install_user32_hook(user32, "GetRawInputData", reinterpret_cast<void*>(&hk_GetRawInputData), g_hk_getrawinputdata,
g_id_rawinput);
// DirectInput: vtable-swap GetDeviceState (best-effort -- dinput8.dll may load later, retried
// from mkb_pump). The probe is built once and kept alive (avoids COM churn on a re-enable).
g_di_mouse_primed.store(false, std::memory_order_relaxed);
@@ -485,8 +428,7 @@ bool install_mkb_hooks(IpcClient& ipc)
void remove_mkb_hooks()
{
if (!g_installed)
{
if (!g_installed) {
return;
}
g_active.store(false, std::memory_order_release);
@@ -499,12 +441,11 @@ void remove_mkb_hooks()
disable_for_removal(g_hk_kbstate);
disable_for_removal(g_hk_cursor);
disable_for_removal(g_hk_getrawinputdata);
g_vh_di_getstate.remove(); // restore the DI GetDeviceState slot (probe kept alive for re-enable)
g_gate.drain(); // wait for any in-flight polling / DI / raw detour before clearing state
g_vh_di_getstate.remove(); // restore the DI GetDeviceState slot (probe kept alive for re-enable)
g_gate.drain(); // wait for any in-flight polling / DI / raw detour before clearing state
hook_set_installed(g_id_di_getstate, false);
hook_set_installed(g_id_rawinput, false);
for (int vk = 0; vk < 256; ++vk)
{
for (int vk = 0; vk < 256; ++vk) {
g_key_down[vk].store(false, std::memory_order_relaxed); // no stuck keys
}
g_have_cursor.store(false, std::memory_order_relaxed);
@@ -518,47 +459,39 @@ void remove_mkb_hooks()
void mkb_pump(IpcClient& ipc)
{
MkbRing* ring = ipc.mkb_ring();
if (ring == nullptr || !g_active.load(std::memory_order_relaxed))
{
if (ring == nullptr || !g_active.load(std::memory_order_relaxed)) {
return;
}
if (!g_vh_di_getstate)
{
if (!g_vh_di_getstate) {
install_dinput_hook(); // dinput8.dll can load after we installed; keep retrying cheaply
}
HWND hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
if (hwnd == nullptr || !IsWindow(hwnd))
{
if (hwnd == nullptr || !IsWindow(hwnd)) {
hwnd = find_main_window(GetCurrentProcessId());
g_target.store(hwnd, std::memory_order_relaxed);
}
MkbEvent ev{};
while (pop_mkb_event(*ring, ev))
{
while (pop_mkb_event(*ring, ev)) {
hook_note_call(g_id_pump);
switch (ev.type)
{
switch (ev.type) {
case Mkb_KeyDown:
set_key(ev.code, true);
if (hwnd != nullptr)
{
if (hwnd != nullptr) {
PostMessageW(hwnd, WM_KEYDOWN, ev.code, key_lparam(ev.code, false));
}
post_raw_key(hwnd, ev.code, true); // also feed Raw Input games
break;
case Mkb_KeyUp:
set_key(ev.code, false);
if (hwnd != nullptr)
{
if (hwnd != nullptr) {
PostMessageW(hwnd, WM_KEYUP, ev.code, key_lparam(ev.code, true));
}
post_raw_key(hwnd, ev.code, false);
break;
case Mkb_Char:
if (hwnd != nullptr)
{
if (hwnd != nullptr) {
PostMessageW(hwnd, WM_CHAR, ev.code, 1);
}
break;

View File

@@ -10,8 +10,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
bool install_mkb_hooks(IpcClient& ipc);
void remove_mkb_hooks();

View File

@@ -17,11 +17,9 @@
#include "hook_registry.hpp"
#include "shared_video_texture.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight swap detours before remove frees the shared D3D state
@@ -37,8 +35,8 @@ using PFN_wglGetCurrentContext = HGLRC(WINAPI*)();
IpcClient* g_ipc = nullptr;
unsigned long g_pid = 0;
safetyhook::InlineHook g_hk_swapbuffers; // gdi32!SwapBuffers
safetyhook::InlineHook g_hk_wglswap; // opengl32!wglSwapBuffers
safetyhook::InlineHook g_hk_swapbuffers; // gdi32!SwapBuffers
safetyhook::InlineHook g_hk_wglswap; // opengl32!wglSwapBuffers
int g_id_swapbuffers = -1;
int g_id_wglswap = -1;
@@ -56,8 +54,8 @@ ID3D11Device* g_device = nullptr;
ID3D11DeviceContext* g_ctx = nullptr;
SharedVideoTexture g_shared;
std::vector<unsigned char> g_read_buf; // glReadPixels target (bottom-up)
std::vector<unsigned char> g_flip_buf; // vertically flipped, uploaded to D3D
std::vector<unsigned char> g_read_buf; // glReadPixels target (bottom-up)
std::vector<unsigned char> g_flip_buf; // vertically flipped, uploaded to D3D
// GetBuffer/ReleaseBuffer-style re-entrancy guard: wglSwapBuffers may call
// gdi32!SwapBuffers (or vice versa); capture only on the outermost call.
@@ -65,13 +63,11 @@ thread_local bool t_in_swap = false;
void resolve_gl()
{
if (g_gl_resolved)
{
if (g_gl_resolved) {
return;
}
HMODULE gl = GetModuleHandleW(L"opengl32.dll");
if (gl == nullptr)
{
if (gl == nullptr) {
return; // not an OpenGL process (yet)
}
g_glReadPixels = reinterpret_cast<PFN_glReadPixels>(GetProcAddress(gl, "glReadPixels"));
@@ -82,14 +78,12 @@ void resolve_gl()
bool ensure_device()
{
if (g_device != nullptr)
{
if (g_device != nullptr) {
return true;
}
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, &g_device, nullptr, &g_ctx);
if (FAILED(hr) || g_device == nullptr)
{
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
&g_device, nullptr, &g_ctx);
if (FAILED(hr) || g_device == nullptr) {
logf("opengl: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
return false;
}
@@ -100,10 +94,8 @@ bool ensure_device()
void capture_gl(HDC hdc)
{
resolve_gl();
if (!g_gl_resolved || g_wglGetCurrentContext() == nullptr)
{
if (!g_unsupported_logged)
{
if (!g_gl_resolved || g_wglGetCurrentContext() == nullptr) {
if (!g_unsupported_logged) {
logf("opengl: no current GL context / glReadPixels; capture idle");
g_unsupported_logged = true;
}
@@ -112,32 +104,27 @@ void capture_gl(HDC hdc)
HWND hwnd = WindowFromDC(hdc);
RECT rc{};
if (hwnd == nullptr || !GetClientRect(hwnd, &rc))
{
if (hwnd == nullptr || !GetClientRect(hwnd, &rc)) {
return;
}
const UINT w = static_cast<UINT>(rc.right - rc.left);
const UINT h = static_cast<UINT>(rc.bottom - rc.top);
if (w == 0 || h == 0)
{
if (w == 0 || h == 0) {
return;
}
// DXGI_FORMAT_R8G8B8A8_UNORM matches glReadPixels(GL_RGBA) byte order.
if (!ensure_device() || !g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "opengl"))
{
if (!ensure_device() || !g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "opengl")) {
return;
}
const size_t bytes = static_cast<size_t>(w) * h * 4;
if (g_read_buf.size() != bytes)
{
if (g_read_buf.size() != bytes) {
g_read_buf.resize(bytes);
g_flip_buf.resize(bytes);
}
if (g_glPixelStorei != nullptr)
{
if (g_glPixelStorei != nullptr) {
g_glPixelStorei(GL_PACK_ALIGNMENT, 1);
}
// Reads the back buffer of the current context (bottom-up, origin lower-left).
@@ -145,19 +132,16 @@ void capture_gl(HDC hdc)
// Flip vertically so the image is top-down like a D3D backbuffer.
const size_t row = static_cast<size_t>(w) * 4;
for (UINT y = 0; y < h; ++y)
{
for (UINT y = 0; y < h; ++y) {
memcpy(g_flip_buf.data() + y * row, g_read_buf.data() + (h - 1 - y) * row, row);
}
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_flip_buf.data(), static_cast<UINT>(row), 0);
g_ctx->Flush();
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
}
}
@@ -170,18 +154,15 @@ BOOL swap_detour(safetyhook::InlineHook& hook, int hook_id, HDC hdc)
hook_note_call(hook_id);
g_swaps.fetch_add(1, std::memory_order_relaxed);
const bool outer = !t_in_swap;
if (outer)
{
if (outer) {
t_in_swap = true;
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_present();
}
capture_gl(hdc);
}
const BOOL r = hook.stdcall<BOOL>(hdc); // __stdcall: call() is __cdecl on x86 -> crash
if (outer)
{
if (outer) {
t_in_swap = false;
}
return r;
@@ -205,8 +186,7 @@ bool install_opengl_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_swapbuffers.enabled() || g_hk_wglswap.enabled())
{
if (g_hk_swapbuffers.enabled() || g_hk_wglswap.enabled()) {
return true; // already installed (persistent hooks; re-install below re-enables them)
}
@@ -215,18 +195,14 @@ bool install_opengl_hooks(IpcClient& ipc)
g_unsupported_logged = false;
// gdi32!SwapBuffers is always available (the common GL present call).
if (HMODULE gdi = GetModuleHandleW(L"gdi32.dll"))
{
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gdi, "SwapBuffers")))
{
if (HMODULE gdi = GetModuleHandleW(L"gdi32.dll")) {
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gdi, "SwapBuffers"))) {
install_inline(g_hk_swapbuffers, fn, &hk_SwapBuffers);
}
}
// opengl32!wglSwapBuffers if OpenGL is already loaded.
if (HMODULE gl = GetModuleHandleW(L"opengl32.dll"))
{
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gl, "wglSwapBuffers")))
{
if (HMODULE gl = GetModuleHandleW(L"opengl32.dll")) {
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gl, "wglSwapBuffers"))) {
install_inline(g_hk_wglswap, fn, &hk_wglSwapBuffers);
}
}
@@ -251,13 +227,11 @@ void remove_opengl_hooks()
hook_set_installed(g_id_wglswap, false);
g_gate.drain();
g_shared.release();
if (g_ctx != nullptr)
{
if (g_ctx != nullptr) {
g_ctx->Release();
g_ctx = nullptr;
}
if (g_device != nullptr)
{
if (g_device != nullptr) {
g_device->Release();
g_device = nullptr;
}

View File

@@ -11,8 +11,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Installs the OpenGL swap hooks. `ipc` must outlive the hooks. Returns true if at
// least SwapBuffers was hooked. Safe to call repeatedly.

View File

@@ -22,11 +22,9 @@
#include "shared_video_texture.hpp"
#include "vtable_hook.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight Present/ECL detours before remove frees the shared state
@@ -116,8 +114,7 @@ std::atomic<ID3D12CommandQueue*> g_present_queue{nullptr};
// test present is counted but produces no frame. Render-thread only; small fixed tables.
constexpr int kMaxLoggedPresents = 16;
constexpr int kMaxLoggedSwapchains = 8;
struct LoggedPresent
{
struct LoggedPresent {
void* swapchain;
UINT flags;
};
@@ -129,15 +126,12 @@ int g_logged_swapchains_n = 0;
// True the first time this (swapchain, flags) pair is presented, so the caller logs once.
bool first_present_with_flags(void* swapchain, UINT flags)
{
for (int i = 0; i < g_logged_presents_n; ++i)
{
if (g_logged_presents[i].swapchain == swapchain && g_logged_presents[i].flags == flags)
{
for (int i = 0; i < g_logged_presents_n; ++i) {
if (g_logged_presents[i].swapchain == swapchain && g_logged_presents[i].flags == flags) {
return false;
}
}
if (g_logged_presents_n >= kMaxLoggedPresents)
{
if (g_logged_presents_n >= kMaxLoggedPresents) {
return false;
}
g_logged_presents[g_logged_presents_n++] = {swapchain, flags};
@@ -147,15 +141,12 @@ bool first_present_with_flags(void* swapchain, UINT flags)
// True the first time this swapchain feeds the capture, so the caller logs it once.
bool first_capture_from(void* swapchain)
{
for (int i = 0; i < g_logged_swapchains_n; ++i)
{
if (g_logged_swapchains[i] == swapchain)
{
for (int i = 0; i < g_logged_swapchains_n; ++i) {
if (g_logged_swapchains[i] == swapchain) {
return false;
}
}
if (g_logged_swapchains_n >= kMaxLoggedSwapchains)
{
if (g_logged_swapchains_n >= kMaxLoggedSwapchains) {
return false;
}
g_logged_swapchains[g_logged_swapchains_n++] = swapchain;
@@ -165,33 +156,27 @@ bool first_capture_from(void* swapchain)
// Drop the D3D11On12 bridge. Caller holds g_tex_mutex.
void release_on12_locked()
{
if (g_on12_ctx != nullptr)
{
if (g_on12_ctx != nullptr) {
g_on12_ctx->Release();
g_on12_ctx = nullptr;
}
if (g_on12 != nullptr)
{
if (g_on12 != nullptr) {
g_on12->Release();
g_on12 = nullptr;
}
if (g_on12_d3d11 != nullptr)
{
if (g_on12_d3d11 != nullptr) {
g_on12_d3d11->Release();
g_on12_d3d11 = nullptr;
}
if (g_on12_queue != nullptr)
{
if (g_on12_queue != nullptr) {
g_on12_queue->Release();
g_on12_queue = nullptr;
}
if (g_on12_d3d12 != nullptr)
{
if (g_on12_d3d12 != nullptr) {
g_on12_d3d12->Release();
g_on12_d3d12 = nullptr;
}
if (g_copy_fence != nullptr)
{
if (g_copy_fence != nullptr) {
g_copy_fence->Release();
g_copy_fence = nullptr;
}
@@ -205,8 +190,7 @@ void release_on12_locked()
// holds g_tex_mutex. Returns true when the bridge is ready.
bool ensure_on12_locked(ID3D12Device* dev)
{
if (g_on12 != nullptr && g_on12_d3d12 == dev)
{
if (g_on12 != nullptr && g_on12_d3d12 == dev) {
return true;
}
release_on12_locked();
@@ -215,8 +199,7 @@ bool ensure_on12_locked(ID3D12Device* dev)
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ID3D12CommandQueue* queue = nullptr;
HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue));
if (FAILED(hr) || queue == nullptr)
{
if (FAILED(hr) || queue == nullptr) {
logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast<unsigned long>(hr));
return false;
}
@@ -225,19 +208,16 @@ bool ensure_on12_locked(ID3D12Device* dev)
ID3D11Device* d11 = nullptr;
ID3D11DeviceContext* ctx = nullptr;
hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr);
if (FAILED(hr) || d11 == nullptr)
{
if (FAILED(hr) || d11 == nullptr) {
logf("present(d3d12): D3D11On12CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
queue->Release();
return false;
}
ID3D11On12Device* on12 = nullptr;
hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast<void**>(&on12));
if (FAILED(hr) || on12 == nullptr)
{
if (FAILED(hr) || on12 == nullptr) {
logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast<unsigned long>(hr));
if (ctx != nullptr)
{
if (ctx != nullptr) {
ctx->Release();
}
d11->Release();
@@ -249,10 +229,8 @@ bool ensure_on12_locked(ID3D12Device* dev)
// cross-queue ordering (the copy can then race the frame, the pre-fence behavior).
ID3D12Fence* fence = nullptr;
hr = dev->CreateFence(0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), reinterpret_cast<void**>(&fence));
if (FAILED(hr) || fence == nullptr)
{
logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)",
static_cast<unsigned long>(hr));
if (FAILED(hr) || fence == nullptr) {
logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)", static_cast<unsigned long>(hr));
fence = nullptr;
}
@@ -283,17 +261,14 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
// buffer. Query IDXGISwapChain3 for it; fall back to 0 only if unavailable.
UINT bb_index = 0;
IDXGISwapChain3* sc3 = nullptr;
if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast<void**>(&sc3))) && sc3 != nullptr)
{
if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast<void**>(&sc3))) && sc3 != nullptr) {
bb_index = sc3->GetCurrentBackBufferIndex();
sc3->Release();
}
ID3D12Resource* bb = nullptr;
if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr)
{
if (!g_unsupported_logged)
{
if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr) {
if (!g_unsupported_logged) {
logf("present: backbuffer is neither ID3D11Texture2D nor ID3D12Resource (D3D9/Vulkan?); idle");
g_unsupported_logged = true;
}
@@ -309,25 +284,21 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
// The game's present queue, preferring the one seen on this (the render) thread.
ID3D12CommandQueue* game_queue = t_present_queue;
if (game_queue == nullptr)
{
if (game_queue == nullptr) {
game_queue = g_present_queue.load(std::memory_order_relaxed);
}
// Log each distinct swapchain feeding the capture once (size/format/buffer index).
if (first_capture_from(sc))
{
if (first_capture_from(sc)) {
const D3D12_RESOURCE_DESC rd = bb->GetDesc();
logf("present: swapchain=%p capturing D3D12 backbuffer %llux%u fmt=%d samples=%u bufferindex=%u queue=%s",
sc, static_cast<unsigned long long>(rd.Width), rd.Height, static_cast<int>(rd.Format),
rd.SampleDesc.Count, bb_index, game_queue != nullptr ? "known" : "unknown");
logf("present: swapchain=%p capturing D3D12 backbuffer %llux%u fmt=%d samples=%u bufferindex=%u queue=%s", sc,
static_cast<unsigned long long>(rd.Width), rd.Height, static_cast<int>(rd.Format), rd.SampleDesc.Count,
bb_index, game_queue != nullptr ? "known" : "unknown");
}
if (dev != nullptr)
{
if (dev != nullptr) {
std::scoped_lock lock(g_tex_mutex);
if (ensure_on12_locked(dev))
{
if (ensure_on12_locked(dev)) {
// DX12 capture costs more present-thread overhead than DX11/OpenGL (~0.38 ms vs
// ~0.05/0.09 ms, measured) because it goes through the D3D11On12 bridge: the
// CopyResource on the 11On12 immediate context (~0.13 ms) plus the mandatory Flush
@@ -339,8 +310,7 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
// Order our copy after the game's frame without burdening the game's queue: the
// game queue signals the fence (cheap), our copy queue waits on it. Skipped if the
// queue isn't captured yet or the fence is missing (one possibly-early frame).
if (game_queue != nullptr && g_copy_fence != nullptr)
{
if (game_queue != nullptr && g_copy_fence != nullptr) {
const UINT64 fence_val = ++g_copy_fence_val;
game_queue->Signal(g_copy_fence, fence_val);
g_on12_queue->Wait(g_copy_fence, fence_val);
@@ -349,33 +319,26 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
D3D11_RESOURCE_FLAGS rf{};
rf.BindFlags = D3D11_BIND_RENDER_TARGET;
ID3D11Resource* wrapped = nullptr;
HRESULT hr = g_on12->CreateWrappedResource(bb, &rf, D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_PRESENT, __uuidof(ID3D11Resource),
reinterpret_cast<void**>(&wrapped));
if (SUCCEEDED(hr) && wrapped != nullptr)
{
HRESULT hr =
g_on12->CreateWrappedResource(bb, &rf, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_PRESENT,
__uuidof(ID3D11Resource), reinterpret_cast<void**>(&wrapped));
if (SUCCEEDED(hr) && wrapped != nullptr) {
g_on12->AcquireWrappedResources(&wrapped, 1);
ID3D11Texture2D* wtex = nullptr;
if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D),
reinterpret_cast<void**>(&wtex))) &&
wtex != nullptr)
{
if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&wtex)))
&& wtex != nullptr) {
D3D11_TEXTURE2D_DESC d{};
wtex->GetDesc(&d);
w = d.Width;
h = d.Height;
fmt = d.Format;
if (d.SampleDesc.Count == 1 &&
g_shared.ensure(g_on12_d3d11, w, h, fmt, g_pid, "present", kShareBind))
{
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 0) == S_OK)
{
if (d.SampleDesc.Count == 1
&& g_shared.ensure(g_on12_d3d11, w, h, fmt, g_pid, "present", kShareBind)) {
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 0) == S_OK) {
g_on12_ctx->CopyResource(g_shared.texture(), wtex);
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
shared = true;
}
else
{
} else {
dropped = true; // host held the mutex -> this frame never reaches the mirror
}
}
@@ -384,29 +347,22 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
g_on12->ReleaseWrappedResources(&wrapped, 1);
g_on12_ctx->Flush();
wrapped->Release();
}
else if (!g_unsupported_logged)
{
} else if (!g_unsupported_logged) {
logf("present(d3d12): CreateWrappedResource failed hr=0x%08lX", static_cast<unsigned long>(hr));
g_unsupported_logged = true;
}
}
}
if (shared)
{
if (shared) {
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(fmt));
}
}
else if (dropped && g_ipc != nullptr)
{
} else if (dropped && g_ipc != nullptr) {
g_ipc->note_video_dropped();
}
if (dev != nullptr)
{
if (dev != nullptr) {
dev->Release();
}
bb->Release();
@@ -415,26 +371,22 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
// Drop the hook-owned D3D11 device and the D3D10 staging texture. Caller holds g_tex_mutex.
void release_aux_locked()
{
if (g_d3d10_staging != nullptr)
{
if (g_d3d10_staging != nullptr) {
g_d3d10_staging->Release();
g_d3d10_staging = nullptr;
}
if (g_d3d10_dev != nullptr)
{
if (g_d3d10_dev != nullptr) {
g_d3d10_dev->Release();
g_d3d10_dev = nullptr;
}
g_d3d10_w = g_d3d10_h = 0;
g_d3d10_fmt = DXGI_FORMAT_UNKNOWN;
g_force_d3d10 = false;
if (g_aux_ctx != nullptr)
{
if (g_aux_ctx != nullptr) {
g_aux_ctx->Release();
g_aux_ctx = nullptr;
}
if (g_aux_d3d11 != nullptr)
{
if (g_aux_d3d11 != nullptr) {
g_aux_d3d11->Release();
g_aux_d3d11 = nullptr;
}
@@ -444,16 +396,14 @@ void release_aux_locked()
// (the game has no D3D11 device of its own). Caller holds g_tex_mutex.
bool ensure_aux_d3d11_locked()
{
if (g_aux_d3d11 != nullptr)
{
if (g_aux_d3d11 != nullptr) {
return true;
}
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0};
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels,
static_cast<UINT>(std::size(levels)), D3D11_SDK_VERSION, &g_aux_d3d11, nullptr,
&g_aux_ctx);
if (FAILED(hr) || g_aux_d3d11 == nullptr)
{
HRESULT hr =
D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, static_cast<UINT>(std::size(levels)),
D3D11_SDK_VERSION, &g_aux_d3d11, nullptr, &g_aux_ctx);
if (FAILED(hr) || g_aux_d3d11 == nullptr) {
logf("present(d3d10): aux D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
g_aux_d3d11 = nullptr;
g_aux_ctx = nullptr;
@@ -470,20 +420,17 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
{
D3D10_TEXTURE2D_DESC bd{};
backbuf->GetDesc(&bd);
if (first_capture_from(sc))
{
if (first_capture_from(sc)) {
logf("present: swapchain=%p capturing D3D10 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width, bd.Height,
static_cast<int>(bd.Format), bd.SampleDesc.Count);
}
if (bd.SampleDesc.Count != 1)
{
if (bd.SampleDesc.Count != 1) {
return; // MSAA: would need ResolveSubresource; skip rather than mis-copy
}
ID3D10Device* gdev = nullptr;
backbuf->GetDevice(&gdev);
if (gdev == nullptr)
{
if (gdev == nullptr) {
return;
}
@@ -491,16 +438,13 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
bool dropped = false;
{
std::scoped_lock lock(g_tex_mutex);
if (!(g_d3d10_staging != nullptr && g_d3d10_dev == gdev && g_d3d10_w == bd.Width &&
g_d3d10_h == bd.Height && g_d3d10_fmt == bd.Format))
{
if (g_d3d10_staging != nullptr)
{
if (!(g_d3d10_staging != nullptr && g_d3d10_dev == gdev && g_d3d10_w == bd.Width && g_d3d10_h == bd.Height
&& g_d3d10_fmt == bd.Format)) {
if (g_d3d10_staging != nullptr) {
g_d3d10_staging->Release();
g_d3d10_staging = nullptr;
}
if (g_d3d10_dev != nullptr)
{
if (g_d3d10_dev != nullptr) {
g_d3d10_dev->Release();
g_d3d10_dev = nullptr;
}
@@ -513,8 +457,7 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
sd.SampleDesc.Count = 1;
sd.Usage = D3D10_USAGE_STAGING;
sd.CPUAccessFlags = D3D10_CPU_ACCESS_READ;
if (SUCCEEDED(gdev->CreateTexture2D(&sd, nullptr, &g_d3d10_staging)) && g_d3d10_staging != nullptr)
{
if (SUCCEEDED(gdev->CreateTexture2D(&sd, nullptr, &g_d3d10_staging)) && g_d3d10_staging != nullptr) {
g_d3d10_dev = gdev;
gdev->AddRef();
g_d3d10_w = bd.Width;
@@ -523,21 +466,16 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
}
}
if (g_d3d10_staging != nullptr && ensure_aux_d3d11_locked() &&
g_shared.ensure(g_aux_d3d11, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind))
{
if (g_d3d10_staging != nullptr && ensure_aux_d3d11_locked()
&& g_shared.ensure(g_aux_d3d11, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind)) {
gdev->CopyResource(g_d3d10_staging, backbuf);
D3D10_MAPPED_TEXTURE2D m{};
if (SUCCEEDED(g_d3d10_staging->Map(0, D3D10_MAP_READ, 0, &m)) && m.pData != nullptr)
{
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
if (SUCCEEDED(g_d3d10_staging->Map(0, D3D10_MAP_READ, 0, &m)) && m.pData != nullptr) {
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
g_aux_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, m.pData, m.RowPitch, 0);
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
shared = true;
}
else
{
} else {
dropped = true;
}
g_d3d10_staging->Unmap(0);
@@ -545,16 +483,12 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
}
}
if (shared)
{
if (shared) {
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
}
}
else if (dropped && g_ipc != nullptr)
{
} else if (dropped && g_ipc != nullptr) {
g_ipc->note_video_dropped();
}
gdev->Release();
@@ -568,12 +502,10 @@ void capture_backbuffer(IDXGISwapChain* sc)
// ID3D11Texture2D (so we can't discriminate by GetBuffer), but its feature-level-10 device
// rejects the share flags -- so a failed shared-texture creation is the signal to switch
// (sticky) to the D3D10 read-back path, which reads through the game's own D3D10 device.
if (!g_force_d3d10)
{
if (!g_force_d3d10) {
ID3D11Texture2D* backbuf = nullptr;
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf))) ||
backbuf == nullptr)
{
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf)))
|| backbuf == nullptr) {
capture_backbuffer_d3d12(sc); // D3D12 game: bridge via D3D11On12 (or idle if neither)
return;
}
@@ -583,8 +515,7 @@ void capture_backbuffer(IDXGISwapChain* sc)
bool shared = false;
bool dropped = false;
bool cant_host = false;
if (bd.SampleDesc.Count != 1)
{
if (bd.SampleDesc.Count != 1) {
backbuf->Release(); // MSAA would need ResolveSubresource; skip rather than mis-copy
return;
}
@@ -592,62 +523,47 @@ void capture_backbuffer(IDXGISwapChain* sc)
ID3D11Device* device = nullptr;
backbuf->GetDevice(&device);
ID3D11DeviceContext* ctx = nullptr;
if (device != nullptr)
{
if (device != nullptr) {
device->GetImmediateContext(&ctx);
}
if (device != nullptr && ctx != nullptr)
{
if (device != nullptr && ctx != nullptr) {
std::scoped_lock lock(g_tex_mutex);
if (g_shared.ensure(device, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind))
{
if (first_capture_from(sc))
{
if (g_shared.ensure(device, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind)) {
if (first_capture_from(sc)) {
logf("present: swapchain=%p capturing D3D11 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width,
bd.Height, static_cast<int>(bd.Format), bd.SampleDesc.Count);
}
// Key 0 on both sides: a plain cross-process mutex on the texture (created
// released at key 0). Bounded wait so a stalled host consumer can never hang
// the game's render thread.
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
ctx->CopyResource(g_shared.texture(), backbuf);
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
shared = true;
}
else
{
} else {
dropped = true; // host held the mutex past the wait -> frame lost (rare on D3D11)
}
}
else
{
} else {
cant_host = true; // device can't host the shared texture -> try the D3D10 path
}
}
if (ctx != nullptr)
{
if (ctx != nullptr) {
ctx->Release();
}
if (device != nullptr)
{
if (device != nullptr) {
device->Release();
}
backbuf->Release();
if (shared)
{
if (shared) {
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
}
return;
}
if (!cant_host)
{
if (dropped && g_ipc != nullptr)
{
if (!cant_host) {
if (dropped && g_ipc != nullptr) {
g_ipc->note_video_dropped();
}
return; // captured-or-dropped on the D3D11 path; nothing else to try this frame
@@ -659,8 +575,7 @@ void capture_backbuffer(IDXGISwapChain* sc)
// D3D10 game: its backbuffer must be read through its own D3D10 device.
ID3D10Texture2D* bb10 = nullptr;
if (SUCCEEDED(sc->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&bb10))) && bb10 != nullptr)
{
if (SUCCEEDED(sc->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&bb10))) && bb10 != nullptr) {
capture_backbuffer_d3d10(sc, bb10);
bb10->Release();
}
@@ -672,8 +587,7 @@ void STDMETHODCALLTYPE hk_ExecuteCommandLists(ID3D12CommandQueue* queue, UINT nu
DetourGate::Guard guard(g_gate); // keep g_present_queue/g_hk_ecl alive for this detour
// Record the graphics queue; compute/copy queues never present, so skip them and
// keep the last DIRECT one (the present queue on single-graphics-queue engines).
if (queue != nullptr && queue->GetDesc().Type == D3D12_COMMAND_LIST_TYPE_DIRECT)
{
if (queue != nullptr && queue->GetDesc().Type == D3D12_COMMAND_LIST_TYPE_DIRECT) {
t_present_queue = queue;
g_present_queue.store(queue, std::memory_order_relaxed);
hook_note_call(g_id_ecl);
@@ -689,29 +603,25 @@ void STDMETHODCALLTYPE hk_ExecuteCommandLists(ID3D12CommandQueue* queue, UINT nu
void* grab_execute_command_lists_address()
{
HMODULE d3d12 = GetModuleHandleW(L"d3d12.dll");
if (d3d12 == nullptr)
{
if (d3d12 == nullptr) {
return nullptr; // not a D3D12 game -> nothing to capture
}
using PFN_D3D12_CREATE_DEVICE = HRESULT(WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
auto create = reinterpret_cast<PFN_D3D12_CREATE_DEVICE>(GetProcAddress(d3d12, "D3D12CreateDevice"));
if (create == nullptr)
{
if (create == nullptr) {
return nullptr;
}
ID3D12Device* dev = nullptr;
if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast<void**>(&dev))) ||
dev == nullptr)
{
if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast<void**>(&dev)))
|| dev == nullptr) {
return nullptr;
}
D3D12_COMMAND_QUEUE_DESC qd{};
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ID3D12CommandQueue* queue = nullptr;
void* addr = nullptr;
if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue))) &&
queue != nullptr)
{
if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue)))
&& queue != nullptr) {
addr = vtable_method(queue, kIdx_ID3D12CommandQueue_ExecuteCommandLists);
queue->Release();
}
@@ -728,17 +638,14 @@ void on_present(IDXGISwapChain* sc, UINT flags, int hook_id, const char* method)
{
hook_note_call(hook_id);
g_present_calls.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_present();
}
if (first_present_with_flags(sc, flags))
{
if (first_present_with_flags(sc, flags)) {
logf("present: swapchain=%p %s flags=0x%08X%s", sc, method, flags,
(flags & DXGI_PRESENT_TEST) ? " (DXGI_PRESENT_TEST: occlusion probe, no frame drawn)" : "");
}
if ((flags & DXGI_PRESENT_TEST) == 0)
{
if ((flags & DXGI_PRESENT_TEST) == 0) {
capture_backbuffer(sc);
}
}
@@ -758,7 +665,7 @@ HRESULT STDMETHODCALLTYPE hk_Present(IDXGISwapChain* sc, UINT sync_interval, UIN
HRESULT STDMETHODCALLTYPE hk_Present1(IDXGISwapChain1* sc, UINT sync_interval, UINT flags,
const DXGI_PRESENT_PARAMETERS* params)
{
DetourGate::Guard guard(g_gate); // keep the shared texture / On12 bridge alive for this detour
DetourGate::Guard guard(g_gate); // keep the shared texture / On12 bridge alive for this detour
on_present(sc, flags, g_id_present1, "Present1"); // IDXGISwapChain1 derives from IDXGISwapChain
return g_hk_present1.stdcall<HRESULT>(sc, sync_interval, flags, params); // __stdcall, see hk_Present
}
@@ -779,8 +686,7 @@ void* grab_present_address(void** present1_out)
RegisterClassExW(&wc);
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 8, 8, nullptr, nullptr,
wc.hInstance, nullptr);
if (hwnd == nullptr)
{
if (hwnd == nullptr) {
return nullptr;
}
@@ -801,31 +707,24 @@ void* grab_present_address(void** present1_out)
const HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, &scd, &swapchain, &device, nullptr, &ctx);
void* present = nullptr;
if (SUCCEEDED(hr) && swapchain != nullptr)
{
if (SUCCEEDED(hr) && swapchain != nullptr) {
present = vtable_method(swapchain, kIdx_IDXGISwapChain_Present);
IDXGISwapChain1* sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(__uuidof(IDXGISwapChain1), reinterpret_cast<void**>(&sc1))) &&
sc1 != nullptr)
{
if (SUCCEEDED(swapchain->QueryInterface(__uuidof(IDXGISwapChain1), reinterpret_cast<void**>(&sc1)))
&& sc1 != nullptr) {
*present1_out = vtable_method(sc1, kIdx_IDXGISwapChain1_Present1);
sc1->Release();
}
}
else
{
} else {
logf("present: D3D11CreateDeviceAndSwapChain(probe) failed hr=0x%08lX", static_cast<unsigned long>(hr));
}
if (ctx != nullptr)
{
if (ctx != nullptr) {
ctx->Release();
}
if (device != nullptr)
{
if (device != nullptr) {
device->Release();
}
if (swapchain != nullptr)
{
if (swapchain != nullptr) {
swapchain->Release();
}
DestroyWindow(hwnd);
@@ -839,8 +738,7 @@ bool install_present_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_present.enabled())
{
if (g_hk_present.enabled()) {
return true; // already installed (persistent hook; the re-install path below re-enables it)
}
@@ -850,15 +748,13 @@ bool install_present_hooks(IpcClient& ipc)
void* present1 = nullptr;
void* present = grab_present_address(&present1);
if (present == nullptr)
{
if (present == nullptr) {
hook_set_installed(g_id_present, false);
hook_set_installed(g_id_present1, false);
return false;
}
install_inline(g_hk_present, present, &hk_Present);
if (present1 != nullptr)
{
if (present1 != nullptr) {
install_inline(g_hk_present1, present1, &hk_Present1);
}
g_unsupported_logged = false;
@@ -871,15 +767,11 @@ bool install_present_hooks(IpcClient& ipc)
// here at injection time -- d3d12.dll is already loaded in a running D3D12 game --
// so the queue is recovered even though we attached after it was created.
void* ecl = grab_execute_command_lists_address();
if (ecl != nullptr)
{
if (ecl != nullptr) {
install_inline(g_hk_ecl, ecl, &hk_ExecuteCommandLists);
hook_set_installed(g_id_ecl, static_cast<bool>(g_hk_ecl));
logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl,
static_cast<bool>(g_hk_ecl) ? 1 : 0);
}
else
{
logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl, static_cast<bool>(g_hk_ecl) ? 1 : 0);
} else {
hook_set_installed(g_id_ecl, false); // not a D3D12 game; On12 path uses its own queue
}
return static_cast<bool>(g_hk_present);

View File

@@ -13,8 +13,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Installs the Present hook. Grabs IDXGISwapChain::Present from a throwaway
// swapchain and inline-hooks it, so every swapchain in the process is caught.

View File

@@ -25,20 +25,17 @@
#include <cstdint>
namespace coop::hook
{
namespace coop::hook {
// Snap a measured rate to the nearest standard rate when within `tol` (fractional);
// returns 0 when it doesn't land near any standard rate. The standard rates are spaced
// >8% apart, so a 2% tolerance is unambiguous.
inline std::uint32_t snap_standard_rate(double measured, double tol = 0.02)
{
static constexpr std::uint32_t kStd[] = {8000, 11025, 16000, 22050, 32000, 44100,
static constexpr std::uint32_t kStd[] = {8000, 11025, 16000, 22050, 32000, 44100,
48000, 88200, 96000, 176400, 192000};
for (std::uint32_t s : kStd)
{
if (measured >= s * (1.0 - tol) && measured <= s * (1.0 + tol))
{
for (std::uint32_t s : kStd) {
if (measured >= s * (1.0 - tol) && measured <= s * (1.0 + tol)) {
return s;
}
}
@@ -46,16 +43,14 @@ inline std::uint32_t snap_standard_rate(double measured, double tol = 0.02)
}
// Outcome of feeding one measurement tick.
struct RateEstimate
{
bool done = false; // a rate has been decided (stop feeding)
std::uint32_t rate = 0; // the decided rate, valid when done
bool confident = false; // true = consensus on a standard rate; false = low-confidence fallback
struct RateEstimate {
bool done = false; // a rate has been decided (stop feeding)
std::uint32_t rate = 0; // the decided rate, valid when done
bool confident = false; // true = consensus on a standard rate; false = low-confidence fallback
};
class RateEstimator
{
public:
class RateEstimator {
public:
// Window length, consensus count, and the attempt budget before giving up to a
// low-confidence estimate. Public so a caller/test can tune them; the defaults are
// what the hook ships.
@@ -68,18 +63,15 @@ public:
// Call repeatedly (e.g. each worker tick); returns done=false while still measuring.
RateEstimate feed(std::uint64_t frames, std::int64_t now_qpc, std::int64_t freq)
{
if (freq <= 0)
{
if (freq <= 0) {
return {};
}
if (window_qpc_ == 0)
{
if (window_qpc_ == 0) {
start_window(frames, now_qpc); // begin the first window
return {};
}
const std::int64_t dt = now_qpc - window_qpc_;
if (dt < static_cast<std::int64_t>(window_seconds * static_cast<double>(freq)))
{
if (dt < static_cast<std::int64_t>(window_seconds * static_cast<double>(freq))) {
return {}; // window still filling
}
const std::uint64_t df = frames - window_frames_;
@@ -87,16 +79,14 @@ public:
start_window(frames, now_qpc); // next window starts here
const double raw = static_cast<double>(df) / secs;
if (raw < min_audio_rate)
{
if (raw < min_audio_rate) {
// Stream went (near-)idle this window: can't trust it. Drop back to the
// warm-up state so the next active window is discarded, not measured.
primed_ = false;
reset_consensus();
return {};
}
if (!primed_)
{
if (!primed_) {
// Discard the first full active window: a freshly-attached stream can deliver
// its already-queued buffers in a burst, over-counting frames.
primed_ = true;
@@ -107,32 +97,23 @@ public:
++attempts_;
last_raw_ = raw;
const std::uint32_t snapped = snap_standard_rate(raw);
if (snapped != 0)
{
if (snapped == last_snapped_)
{
if (snapped != 0) {
if (snapped == last_snapped_) {
++agree_;
}
else
{
} else {
last_snapped_ = snapped;
agree_ = 1;
}
if (agree_ >= needed_agree)
{
if (agree_ >= needed_agree) {
return {true, snapped, true}; // consensus -> confident
}
}
else
{
} else {
reset_consensus(); // a non-snapping window breaks the streak
}
if (attempts_ >= max_attempts)
{
if (attempts_ >= max_attempts) {
// Give up on consensus: a snapped value seen along the way beats a raw one.
const std::uint32_t best =
last_snapped_ != 0 ? last_snapped_ : static_cast<std::uint32_t>(last_raw_ + 0.5);
const std::uint32_t best = last_snapped_ != 0 ? last_snapped_ : static_cast<std::uint32_t>(last_raw_ + 0.5);
return {true, best, false}; // low-confidence
}
return {};
@@ -149,7 +130,7 @@ public:
reset_consensus();
}
private:
private:
void start_window(std::uint64_t frames, std::int64_t qpc)
{
window_frames_ = frames;

View File

@@ -13,17 +13,12 @@
#include "coop/shared_memory.hpp"
#include "debug_log.hpp"
namespace coop::hook
{
namespace coop::hook {
class SharedVideoTexture
{
public:
class SharedVideoTexture {
public:
SharedVideoTexture() = default;
~SharedVideoTexture()
{
release();
}
~SharedVideoTexture() { release(); }
SharedVideoTexture(const SharedVideoTexture&) = delete;
SharedVideoTexture& operator=(const SharedVideoTexture&) = delete;
@@ -35,8 +30,7 @@ public:
bool ensure(ID3D11Device* device, UINT w, UINT h, DXGI_FORMAT fmt, unsigned long pid, const char* tag,
UINT bind = D3D11_BIND_SHADER_RESOURCE)
{
if (m_tex != nullptr && m_w == w && m_h == h && m_fmt == fmt)
{
if (m_tex != nullptr && m_w == w && m_h == h && m_fmt == fmt) {
return true;
}
release();
@@ -52,35 +46,31 @@ public:
desc.BindFlags = bind;
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
HRESULT hr = device->CreateTexture2D(&desc, nullptr, &m_tex);
if (FAILED(hr) || m_tex == nullptr)
{
logf("%s: CreateTexture2D(shared) failed hr=0x%08lX (%ux%u fmt=%d)", tag,
static_cast<unsigned long>(hr), w, h, static_cast<int>(fmt));
if (FAILED(hr) || m_tex == nullptr) {
logf("%s: CreateTexture2D(shared) failed hr=0x%08lX (%ux%u fmt=%d)", tag, static_cast<unsigned long>(hr), w,
h, static_cast<int>(fmt));
release();
return false;
}
IDXGIResource1* res = nullptr;
hr = m_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast<void**>(&res));
if (FAILED(hr) || res == nullptr)
{
if (FAILED(hr) || res == nullptr) {
logf("%s: QI IDXGIResource1 failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
release();
return false;
}
const std::wstring name = video_share_name(pid);
hr = res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
name.c_str(), &m_handle);
hr = res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, name.c_str(),
&m_handle);
res->Release();
if (FAILED(hr) || m_handle == nullptr)
{
if (FAILED(hr) || m_handle == nullptr) {
logf("%s: CreateSharedHandle failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
release();
return false;
}
hr = m_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast<void**>(&m_mutex));
if (FAILED(hr) || m_mutex == nullptr)
{
if (FAILED(hr) || m_mutex == nullptr) {
logf("%s: QI IDXGIKeyedMutex failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
release();
return false;
@@ -95,18 +85,15 @@ public:
void release()
{
if (m_mutex != nullptr)
{
if (m_mutex != nullptr) {
m_mutex->Release();
m_mutex = nullptr;
}
if (m_tex != nullptr)
{
if (m_tex != nullptr) {
m_tex->Release();
m_tex = nullptr;
}
if (m_handle != nullptr)
{
if (m_handle != nullptr) {
CloseHandle(m_handle);
m_handle = nullptr;
}
@@ -114,16 +101,10 @@ public:
m_fmt = DXGI_FORMAT_UNKNOWN;
}
[[nodiscard]] ID3D11Texture2D* texture() const
{
return m_tex;
}
[[nodiscard]] IDXGIKeyedMutex* mutex() const
{
return m_mutex;
}
[[nodiscard]] ID3D11Texture2D* texture() const { return m_tex; }
[[nodiscard]] IDXGIKeyedMutex* mutex() const { return m_mutex; }
private:
private:
ID3D11Texture2D* m_tex = nullptr;
IDXGIKeyedMutex* m_mutex = nullptr;
HANDLE m_handle = nullptr; // named NT handle backing the share; closed on release

View File

@@ -4,8 +4,7 @@
#include "coop/protocol.hpp"
namespace coop::hook
{
namespace coop::hook {
VkCapture::~VkCapture()
{
@@ -27,43 +26,31 @@ bool VkCapture::find_readback_memory(std::uint32_t type_bits, std::uint32_t& out
int best = -1;
bool best_coherent = true;
int best_rank = -1;
for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i)
{
if ((type_bits & (1u << i)) == 0)
{
for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i) {
if ((type_bits & (1u << i)) == 0) {
continue;
}
const VkMemoryPropertyFlags f = mp.memoryTypes[i].propertyFlags;
if ((f & vis) == 0)
{
if ((f & vis) == 0) {
continue;
}
int rank;
if ((f & cached) && (f & coherent))
{
if ((f & cached) && (f & coherent)) {
rank = 3;
}
else if (f & cached)
{
} else if (f & cached) {
rank = 2;
}
else if (f & coherent)
{
} else if (f & coherent) {
rank = 1;
}
else
{
} else {
continue; // host-visible but neither cached nor coherent: unusable for a CPU read-back
}
if (rank > best_rank)
{
if (rank > best_rank) {
best_rank = rank;
best = static_cast<int>(i);
best_coherent = (f & coherent) != 0;
}
}
if (best < 0)
{
if (best < 0) {
return false;
}
out_index = static_cast<std::uint32_t>(best);
@@ -73,33 +60,28 @@ bool VkCapture::find_readback_memory(std::uint32_t type_bits, std::uint32_t& out
bool VkCapture::ensure_slot_pool()
{
if (m_pool != VK_NULL_HANDLE)
{
if (m_pool != VK_NULL_HANDLE) {
return true;
}
if (m_queue == VK_NULL_HANDLE)
{
if (m_queue == VK_NULL_HANDLE) {
m_fns.GetDeviceQueue(m_device, m_qfam, 0, &m_queue);
}
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = m_qfam;
if (m_fns.CreateCommandPool(m_device, &pci, nullptr, &m_pool) != VK_SUCCESS)
{
if (m_fns.CreateCommandPool(m_device, &pci, nullptr, &m_pool) != VK_SUCCESS) {
return false;
}
for (Slot& s : m_slots)
{
for (Slot& s : m_slots) {
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ai.commandPool = m_pool;
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ai.commandBufferCount = 1;
VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
if (m_fns.AllocateCommandBuffers(m_device, &ai, &s.cmd) != VK_SUCCESS ||
m_fns.CreateFence(m_device, &fi, nullptr, &s.fence) != VK_SUCCESS ||
m_fns.CreateSemaphore(m_device, &si, nullptr, &s.present_sem) != VK_SUCCESS)
{
if (m_fns.AllocateCommandBuffers(m_device, &ai, &s.cmd) != VK_SUCCESS
|| m_fns.CreateFence(m_device, &fi, nullptr, &s.fence) != VK_SUCCESS
|| m_fns.CreateSemaphore(m_device, &si, nullptr, &s.present_sem) != VK_SUCCESS) {
return false;
}
}
@@ -109,22 +91,18 @@ bool VkCapture::ensure_slot_pool()
bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
{
const VkDeviceSize need = static_cast<VkDeviceSize>(w) * h * 4;
if (s.staging != VK_NULL_HANDLE && s.size == need)
{
if (s.staging != VK_NULL_HANDLE && s.size == need) {
return true;
}
if (s.mapped != nullptr)
{
if (s.mapped != nullptr) {
m_fns.UnmapMemory(m_device, s.mem);
s.mapped = nullptr;
}
if (s.staging != VK_NULL_HANDLE)
{
if (s.staging != VK_NULL_HANDLE) {
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
s.staging = VK_NULL_HANDLE;
}
if (s.mem != VK_NULL_HANDLE)
{
if (s.mem != VK_NULL_HANDLE) {
m_fns.FreeMemory(m_device, s.mem, nullptr);
s.mem = VK_NULL_HANDLE;
}
@@ -133,16 +111,14 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
bci.size = need;
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (m_fns.CreateBuffer(m_device, &bci, nullptr, &s.staging) != VK_SUCCESS)
{
if (m_fns.CreateBuffer(m_device, &bci, nullptr, &s.staging) != VK_SUCCESS) {
return false;
}
VkMemoryRequirements mr{};
m_fns.GetBufferMemoryRequirements(m_device, s.staging, &mr);
std::uint32_t mt = 0;
bool coherent = true;
if (!find_readback_memory(mr.memoryTypeBits, mt, coherent))
{
if (!find_readback_memory(mr.memoryTypeBits, mt, coherent)) {
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
s.staging = VK_NULL_HANDLE;
return false;
@@ -150,12 +126,10 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
mai.allocationSize = mr.size;
mai.memoryTypeIndex = mt;
if (m_fns.AllocateMemory(m_device, &mai, nullptr, &s.mem) != VK_SUCCESS ||
m_fns.BindBufferMemory(m_device, s.staging, s.mem, 0) != VK_SUCCESS ||
m_fns.MapMemory(m_device, s.mem, 0, VK_WHOLE_SIZE, 0, &s.mapped) != VK_SUCCESS)
{
if (s.mem != VK_NULL_HANDLE)
{
if (m_fns.AllocateMemory(m_device, &mai, nullptr, &s.mem) != VK_SUCCESS
|| m_fns.BindBufferMemory(m_device, s.staging, s.mem, 0) != VK_SUCCESS
|| m_fns.MapMemory(m_device, s.mem, 0, VK_WHOLE_SIZE, 0, &s.mapped) != VK_SUCCESS) {
if (s.mem != VK_NULL_HANDLE) {
m_fns.FreeMemory(m_device, s.mem, nullptr);
s.mem = VK_NULL_HANDLE;
}
@@ -170,38 +144,31 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
void VkCapture::free_slots()
{
for (Slot& s : m_slots)
{
if (s.mapped != nullptr)
{
for (Slot& s : m_slots) {
if (s.mapped != nullptr) {
m_fns.UnmapMemory(m_device, s.mem);
s.mapped = nullptr;
}
if (s.staging != VK_NULL_HANDLE)
{
if (s.staging != VK_NULL_HANDLE) {
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
s.staging = VK_NULL_HANDLE;
}
if (s.mem != VK_NULL_HANDLE)
{
if (s.mem != VK_NULL_HANDLE) {
m_fns.FreeMemory(m_device, s.mem, nullptr);
s.mem = VK_NULL_HANDLE;
}
if (s.present_sem != VK_NULL_HANDLE)
{
if (s.present_sem != VK_NULL_HANDLE) {
m_fns.DestroySemaphore(m_device, s.present_sem, nullptr);
s.present_sem = VK_NULL_HANDLE;
}
if (s.fence != VK_NULL_HANDLE)
{
if (s.fence != VK_NULL_HANDLE) {
m_fns.DestroyFence(m_device, s.fence, nullptr);
s.fence = VK_NULL_HANDLE;
}
s.size = 0;
s.busy.store(false, std::memory_order_relaxed);
}
if (m_pool != VK_NULL_HANDLE)
{
if (m_pool != VK_NULL_HANDLE) {
m_fns.DestroyCommandPool(m_device, m_pool, nullptr); // frees the command buffers
m_pool = VK_NULL_HANDLE;
}
@@ -210,25 +177,22 @@ void VkCapture::free_slots()
// --- D3D11 shared texture (reaper thread only; shutdown releases after the reaper has joined) ------
bool VkCapture::ensure_d3d()
{
if (m_d3d != nullptr)
{
if (m_d3d != nullptr) {
return true;
}
return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, &m_d3d, nullptr, &m_d3d_ctx)) &&
m_d3d != nullptr;
return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
&m_d3d, nullptr, &m_d3d_ctx))
&& m_d3d != nullptr;
}
void VkCapture::release_d3d()
{
m_shared.release();
if (m_d3d_ctx != nullptr)
{
if (m_d3d_ctx != nullptr) {
m_d3d_ctx->Release();
m_d3d_ctx = nullptr;
}
if (m_d3d != nullptr)
{
if (m_d3d != nullptr) {
m_d3d->Release();
m_d3d = nullptr;
}
@@ -238,8 +202,7 @@ void VkCapture::release_d3d()
void VkCapture::init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns,
unsigned long pid, std::function<void(std::uint32_t, std::uint32_t)> on_frame)
{
if (m_device != VK_NULL_HANDLE)
{
if (m_device != VK_NULL_HANDLE) {
return; // already initialised
}
m_phys = phys;
@@ -249,8 +212,7 @@ void VkCapture::init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue
m_pid = pid;
m_on_frame = std::move(on_frame);
m_stop = false;
if (!ensure_slot_pool())
{
if (!ensure_slot_pool()) {
return; // leave m_device set but the pool empty -> present() will fail format/staging checks
}
m_reaper = std::thread([this] { reaper_main(); });
@@ -261,8 +223,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
{
const bool bgra = fmt == VK_FORMAT_B8G8R8A8_UNORM || fmt == VK_FORMAT_B8G8R8A8_SRGB;
const bool rgba = fmt == VK_FORMAT_R8G8B8A8_UNORM || fmt == VK_FORMAT_R8G8B8A8_SRGB;
if (m_device == VK_NULL_HANDLE || m_pool == VK_NULL_HANDLE || (!bgra && !rgba))
{
if (m_device == VK_NULL_HANDLE || m_pool == VK_NULL_HANDLE || (!bgra && !rgba)) {
return false;
}
// No time-based throttle here: capture follows the game's present rate, which vsync paces (if the
@@ -272,23 +233,19 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
// Pick a slot whose previous capture the reaper has finished. None free -> the reaper is behind,
// so skip this frame (the game keeps its rate; the mirror just drops a frame).
int idx = -1;
for (int n = 0; n < kSlots; ++n)
{
for (int n = 0; n < kSlots; ++n) {
const int cand = (m_next + n) % kSlots;
if (!m_slots[cand].busy.load(std::memory_order_acquire))
{
if (!m_slots[cand].busy.load(std::memory_order_acquire)) {
idx = cand;
break;
}
}
if (idx < 0)
{
if (idx < 0) {
return false;
}
m_next = (idx + 1) % kSlots;
Slot& s = m_slots[idx];
if (!ensure_staging(s, w, h))
{
if (!ensure_staging(s, w, h)) {
return false;
}
@@ -306,17 +263,17 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = image;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
m_fns.CmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 0, nullptr, 0, nullptr, 1, &b);
m_fns.CmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0,
nullptr, 0, nullptr, 1, &b);
};
image_barrier(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT);
image_barrier(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_READ_BIT,
VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.imageExtent = {w, h, 1};
m_fns.CmdCopyImageToBuffer(s.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, s.staging, 1, &region);
image_barrier(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_READ_BIT);
image_barrier(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_MEMORY_READ_BIT);
m_fns.EndCommandBuffer(s.cmd);
std::vector<VkPipelineStageFlags> stages(wait_count, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
@@ -328,8 +285,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
si.pCommandBuffers = &s.cmd;
si.signalSemaphoreCount = 1;
si.pSignalSemaphores = &s.present_sem;
if (m_fns.QueueSubmit(m_queue, 1, &si, s.fence) != VK_SUCCESS)
{
if (m_fns.QueueSubmit(m_queue, 1, &si, s.fence) != VK_SUCCESS) {
return false;
}
s.w = w;
@@ -348,8 +304,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
void VkCapture::reap_slot(Slot& s)
{
m_fns.WaitForFences(m_device, 1, &s.fence, VK_TRUE, UINT64_MAX);
if (!s.coherent)
{
if (!s.coherent) {
VkMappedMemoryRange r{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
r.memory = s.mem;
r.offset = 0;
@@ -359,43 +314,35 @@ void VkCapture::reap_slot(Slot& s)
const bool bgra = s.fmt == VK_FORMAT_B8G8R8A8_UNORM || s.fmt == VK_FORMAT_B8G8R8A8_SRGB;
const size_t row = static_cast<size_t>(s.w) * 4;
if (m_rgba.size() != row * s.h)
{
if (m_rgba.size() != row * s.h) {
m_rgba.resize(row * s.h);
}
const auto* src = static_cast<const unsigned char*>(s.mapped);
for (std::uint32_t y = 0; y < s.h; ++y)
{
for (std::uint32_t y = 0; y < s.h; ++y) {
const unsigned char* in = src + static_cast<size_t>(y) * row;
unsigned char* o = m_rgba.data() + static_cast<size_t>(y) * row;
if (bgra)
{
for (std::uint32_t x = 0; x < s.w; ++x)
{
if (bgra) {
for (std::uint32_t x = 0; x < s.w; ++x) {
o[x * 4 + 0] = in[x * 4 + 2];
o[x * 4 + 1] = in[x * 4 + 1];
o[x * 4 + 2] = in[x * 4 + 0];
o[x * 4 + 3] = 255;
}
}
else
{
} else {
std::memcpy(o, in, row);
}
}
bool published = false;
if (ensure_d3d() && m_shared.ensure(m_d3d, s.w, s.h, DXGI_FORMAT_R8G8B8A8_UNORM, m_pid, "vk") &&
m_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
if (ensure_d3d() && m_shared.ensure(m_d3d, s.w, s.h, DXGI_FORMAT_R8G8B8A8_UNORM, m_pid, "vk")
&& m_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
m_d3d_ctx->UpdateSubresource(m_shared.texture(), 0, nullptr, m_rgba.data(), static_cast<UINT>(row), 0);
m_d3d_ctx->Flush();
m_shared.mutex()->ReleaseSync(kVideoMutexKey);
published = true;
}
if (published)
{
if (published) {
{
std::lock_guard<std::mutex> lk(m_last_mutex);
m_last = m_rgba;
@@ -403,8 +350,7 @@ void VkCapture::reap_slot(Slot& s)
m_last_h = s.h;
}
m_published.fetch_add(1, std::memory_order_relaxed);
if (m_on_frame)
{
if (m_on_frame) {
m_on_frame(s.w, s.h);
}
}
@@ -415,14 +361,12 @@ void VkCapture::reap_slot(Slot& s)
void VkCapture::reaper_main()
{
for (;;)
{
for (;;) {
int idx;
{
std::unique_lock<std::mutex> lk(m_q_mutex);
m_q_cv.wait(lk, [this] { return m_stop || !m_pending.empty(); });
if (m_stop && m_pending.empty())
{
if (m_stop && m_pending.empty()) {
return;
}
idx = m_pending.front();
@@ -434,8 +378,7 @@ void VkCapture::reaper_main()
void VkCapture::shutdown()
{
if (m_reaper.joinable())
{
if (m_reaper.joinable()) {
{
std::lock_guard<std::mutex> lk(m_q_mutex);
m_stop = true;
@@ -445,10 +388,8 @@ void VkCapture::shutdown()
}
// The reaper is gone (no more submits/reads); drain any GPU work still referencing our
// resources, then free.
if (m_device != VK_NULL_HANDLE)
{
if (m_fns.DeviceWaitIdle != nullptr)
{
if (m_device != VK_NULL_HANDLE) {
if (m_fns.DeviceWaitIdle != nullptr) {
m_fns.DeviceWaitIdle(m_device);
}
free_slots();
@@ -463,8 +404,7 @@ void VkCapture::shutdown()
bool VkCapture::last_frame(std::vector<unsigned char>& out, std::uint32_t& w, std::uint32_t& h)
{
std::lock_guard<std::mutex> lk(m_last_mutex);
if (m_last.empty())
{
if (m_last.empty()) {
return false;
}
out = m_last;

View File

@@ -36,16 +36,13 @@
#include "shared_video_texture.hpp"
namespace coop::hook
{
namespace coop::hook {
class VkCapture
{
public:
class VkCapture {
public:
// Device entry points the read-back needs (resolved by the caller via the real
// vkGetDeviceProcAddr; GetPhysicalDeviceMemoryProperties is instance-level).
struct Fns
{
struct Fns {
PFN_vkGetDeviceQueue GetDeviceQueue;
PFN_vkCreateCommandPool CreateCommandPool;
PFN_vkDestroyCommandPool DestroyCommandPool;
@@ -84,8 +81,8 @@ public:
// Bind to the game's device + queue family and start the reaper thread. `pid` names the shared
// texture (video_share_name). `on_frame(w,h)` runs on the reaper thread after each frame is
// published (the caller does its own IPC / stat bookkeeping there). Idempotent-ish: call once.
void init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns,
unsigned long pid, std::function<void(std::uint32_t, std::uint32_t)> on_frame);
void init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns, unsigned long pid,
std::function<void(std::uint32_t, std::uint32_t)> on_frame);
bool active() const { return m_device != VK_NULL_HANDLE; }
@@ -107,11 +104,10 @@ public:
// Test seam: copy the most recently published RGBA frame out (tightly packed w*4). False if none.
bool last_frame(std::vector<unsigned char>& out, std::uint32_t& w, std::uint32_t& h);
private:
private:
static constexpr int kSlots = 4; // in-flight copies; also the present-semaphore reuse slack
struct Slot
{
struct Slot {
VkCommandBuffer cmd = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
VkSemaphore present_sem = VK_NULL_HANDLE;

View File

@@ -24,11 +24,9 @@
#include "hook_registry.hpp"
#include "vk_capture.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight present/create detours before remove frees the Vulkan state
// Capture gate. Unlike the other backends, the game caches our hk_vkQueuePresentKHR pointer at
@@ -65,8 +63,7 @@ std::uint32_t g_qfam = 0;
VkCapture g_cap; // the shared, off-present-thread read-back (same component the layer uses)
// Tracked swap chains (small; engines have one or two).
struct SwapInfo
{
struct SwapInfo {
VkSwapchainKHR sc;
VkFormat fmt;
std::uint32_t w;
@@ -95,10 +92,8 @@ PFN_vkVoidFunction real_gipa(VkInstance inst, const char* name)
// (copy out what you need before unlocking, since another thread can push_back and reallocate).
const SwapInfo* find_swap(VkSwapchainKHR sc)
{
for (const SwapInfo& s : g_swaps)
{
if (s.sc == sc)
{
for (const SwapInfo& s : g_swaps) {
if (s.sc == sc) {
return &s;
}
}
@@ -110,17 +105,15 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
DetourGate::Guard guard(g_gate); // keep the read-back resources alive for this whole detour
hook_note_call(g_id_present);
g_presents.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_present();
}
// Capture only the simple, common single-swapchain present; pass anything else through. The
// gate lets removal stop capture (and pass through to the real present) before it frees the
// read-back state, even though the game keeps calling this cached detour pointer.
if (g_capture_enabled.load(std::memory_order_acquire) && g_device != VK_NULL_HANDLE &&
pPresentInfo != nullptr && pPresentInfo->swapchainCount == 1)
{
if (g_capture_enabled.load(std::memory_order_acquire) && g_device != VK_NULL_HANDLE && pPresentInfo != nullptr
&& pPresentInfo->swapchainCount == 1) {
// Copy the matched swapchain's fields out under the lock, then capture without holding it (so
// the GPU submit can't block a concurrent create, and the SwapInfo* can't dangle on a realloc).
VkImage image = VK_NULL_HANDLE;
@@ -131,8 +124,7 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
std::scoped_lock lock(g_swaps_mutex);
const SwapInfo* s = find_swap(pPresentInfo->pSwapchains[0]);
const std::uint32_t idx = pPresentInfo->pImageIndices[0];
if (s != nullptr && idx < s->images.size())
{
if (s != nullptr && idx < s->images.size()) {
image = s->images[idx];
fmt = s->fmt;
w = s->w;
@@ -140,12 +132,10 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
matched = true;
}
}
if (matched)
{
if (matched) {
VkSemaphore chained = VK_NULL_HANDLE;
if (g_cap.present(image, fmt, w, h, pPresentInfo->pWaitSemaphores,
pPresentInfo->waitSemaphoreCount, chained))
{
if (g_cap.present(image, fmt, w, h, pPresentInfo->pWaitSemaphores, pPresentInfo->waitSemaphoreCount,
chained)) {
// Replace the present's wait with our chained semaphore (our submit consumed the
// originals and signals this one), so the present still orders after rendering.
VkPresentInfoKHR pi = *pPresentInfo;
@@ -159,12 +149,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
}
VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR* ci,
const VkAllocationCallbacks* alloc, VkSwapchainKHR* out)
const VkAllocationCallbacks* alloc, VkSwapchainKHR* out)
{
DetourGate::Guard guard(g_gate); // keep g_swaps stable while remove may be clearing it
const VkResult r = g_real_create_swapchain(device, ci, alloc, out);
if (r == VK_SUCCESS && out != nullptr && g_get_swapchain_images != nullptr)
{
if (r == VK_SUCCESS && out != nullptr && g_get_swapchain_images != nullptr) {
SwapInfo info{};
info.sc = *out;
info.fmt = ci->imageFormat;
@@ -178,12 +167,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateSwapchainKHR(VkDevice device, const Vk
std::scoped_lock lock(g_swaps_mutex);
// De-dup a recycled handle value, then bound growth (drop the oldest; the just-created
// active swapchain is newest and stays).
g_swaps.erase(std::remove_if(g_swaps.begin(), g_swaps.end(),
[&](const SwapInfo& e) { return e.sc == info.sc; }),
g_swaps.end());
g_swaps.erase(
std::remove_if(g_swaps.begin(), g_swaps.end(), [&](const SwapInfo& e) { return e.sc == info.sc; }),
g_swaps.end());
g_swaps.push_back(std::move(info));
if (g_swaps.size() > kMaxTrackedSwaps)
{
if (g_swaps.size() > kMaxTrackedSwaps) {
g_swaps.erase(g_swaps.begin());
}
}
@@ -236,8 +224,7 @@ void start_capture(VkDevice device)
// Reaper thread, after each frame is mirrored into the shared texture.
g_present_captured.store(true, std::memory_order_relaxed);
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
}
});
@@ -254,8 +241,7 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateDevice(VkPhysicalDevice phys, const Vk
g_device = *out;
g_qfam = ci->queueCreateInfoCount > 0 ? ci->pQueueCreateInfos[0].queueFamilyIndex : 0;
g_real_gdpa = reinterpret_cast<PFN_vkGetDeviceProcAddr>(real_gipa(g_instance, "vkGetDeviceProcAddr"));
g_real_create_swapchain =
reinterpret_cast<PFN_vkCreateSwapchainKHR>(g_real_gdpa(*out, "vkCreateSwapchainKHR"));
g_real_create_swapchain = reinterpret_cast<PFN_vkCreateSwapchainKHR>(g_real_gdpa(*out, "vkCreateSwapchainKHR"));
g_real_present = reinterpret_cast<PFN_vkQueuePresentKHR>(g_real_gdpa(*out, "vkQueuePresentKHR"));
start_capture(*out);
// Arm capture only once every real_* pointer + VkCapture is populated (release pairs with the
@@ -266,13 +252,12 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateDevice(VkPhysicalDevice phys, const Vk
return r;
}
VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* ci,
const VkAllocationCallbacks* alloc, VkInstance* out)
VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* ci, const VkAllocationCallbacks* alloc,
VkInstance* out)
{
auto real_create = reinterpret_cast<PFN_vkCreateInstance>(real_gipa(nullptr, "vkCreateInstance"));
const VkResult r = real_create(ci, alloc, out);
if (r == VK_SUCCESS && out != nullptr)
{
if (r == VK_SUCCESS && out != nullptr) {
g_instance = *out;
g_real_create_device = reinterpret_cast<PFN_vkCreateDevice>(real_gipa(*out, "vkCreateDevice"));
logf("vk: instance created -- intercepting device/swapchain/present");
@@ -282,14 +267,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* c
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetDeviceProcAddr(VkDevice device, const char* name)
{
if (name != nullptr)
{
if (std::strcmp(name, "vkQueuePresentKHR") == 0)
{
if (name != nullptr) {
if (std::strcmp(name, "vkQueuePresentKHR") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
}
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0)
{
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
}
}
@@ -298,22 +280,17 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetDeviceProcAddr(VkDevice device,
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance instance, const char* name)
{
if (name != nullptr)
{
if (std::strcmp(name, "vkGetInstanceProcAddr") == 0)
{
if (name != nullptr) {
if (std::strcmp(name, "vkGetInstanceProcAddr") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetInstanceProcAddr);
}
if (std::strcmp(name, "vkCreateInstance") == 0)
{
if (std::strcmp(name, "vkCreateInstance") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateInstance);
}
if (std::strcmp(name, "vkCreateDevice") == 0)
{
if (std::strcmp(name, "vkCreateDevice") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateDevice);
}
if (std::strcmp(name, "vkGetDeviceProcAddr") == 0)
{
if (std::strcmp(name, "vkGetDeviceProcAddr") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetDeviceProcAddr);
}
// vkGetInstanceProcAddr can also resolve device-level functions (the loader returns a
@@ -321,12 +298,10 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance ins
// swapchain entry points this way (rather than via vkGetDeviceProcAddr) would otherwise get
// the real loader pointer and bypass our capture, so intercept them here too. (Our detours
// gate on g_capture_enabled / g_device, so handing them out before the device exists is safe.)
if (std::strcmp(name, "vkQueuePresentKHR") == 0)
{
if (std::strcmp(name, "vkQueuePresentKHR") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
}
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0)
{
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) {
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
}
}
@@ -336,8 +311,7 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance ins
void* gipa_export_address()
{
HMODULE vk = GetModuleHandleW(L"vulkan-1.dll");
if (vk == nullptr)
{
if (vk == nullptr) {
return nullptr; // not a Vulkan process (yet)
}
return reinterpret_cast<void*>(GetProcAddress(vk, "vkGetInstanceProcAddr"));
@@ -349,17 +323,14 @@ bool install_vk_hooks(IpcClient& ipc)
{
g_ipc = &ipc;
g_pid = GetCurrentProcessId();
if (g_hk_gipa.enabled())
{
if (g_hk_gipa.enabled()) {
return true; // already installed (persistent hook; re-install below re-enables it)
}
if (g_id_present < 0)
{
if (g_id_present < 0) {
g_id_present = hook_register("vkQueuePresentKHR", HookSubsys_Video);
}
void* gipa = gipa_export_address();
if (gipa == nullptr)
{
if (gipa == nullptr) {
hook_set_installed(g_id_present, false);
return false; // vulkan-1.dll not loaded; caller can retry once the game loads it
}
@@ -420,8 +391,7 @@ bool vk_injected_too_late()
// hook (we're not in the chain). A game we hooked early always trips hk_vkCreateDevice
// (g_device != null) well within the grace window, even before it presents. The host shows
// the "relaunch with Auto-attach / Vulkan layer" banner on this.
if (GetModuleHandleW(L"vulkan-1.dll") == nullptr || g_device != VK_NULL_HANDLE || g_install_tick == 0)
{
if (GetModuleHandleW(L"vulkan-1.dll") == nullptr || g_device != VK_NULL_HANDLE || g_install_tick == 0) {
return false;
}
return (GetTickCount64() - g_install_tick) > 4000;

View File

@@ -10,8 +10,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Installs the Vulkan capture hook (inline-hooks vkGetInstanceProcAddr). `ipc` must outlive the
// hook. Returns true if vulkan-1.dll is loaded and the export was hooked; false otherwise, so

View File

@@ -11,8 +11,7 @@
#include <windows.h>
namespace coop::hook
{
namespace coop::hook {
// Read a COM object's vtable slot (e.g. to grab a method's address off a probe object for an
// inline hook).
@@ -21,19 +20,16 @@ inline void* vtable_method(void* obj, unsigned index)
return (*reinterpret_cast<void***>(obj))[index];
}
class VtableHook
{
public:
class VtableHook {
public:
bool install(void* com_object, unsigned index, void* detour)
{
if (m_vtable != nullptr)
{
if (m_vtable != nullptr) {
return true; // already installed (shared vtable covers every instance)
}
auto** vtable = *reinterpret_cast<void***>(com_object);
DWORD old_protect = 0;
if (!VirtualProtect(&vtable[index], sizeof(void*), PAGE_READWRITE, &old_protect))
{
if (!VirtualProtect(&vtable[index], sizeof(void*), PAGE_READWRITE, &old_protect)) {
return false;
}
m_original = vtable[index];
@@ -46,13 +42,11 @@ public:
void remove()
{
if (m_vtable == nullptr)
{
if (m_vtable == nullptr) {
return;
}
DWORD old_protect = 0;
if (VirtualProtect(&m_vtable[m_index], sizeof(void*), PAGE_READWRITE, &old_protect))
{
if (VirtualProtect(&m_vtable[m_index], sizeof(void*), PAGE_READWRITE, &old_protect)) {
m_vtable[m_index] = m_original;
VirtualProtect(&m_vtable[m_index], sizeof(void*), old_protect, &old_protect);
}
@@ -63,10 +57,14 @@ public:
m_index = 0;
}
template <typename Fn> Fn original() const { return reinterpret_cast<Fn>(m_original); }
template <typename Fn>
Fn original() const
{
return reinterpret_cast<Fn>(m_original);
}
explicit operator bool() const { return m_vtable != nullptr; }
private:
private:
void** m_vtable = nullptr;
unsigned m_index = 0;
void* m_original = nullptr;

View File

@@ -13,11 +13,9 @@
#include "hook_install.hpp"
#include "hook_registry.hpp"
namespace coop::hook
{
namespace coop::hook {
namespace
{
namespace {
DetourGate g_gate; // drains in-flight XInput detours before remove nulls the IPC pointer
@@ -41,16 +39,13 @@ std::array<CoopPadState, kMaxPads> g_cache;
void refresh_cache()
{
if (g_ipc == nullptr)
{
if (g_ipc == nullptr) {
return;
}
CoopPadState pads[kMaxPads];
std::uint32_t count = 0;
if (g_ipc->snapshot(pads, count))
{
for (std::uint32_t i = 0; i < kMaxPads; ++i)
{
if (g_ipc->snapshot(pads, count)) {
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
g_cache[i] = pads[i];
}
}
@@ -71,31 +66,26 @@ void fill_gamepad(const CoopPadState& pad, XINPUT_GAMEPAD& out)
// (documented) XInputGetState, which must not report it.
DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
{
if (state == nullptr || user_index >= kMaxPads)
{
if (state == nullptr || user_index >= kMaxPads) {
return ERROR_DEVICE_NOT_CONNECTED;
}
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_state_query(user_index); // proves to the host the game is polling us
}
refresh_cache();
const CoopPadState& pad = g_cache[user_index];
if (!pad.connected)
{
if (!pad.connected) {
return ERROR_DEVICE_NOT_CONNECTED;
}
XINPUT_STATE result = {};
result.dwPacketNumber = pad.packet;
fill_gamepad(pad, result.Gamepad);
if (!keep_guide)
{
if (!keep_guide) {
result.Gamepad.wButtons &= ~kGuideButton;
}
*state = result;
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_read_state(user_index, pad); // round-trip: what the game just read
}
return ERROR_SUCCESS;
@@ -119,17 +109,14 @@ DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_
{
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
hook_note_call(g_id_getcaps);
if (caps == nullptr || user_index >= kMaxPads)
{
if (caps == nullptr || user_index >= kMaxPads) {
return ERROR_DEVICE_NOT_CONNECTED;
}
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->note_caps_query(user_index);
}
refresh_cache();
if (!g_cache[user_index].connected)
{
if (!g_cache[user_index].connected) {
return ERROR_DEVICE_NOT_CONNECTED;
}
@@ -156,12 +143,10 @@ DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* vibration)
{
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
hook_note_call(g_id_setstate);
if (user_index >= kMaxPads || !g_cache[user_index].connected)
{
if (user_index >= kMaxPads || !g_cache[user_index].connected) {
return ERROR_DEVICE_NOT_CONNECTED;
}
if (g_ipc != nullptr && vibration != nullptr)
{
if (g_ipc != nullptr && vibration != nullptr) {
g_ipc->note_rumble(user_index, vibration->wLeftMotorSpeed, vibration->wRightMotorSpeed);
}
return ERROR_SUCCESS;
@@ -170,12 +155,10 @@ DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* vibration)
// `name` is a GetProcAddress LPCSTR: an export name, or MAKEINTRESOURCEA(ordinal).
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
{
if (module == nullptr)
{
if (module == nullptr) {
return;
}
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
{
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name))) {
g_hooks.emplace_back();
install_inline(g_hooks.back(), target, detour); // assign-then-enable (no install race)
hook_set_installed(registry_id, true);
@@ -186,8 +169,7 @@ void hook_export(HMODULE module, const char* name, void* detour, int registry_id
bool install_xinput_hooks(IpcClient& ipc)
{
if (!g_hooks.empty())
{
if (!g_hooks.empty()) {
return true; // already installed
}
g_ipc = &ipc;
@@ -201,22 +183,18 @@ bool install_xinput_hooks(IpcClient& ipc)
// A process generally loads exactly one of these, but hook every one that is
// present so we don't miss the one the game actually calls.
const wchar_t* modules[] = {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinputuap.dll"};
for (const wchar_t* name : modules)
{
for (const wchar_t* name : modules) {
HMODULE module = GetModuleHandleW(name);
if (module == nullptr)
{
if (module == nullptr) {
continue;
}
hook_export(module, "XInputGetState", reinterpret_cast<void*>(&hk_XInputGetState), g_id_getstate);
hook_export(module, MAKEINTRESOURCEA(100), reinterpret_cast<void*>(&hk_XInputGetStateEx),
g_id_getstateex); // XInputGetStateEx is exported by ordinal only
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities),
g_id_getcaps);
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities), g_id_getcaps);
hook_export(module, "XInputSetState", reinterpret_cast<void*>(&hk_XInputSetState), g_id_setstate);
}
if (!g_hooks.empty())
{
if (!g_hooks.empty()) {
g_ipc->mark_attached();
return true;
}
@@ -229,18 +207,16 @@ void remove_xinput_hooks()
// before nulling the IPC pointer they read. The XInput detours return synthesized pad state and
// never call the trampoline, so (unlike the present/MKB hooks) destroying the vector after the
// drain is safe -- there's no live trampoline a stale detour could jump through.
for (auto& h : g_hooks)
{
for (auto& h : g_hooks) {
disable_for_removal(h);
}
hook_set_installed(g_id_getstate, false);
hook_set_installed(g_id_getstateex, false);
hook_set_installed(g_id_getcaps, false);
hook_set_installed(g_id_setstate, false);
g_gate.drain(); // wait for any in-flight detour before nulling the IPC pointer it reads
g_gate.drain(); // wait for any in-flight detour before nulling the IPC pointer it reads
g_hooks.clear(); // no detour in-flight or able to start now -> safe to free the trampolines
if (g_ipc != nullptr)
{
if (g_ipc != nullptr) {
g_ipc->mark_detached();
}
g_ipc = nullptr;

View File

@@ -4,8 +4,7 @@
#include "ipc_client.hpp"
namespace coop::hook
{
namespace coop::hook {
// Locates the loaded XInput module(s) and hooks the state/capability entry
// points. `ipc` must outlive the hooks. Returns true if at least one module was