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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
#include "ipc_client.hpp"
|
||||
|
||||
namespace coop::hook
|
||||
{
|
||||
namespace coop::hook {
|
||||
|
||||
bool install_mkb_hooks(IpcClient& ipc);
|
||||
void remove_mkb_hooks();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ®ion);
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user