From 30eccf749da0a2b9cbf8d5b7e958cb33016c933f Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 12 Jul 2026 11:52:53 +0200 Subject: [PATCH] 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. --- .clang-format | 5 + common/include/coop/audio_correlate.hpp | 160 +++---- common/include/coop/audio_ring.hpp | 32 +- common/include/coop/dpi.hpp | 6 +- common/include/coop/log_ring.hpp | 41 +- common/include/coop/protocol.hpp | 122 +++--- common/include/coop/shared_memory.hpp | 57 +-- common/include/coop/tone_analysis.hpp | 171 +++----- common/include/coop/tool_paths.hpp | 15 +- common/include/coop/wav.hpp | 40 +- hook/src/audio_hook.cpp | 319 +++++--------- hook/src/audio_hook.hpp | 3 +- hook/src/d3d9_hook.cpp | 102 ++--- hook/src/d3d9_hook.hpp | 3 +- hook/src/debug_log.cpp | 37 +- hook/src/debug_log.hpp | 6 +- hook/src/dllmain.cpp | 112 ++--- hook/src/find_window.hpp | 15 +- hook/src/focus_spoof.cpp | 91 ++-- hook/src/focus_spoof.hpp | 3 +- hook/src/hook_guard.hpp | 41 +- hook/src/hook_install.hpp | 6 +- hook/src/hook_registry.cpp | 33 +- hook/src/hook_registry.hpp | 3 +- hook/src/ipc_client.hpp | 98 ++--- hook/src/mkb_hook.cpp | 211 ++++----- hook/src/mkb_hook.hpp | 3 +- hook/src/opengl_hook.cpp | 90 ++-- hook/src/opengl_hook.hpp | 3 +- hook/src/present_hook.cpp | 340 +++++---------- hook/src/present_hook.hpp | 3 +- hook/src/rate_estimator.hpp | 65 +-- hook/src/shared_video_texture.hpp | 57 +-- hook/src/vk_capture.cpp | 200 +++------ hook/src/vk_capture.hpp | 20 +- hook/src/vk_hook.cpp | 106 ++--- hook/src/vk_hook.hpp | 3 +- hook/src/vtable_hook.hpp | 28 +- hook/src/xinput_hook.cpp | 74 ++-- hook/src/xinput_hook.hpp | 3 +- host/src/audio/audio_format_verifier.cpp | 65 +-- host/src/audio/audio_format_verifier.hpp | 20 +- host/src/audio/audio_loopback.cpp | 316 +++++--------- host/src/audio/audio_loopback.hpp | 54 +-- host/src/audio/audio_mix.hpp | 29 +- host/src/audio/audio_overrides.cpp | 48 +-- host/src/audio/audio_overrides.hpp | 28 +- host/src/audio/process_loopback_capture.cpp | 156 +++---- host/src/audio/process_loopback_capture.hpp | 25 +- host/src/audio/render_pacer.hpp | 20 +- host/src/audio_panel.cpp | 180 +++----- host/src/audio_panel.hpp | 71 +-- host/src/capture/dxgi_format.hpp | 6 +- host/src/capture/frame_renderer.cpp | 29 +- host/src/capture/frame_renderer.hpp | 10 +- host/src/capture/keyed_mutex.hpp | 3 +- host/src/capture/shared_texture.cpp | 69 ++- host/src/capture/shared_texture.hpp | 35 +- host/src/capture/window_capture.cpp | 90 ++-- host/src/capture/window_capture.hpp | 35 +- host/src/capture_panel.cpp | 158 +++---- host/src/capture_panel.hpp | 80 ++-- host/src/controllers_panel.cpp | 85 ++-- host/src/controllers_panel.hpp | 25 +- host/src/d3d11_window.cpp | 130 ++---- host/src/d3d11_window.hpp | 42 +- host/src/imgui_layer.cpp | 18 +- host/src/imgui_layer.hpp | 10 +- host/src/inject/dll_probe.cpp | 12 +- host/src/inject/dll_probe.hpp | 3 +- host/src/inject/injector.cpp | 67 +-- host/src/inject/injector.hpp | 9 +- host/src/inject/mkb_forward.cpp | 221 +++++----- host/src/inject/mkb_forward.hpp | 3 +- host/src/inject/mkb_map.hpp | 26 +- host/src/inject/process_list.cpp | 15 +- host/src/inject/process_list.hpp | 6 +- host/src/inject/window_list.cpp | 27 +- host/src/inject/window_list.hpp | 14 +- host/src/injection_panel.cpp | 269 ++++-------- host/src/injection_panel.hpp | 95 ++-- host/src/input/input_source.hpp | 14 +- host/src/input/input_worker.cpp | 41 +- host/src/input/input_worker.hpp | 20 +- host/src/input/steam_input_source.cpp | 66 +-- host/src/input/steam_input_source.hpp | 30 +- host/src/input/xinput_source.cpp | 16 +- host/src/input/xinput_source.hpp | 20 +- host/src/ipc/ipc_server.cpp | 66 +-- host/src/ipc/ipc_server.hpp | 49 +-- host/src/log_panel.cpp | 27 +- host/src/log_panel.hpp | 13 +- host/src/main.cpp | 187 +++----- host/src/test_harness.cpp | 9 +- host/src/test_harness.hpp | 15 +- host/src/ui/app_chrome.cpp | 81 ++-- host/src/ui/app_chrome.hpp | 62 +-- host/src/ui/text_match.hpp | 9 +- host/src/util/utf8.hpp | 9 +- host/src/vk_layer_setup.cpp | 27 +- host/src/vk_layer_setup.hpp | 3 +- tests/audio_correlation_test.cpp | 69 ++- tests/audio_hook_test.cpp | 80 ++-- tests/audio_loopback_test.cpp | 49 +-- tests/audio_mix_test.cpp | 15 +- tests/audio_overrides_test.cpp | 13 +- tests/audio_ring_test.cpp | 27 +- tests/audio_verify_test.cpp | 104 ++--- tests/d3d9_hook_test.cpp | 53 +-- tests/detour_gate_test.cpp | 31 +- tests/dinput_hook_test.cpp | 50 +-- tests/dpi_test.cpp | 6 +- tests/dx12_present_hook_test.cpp | 77 ++-- tests/focus_spoof_test.cpp | 21 +- tests/hook_install_test.cpp | 12 +- tests/hook_selftest.cpp | 36 +- tests/ipc_server_test.cpp | 24 +- tests/keyed_mutex_test.cpp | 6 +- tests/log_ring_test.cpp | 53 +-- tests/mkb_map_test.cpp | 9 +- tests/mkb_ring_test.cpp | 33 +- tests/mock_game_test.cpp | 379 ++++++---------- tests/opengl_hook_test.cpp | 61 +-- tests/present_hook_test.cpp | 68 +-- tests/present_overhead.hpp | 6 +- tests/protocol_test.cpp | 27 +- tests/rate_estimator_test.cpp | 46 +- tests/render_pacer_test.cpp | 65 +-- tests/shared_memory_test.cpp | 6 +- tests/srgb_format_test.cpp | 6 +- tests/text_util_test.cpp | 9 +- tests/tone_analysis_test.cpp | 45 +- tests/tool_paths_test.cpp | 18 +- tests/ui_fit_test.cpp | 22 +- tests/vk_capture_perf_test.cpp | 106 ++--- tests/wav_test.cpp | 31 +- tools/audio_probe/main.cpp | 130 ++---- tools/audio_tone/main.cpp | 22 +- tools/audio_tone/tone_source.hpp | 132 ++---- tools/audio_validate/main.cpp | 454 ++++++++------------ tools/inject_helper/main.cpp | 36 +- tools/input_probe/main.cpp | 76 ++-- tools/mock_game/main.cpp | 84 ++-- tools/mock_game/render_backend.cpp | 24 +- tools/mock_game/render_backend.hpp | 14 +- tools/mock_game/render_dx09.cpp | 47 +- tools/mock_game/render_dx10.cpp | 43 +- tools/mock_game/render_dx11.cpp | 41 +- tools/mock_game/render_dx12.cpp | 63 +-- tools/mock_game/render_gl.cpp | 36 +- tools/mock_game/render_vk.cpp | 97 ++--- tools/sh_concurrency_repro/main.cpp | 32 +- tools/steam_input_probe/main.cpp | 24 +- tools/vk_validate/main.cpp | 158 +++---- vk_layer/coop_vk_layer.cpp | 233 ++++------ 155 files changed, 3333 insertions(+), 6171 deletions(-) diff --git a/.clang-format b/.clang-format index 5518a39..690cadd 100644 --- a/.clang-format +++ b/.clang-format @@ -16,4 +16,9 @@ AlwaysBreakTemplateDeclarations: true BreakBeforeBinaryOperators: NonAssignment ConstructorInitializerAllOnOneLineOrOnePerLine: true PointerAlignment: Left + +# Windows include order is load-bearing (windows.h must precede tlhelp32.h / mmreg.h / +# xinput.h / dinput.h; winsock2.h must precede windows.h), so leave include order alone +# rather than let an alphabetical sort break the build. +SortIncludes: false ... diff --git a/common/include/coop/audio_correlate.hpp b/common/include/coop/audio_correlate.hpp index 750dc3a..74e79b6 100644 --- a/common/include/coop/audio_correlate.hpp +++ b/common/include/coop/audio_correlate.hpp @@ -24,8 +24,7 @@ #include #include -namespace coop -{ +namespace coop { // The standard sample rates a shared-mode WASAPI stream realistically uses. Candidates are this // set; a non-standard true rate is out of scope (and would show as low-confidence either way). @@ -35,30 +34,25 @@ inline const std::vector& standard_audio_rates() return rates; } -struct RateCorrelation -{ - bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up) - unsigned rate = 0; // best candidate rate (Hz) - double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect) +struct RateCorrelation { + bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up) + unsigned rate = 0; // best candidate rate (Hz) + double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect) double runner_up = 0.0; // score of the second-best candidate (for separation) }; -namespace correlate_detail -{ +namespace correlate_detail { // Average interleaved float frames down to a single mono channel. inline void downmix(const float* interleaved, std::size_t frames, unsigned channels, std::vector& out) { out.resize(frames); - if (channels == 0) - { + if (channels == 0) { channels = 1; } - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { float sum = 0.0f; - for (unsigned c = 0; c < channels; ++c) - { + for (unsigned c = 0; c < channels; ++c) { sum += interleaved[i * channels + c]; } out[i] = sum / static_cast(channels); @@ -66,24 +60,20 @@ inline void downmix(const float* interleaved, std::size_t frames, unsigned chann } // Linear-resample a mono signal from src_rate to dst_rate. -inline void resample_linear(const std::vector& in, unsigned src_rate, unsigned dst_rate, - std::vector& out) +inline void resample_linear(const std::vector& in, unsigned src_rate, unsigned dst_rate, std::vector& out) { - if (src_rate == 0 || dst_rate == 0 || in.empty()) - { + if (src_rate == 0 || dst_rate == 0 || in.empty()) { out.clear(); return; } - if (src_rate == dst_rate) - { + if (src_rate == dst_rate) { out = in; return; } const double step = static_cast(src_rate) / static_cast(dst_rate); const std::size_t out_n = static_cast(static_cast(in.size()) / step); out.resize(out_n); - for (std::size_t i = 0; i < out_n; ++i) - { + for (std::size_t i = 0; i < out_n; ++i) { const double pos = static_cast(i) * step; const std::size_t j = static_cast(pos); const double frac = pos - static_cast(j); @@ -97,29 +87,24 @@ inline void resample_linear(const std::vector& in, unsigned src_rate, uns // alignment search (the envelope/content alignment doesn't need full bandwidth). inline void decimate(const std::vector& in, unsigned rate, unsigned corr_rate, std::vector& out) { - if (rate <= corr_rate || in.empty()) - { + if (rate <= corr_rate || in.empty()) { out = in; return; } const double factor = static_cast(rate) / static_cast(corr_rate); const std::size_t out_n = static_cast(static_cast(in.size()) / factor); out.resize(out_n); - for (std::size_t i = 0; i < out_n; ++i) - { + for (std::size_t i = 0; i < out_n; ++i) { const std::size_t lo = static_cast(static_cast(i) * factor); std::size_t hi = static_cast(static_cast(i + 1) * factor); - if (hi <= lo) - { + if (hi <= lo) { hi = lo + 1; } - if (hi > in.size()) - { + if (hi > in.size()) { hi = in.size(); } float sum = 0.0f; - for (std::size_t k = lo; k < hi; ++k) - { + for (std::size_t k = lo; k < hi; ++k) { sum += in[k]; } out[i] = sum / static_cast(hi - lo); @@ -133,29 +118,24 @@ inline double ncc(const std::vector& a, const std::vector& b, long { double sa = 0.0, sb = 0.0; std::size_t n = 0; - for (std::size_t i = start; i < start + len && i < a.size(); ++i) - { + for (std::size_t i = start; i < start + len && i < a.size(); ++i) { const long bi = static_cast(i) + lag; - if (bi < 0 || static_cast(bi) >= b.size()) - { + if (bi < 0 || static_cast(bi) >= b.size()) { continue; } sa += a[i]; sb += b[bi]; ++n; } - if (n < 8) - { + if (n < 8) { return 0.0; } const double ma = sa / static_cast(n); const double mb = sb / static_cast(n); double num = 0.0, da = 0.0, db = 0.0; - for (std::size_t i = start; i < start + len && i < a.size(); ++i) - { + for (std::size_t i = start; i < start + len && i < a.size(); ++i) { const long bi = static_cast(i) + lag; - if (bi < 0 || static_cast(bi) >= b.size()) - { + if (bi < 0 || static_cast(bi) >= b.size()) { continue; } const double xa = a[i] - ma; @@ -164,8 +144,7 @@ inline double ncc(const std::vector& a, const std::vector& b, long da += xa * xa; db += xb * xb; } - if (da < 1e-9 || db < 1e-9) - { + if (da < 1e-9 || db < 1e-9) { return 0.0; } return num / std::sqrt(da * db); @@ -188,11 +167,9 @@ inline double aligned_score(const std::vector& a, const std::vector best) - { + if (c > best) { best = c; best_lag = lag; } @@ -218,8 +195,7 @@ inline RateCorrelation correlate_rate(const std::vector& hook_mono, const { using namespace correlate_detail; RateCorrelation result; - if (hook_mono.empty() || loop_mono.empty() || device_rate == 0) - { + if (hook_mono.empty() || loop_mono.empty() || device_rate == 0) { return result; } constexpr unsigned kCorrRate = 8000; // alignment search rate (Nyquist 4 kHz -- plenty for content) @@ -230,19 +206,15 @@ inline RateCorrelation correlate_rate(const std::vector& hook_mono, const double best = -1.0, second = -1.0; unsigned best_rate = 0; std::vector resampled, hook_ds; - for (unsigned cand : candidates) - { + for (unsigned cand : candidates) { resample_linear(hook_mono, cand, device_rate, resampled); // treat hook as sampled at `cand` decimate(resampled, device_rate, kCorrRate, hook_ds); const double s = aligned_score(hook_ds, loop_ds, kCorrRate); - if (s > best) - { + if (s > best) { second = best; best = s; best_rate = cand; - } - else if (s > second) - { + } else if (s > second) { second = s; } } @@ -250,7 +222,8 @@ inline RateCorrelation correlate_rate(const std::vector& hook_mono, const result.rate = best_rate; result.score = best < 0.0 ? 0.0 : best; result.runner_up = second < 0.0 ? 0.0 : second; - result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation); + result.ok = + result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation); return result; } @@ -266,8 +239,7 @@ inline RateCorrelation correlate_rate(const std::vector& hook_mono, const inline constexpr unsigned kWaveFormatPcm = 1; // WAVE_FORMAT_PCM inline constexpr unsigned kWaveFormatFloat = 3; // WAVE_FORMAT_IEEE_FLOAT -struct LayoutCandidate -{ +struct LayoutCandidate { unsigned channels; unsigned bits; unsigned tag; // kWaveFormatPcm / kWaveFormatFloat @@ -278,16 +250,14 @@ struct LayoutCandidate inline const std::vector& standard_audio_layouts() { static const std::vector v = { - {2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat}, - {8, 32, kWaveFormatFloat}, {4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm}, - {1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm}, {8, 16, kWaveFormatPcm}, - {4, 16, kWaveFormatPcm}, + {2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat}, {8, 32, kWaveFormatFloat}, + {4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm}, {1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm}, + {8, 16, kWaveFormatPcm}, {4, 16, kWaveFormatPcm}, }; return v; } -struct FormatCorrelation -{ +struct FormatCorrelation { bool ok = false; unsigned rate = 0; unsigned channels = 0; @@ -302,15 +272,13 @@ struct FormatCorrelation // padding is stale staging-buffer bytes, so the host must extract the real `count*real_block` bytes // per buffer (and concatenate) before de-interleaving -- otherwise the padding scrambles the audio. // This carries that self-describing capture: `bytes` holds counts[i]*stride bytes per chunk. -struct ChunkedCapture -{ +struct ChunkedCapture { unsigned stride = 0; // bytes per frame as pushed (the guessed/device block_align) std::vector counts; // real frame count of each chunk std::vector bytes; // concatenated, counts[i]*stride bytes per chunk }; -namespace correlate_detail -{ +namespace correlate_detail { // De-interleave raw bytes under (channels/bits/tag) and average to mono float. inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const LayoutCandidate& fmt, std::vector& mono) @@ -318,33 +286,25 @@ inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const Layout mono.clear(); const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels; const unsigned bps = fmt.bits / 8; - if (bps == 0) - { + if (bps == 0) { return; } const std::size_t frame = static_cast(ch) * bps; const std::size_t frames = n / frame; mono.resize(frames); const bool is_float = fmt.tag == kWaveFormatFloat; - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { double sum = 0.0; - for (unsigned c = 0; c < ch; ++c) - { + for (unsigned c = 0; c < ch; ++c) { const std::uint8_t* p = bytes + i * frame + static_cast(c) * bps; float s = 0.0f; - if (is_float && fmt.bits == 32) - { + if (is_float && fmt.bits == 32) { std::memcpy(&s, p, 4); - } - else if (fmt.bits == 16) - { + } else if (fmt.bits == 16) { std::int16_t v; std::memcpy(&v, p, 2); s = v / 32768.0f; - } - else if (fmt.bits == 32) - { + } else if (fmt.bits == 32) { std::int32_t v; std::memcpy(&v, p, 4); s = static_cast(v / 2147483648.0); @@ -372,58 +332,50 @@ inline FormatCorrelation correlate_format(const ChunkedCapture& hook, const std: double min_margin = 0.04) { FormatCorrelation result; - if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0) - { + if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0) { return result; } double best = -1.0, second = -1.0; std::vector clean; std::vector hook_mono; - for (const LayoutCandidate& layout : layouts) - { + for (const LayoutCandidate& layout : layouts) { const unsigned real_block = layout.channels * (layout.bits / 8); - if (real_block == 0 || real_block > hook.stride) - { + if (real_block == 0 || real_block > hook.stride) { continue; // can't extract a frame larger than what was pushed (the guess is the max) } // Pull the real count*real_block bytes out of each padded chunk and concatenate -> contiguous // audio for this candidate layout (the padding, which is stale staging bytes, is dropped). clean.clear(); std::size_t off = 0; - for (std::uint32_t count : hook.counts) - { + for (std::uint32_t count : hook.counts) { const std::size_t chunk_bytes = static_cast(count) * hook.stride; const std::size_t take = static_cast(count) * real_block; - if (off + chunk_bytes <= hook.bytes.size()) - { + if (off + chunk_bytes <= hook.bytes.size()) { clean.insert(clean.end(), hook.bytes.begin() + off, hook.bytes.begin() + off + take); } off += chunk_bytes; } correlate_detail::decode_layout(clean.data(), clean.size(), layout, hook_mono); - if (hook_mono.size() < device_rate / 5) - { + if (hook_mono.size() < device_rate / 5) { continue; // this layout yields too little audio to judge } const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, device_rate, rates, /*min_score=*/0.0, - /*separation=*/1.0); - if (rc.score > best) - { + /*separation=*/1.0); + if (rc.score > best) { second = best; best = rc.score; result.rate = rc.rate; result.channels = layout.channels; result.bits = layout.bits; result.tag = layout.tag; - } - else if (rc.score > second) - { + } else if (rc.score > second) { second = rc.score; } } result.score = best < 0.0 ? 0.0 : best; result.runner_up = second < 0.0 ? 0.0 : second; - result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin); + result.ok = + result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin); return result; } diff --git a/common/include/coop/audio_ring.hpp b/common/include/coop/audio_ring.hpp index 7acb52f..a102ffd 100644 --- a/common/include/coop/audio_ring.hpp +++ b/common/include/coop/audio_ring.hpp @@ -17,8 +17,7 @@ #include #include -namespace coop -{ +namespace coop { // 'AURG' little-endian; sanity-checks the mapping before either side trusts it. inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u; @@ -30,8 +29,7 @@ inline constexpr std::uint32_t kAudioRingVersion = 2; // fields in the header. The host writes the fields then bumps op_seq; the hook applies // the command once per new op_seq. Lets the Audio panel re-measure a stream's rate or // override its format when detection is wrong/unrecoverable. -enum AudioRingOp : std::uint32_t -{ +enum AudioRingOp : std::uint32_t { AudioRingOp_None = 0, AudioRingOp_Remeasure = 1, // re-run the sample-rate measurement for this stream AudioRingOp_Override = 2, // adopt the op_rate/channels/bits/format_tag verbatim @@ -48,8 +46,7 @@ inline constexpr std::uint32_t kAudioRingCapacity = 1u << 20; // the format fields are written once by the producer *before* it publishes // format_valid (release), and read by the consumer *after* it observes // format_valid (acquire), so they need no atomicity of their own. -struct AudioRingHeader -{ +struct AudioRingHeader { std::uint32_t magic; std::uint32_t version; @@ -67,7 +64,7 @@ struct AudioRingHeader std::uint32_t sample_rate; std::uint32_t channels; std::uint32_t bits; - std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE) + std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE) std::uint32_t block_align; // bytes per frame (all channels) std::uint32_t capacity; // bytes in the trailing data region @@ -176,8 +173,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed); const std::uint64_t r = h.read_pos.load(std::memory_order_acquire); const std::uint32_t used = static_cast(w - r); - if (bytes > h.capacity - used) - { + if (bytes > h.capacity - used) { h.overruns.fetch_add(1, std::memory_order_relaxed); return false; } @@ -185,8 +181,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b const std::uint32_t off = static_cast(w % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(data + off, src, first); - if (bytes > first) - { + if (bytes > first) { std::memcpy(data, static_cast(src) + first, bytes - first); } h.write_pos.store(w + bytes, std::memory_order_release); @@ -222,8 +217,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t const std::uint32_t off = static_cast(r % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(dst, data + off, first); - if (bytes > first) - { + if (bytes > first) { std::memcpy(static_cast(dst) + first, data, bytes - first); } h.read_pos.store(r + bytes, std::memory_order_release); @@ -234,8 +228,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t // bumps op_seq (release) so the hook applies it exactly once. For a re-measure the // rate/channels/bits are ignored. inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint32_t rate = 0, - std::uint32_t channels = 0, std::uint32_t bits = 0, - std::uint32_t format_tag = 0) + std::uint32_t channels = 0, std::uint32_t bits = 0, std::uint32_t format_tag = 0) { h.op_kind = kind; h.op_rate = rate; @@ -246,8 +239,7 @@ inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint } // One operator command read back by the hook. -struct AudioRingOpCmd -{ +struct AudioRingOpCmd { std::uint32_t kind = AudioRingOp_None; std::uint32_t rate = 0; std::uint32_t channels = 0; @@ -261,8 +253,7 @@ struct AudioRingOpCmd inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_seq, AudioRingOpCmd& out) { const std::uint32_t seq = h.op_seq.load(std::memory_order_acquire); - if (seq == last_seq) - { + if (seq == last_seq) { return AudioRingOp_None; } last_seq = seq; @@ -280,8 +271,7 @@ inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_ inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0) { std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid); - if (index != 0) - { + if (index != 0) { name += L"_" + std::to_wstring(index); } return name; diff --git a/common/include/coop/dpi.hpp b/common/include/coop/dpi.hpp index 8fc6a23..231e469 100644 --- a/common/include/coop/dpi.hpp +++ b/common/include/coop/dpi.hpp @@ -9,8 +9,7 @@ #include // USER_DEFAULT_SCREEN_DPI (== 96, the 100%-scale baseline) -namespace coop -{ +namespace coop { // ImGui's built-in default font (ProggyClean) rasterizes at this pixel size at 100% scale. Named once // here so the DPI math scales from a single owned constant instead of a bare 13 sprinkled around. @@ -26,8 +25,7 @@ inline constexpr float kMaxUiScale = 8.0f; // callers never derive a zero-size font. inline float dpi_scale_from(unsigned dpi) { - if (dpi == 0) - { + if (dpi == 0) { dpi = USER_DEFAULT_SCREEN_DPI; } const float scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); diff --git a/common/include/coop/log_ring.hpp b/common/include/coop/log_ring.hpp index 10f3e7a..23ffaf0 100644 --- a/common/include/coop/log_ring.hpp +++ b/common/include/coop/log_ring.hpp @@ -15,8 +15,7 @@ #include #include -namespace coop -{ +namespace coop { // 'CLOG' little-endian. inline constexpr std::uint32_t kLogRingMagic = 0x474F4C43u; @@ -25,33 +24,30 @@ inline constexpr std::uint32_t kLogRingVersion = 1; // Per-pid mapping name, mirroring the other channels: coop_log_. inline constexpr wchar_t kLogRingPrefix[] = L"Local\\coop_log_"; -inline constexpr std::uint32_t kLogMsgLen = 192; // chars per line (incl. NUL) +inline constexpr std::uint32_t kLogMsgLen = 192; // chars per line (incl. NUL) inline constexpr std::uint32_t kLogCapacity = 1024; // ring records // Severity of a log line; drives the host Log window's colour. Stored in // LogRecord::level. Info is 0 so existing/zero-filled records read as Info. -enum LogLevel : std::uint32_t -{ +enum LogLevel : std::uint32_t { LogLevel_Info = 0, LogLevel_Warn = 1, LogLevel_Error = 2, }; -struct LogRecord -{ +struct LogRecord { std::atomic seq; // 0 = empty; else (global index + 1) once written std::uint32_t pid; - std::uint32_t level; // LogLevel + std::uint32_t level; // LogLevel std::uint64_t millis; // producer timestamp (GetTickCount64) char text[kLogMsgLen]; }; -struct LogRing -{ +struct LogRing { std::uint32_t magic; std::uint32_t version; - std::uint32_t capacity; // number of records - std::uint32_t msg_len; // kLogMsgLen (sanity) + std::uint32_t capacity; // number of records + std::uint32_t msg_len; // kLogMsgLen (sanity) std::atomic write_index; // total records ever claimed (free-running) std::uint8_t reserved[32]; // LogRecord records[capacity] follows immediately. @@ -83,13 +79,11 @@ inline void log_ring_init(LogRing& r, std::uint32_t capacity) inline bool log_ring_valid(const LogRing& r) { - return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 && - r.msg_len == kLogMsgLen; + return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 && r.msg_len == kLogMsgLen; } // Producer (hook): append a line at severity `level` (LogLevel). Multi-producer safe. -inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis, - const char* text) +inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis, const char* text) { const std::uint64_t idx = r.write_index.fetch_add(1, std::memory_order_acq_rel); LogRecord& rec = log_ring_records(&r)[idx % r.capacity]; @@ -115,23 +109,19 @@ template inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit) { const std::uint64_t w = r.write_index.load(std::memory_order_acquire); - if (w <= cursor) - { + if (w <= cursor) { return; } const std::uint64_t lo = (w > r.capacity) ? (w - r.capacity) : 0; std::uint64_t i = cursor < lo ? lo : cursor; // skip records already overwritten LogRecord* recs = log_ring_records(&r); - for (; i < w; ++i) - { + for (; i < w; ++i) { LogRecord& rec = recs[i % r.capacity]; const std::uint64_t s1 = rec.seq.load(std::memory_order_acquire); - if (s1 <= i) - { + if (s1 <= i) { break; // generation i not written yet (in-flight, or being overwritten); retry next call } - if (s1 != i + 1) - { + if (s1 != i + 1) { continue; // s1 > i+1: overwritten by a later generation before we got here; lost, skip } // Seqlock read: copy the record out, then re-check seq. A producer overwriting this slot stores @@ -142,8 +132,7 @@ inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit) snap.millis = rec.millis; std::memcpy(snap.text, rec.text, kLogMsgLen); std::atomic_thread_fence(std::memory_order_acquire); - if (rec.seq.load(std::memory_order_relaxed) == i + 1) - { + if (rec.seq.load(std::memory_order_relaxed) == i + 1) { emit(snap); // consistent snapshot } // else: overwritten while we copied -> skip (lost) diff --git a/common/include/coop/protocol.hpp b/common/include/coop/protocol.hpp index 4b64976..6c2d797 100644 --- a/common/include/coop/protocol.hpp +++ b/common/include/coop/protocol.hpp @@ -7,8 +7,7 @@ #include #include -namespace coop -{ +namespace coop { // Bump whenever the layout of SharedBlock or CoopPadState changes. The hook // refuses to attach to a host with a mismatched version. @@ -27,12 +26,11 @@ inline constexpr wchar_t kSharedMemoryPrefix[] = L"Local\\coop_ipc_"; // One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the // metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook // can memcpy the trailing region straight into an XINPUT_STATE. -struct CoopPadState -{ +struct CoopPadState { std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot std::uint8_t reserved[3]; - std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber - std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask + std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber + std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask std::uint8_t left_trigger; std::uint8_t right_trigger; std::int16_t thumb_lx; @@ -56,30 +54,27 @@ inline constexpr std::uint32_t kMaxAudioStreams = 4; // we injected (the common case) was never seen at Initialize, so its format starts as a // guess (the device mix format) and its true sample rate is measured from the render // cadence; a stream we watched get created carries its exact Initialize format. -enum AudioFormatState : std::uint32_t -{ - AudioFormat_Unknown = 0, // no format determined yet - AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize - AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured - AudioFormat_Measured = 3, // guessed rate measured (consensus on a standard rate); ch/bits assumed +enum AudioFormatState : std::uint32_t { + AudioFormat_Unknown = 0, // no format determined yet + AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize + AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured + AudioFormat_Measured = 3, // guessed rate measured (consensus on a standard rate); ch/bits assumed AudioFormat_LowConfidence = 4, // rate never reached consensus; best estimate published -- verify/override - AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel) + AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel) }; -struct AudioStreamInfo -{ - std::uint32_t is_primary; // 1 = the stream the hook captures/silences +struct AudioStreamInfo { + std::uint32_t is_primary; // 1 = the stream the hook captures/silences std::uint32_t sample_rate; std::uint16_t channels; std::uint16_t bits; - std::uint32_t format_tag; // WAVE_FORMAT_* of this stream + std::uint32_t format_tag; // WAVE_FORMAT_* of this stream std::uint64_t frames_rendered; std::uint32_t format_state; // AudioFormatState: how the format above was determined }; // Orthogonal hook subsystems the host can install/remove independently. -enum HookSubsystem : std::uint32_t -{ +enum HookSubsystem : std::uint32_t { HookSubsys_Input = 0, // XInput hooks (forward the guest pad) HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused) HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo) @@ -93,17 +88,15 @@ inline constexpr std::uint32_t kMaxHookEntries = 24; // One installed hook, for the Injection panel's hook list. POD diagnostics, like // AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok. -struct HookEntry -{ - char name[40]; // e.g. "XInputGetState" +struct HookEntry { + char name[40]; // e.g. "XInputGetState" std::uint32_t subsystem; // HookSubsystem std::uint32_t installed; // 1 if currently hooked std::uint64_t calls; // cumulative times the detour ran }; // Indices into HookStatus::focus_query_calls. -enum FocusApi : std::uint32_t -{ +enum FocusApi : std::uint32_t { FocusApi_Foreground = 0, // GetForegroundWindow FocusApi_Active = 1, // GetActiveWindow FocusApi_Focus = 2, // GetFocus @@ -115,17 +108,16 @@ enum FocusApi : std::uint32_t // polling, does it use the focus APIs, and does it read input through a // focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic // fields tolerate benign cross-process races. -struct HookStatus -{ - std::atomic heartbeat; // DLL bumps ~4x/sec while alive - std::atomic get_state_calls[kMaxPads]; // XInputGetState/Ex per slot - std::atomic get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot +struct HookStatus { + std::atomic heartbeat; // DLL bumps ~4x/sec while alive + std::atomic get_state_calls[kMaxPads]; // XInputGetState/Ex per slot + std::atomic get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot std::atomic focus_query_calls[FocusApi_Count]; // focus API calls, see FocusApi - std::uint32_t attached; // 1 once XInput hooks are installed - std::uint32_t focus_spoof; // 1 once focus spoofing is active - std::uint32_t game_pid; // the DLL's own pid (sanity check) - std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet) + std::uint32_t attached; // 1 once XInput hooks are installed + std::uint32_t focus_spoof; // 1 once focus spoofing is active + std::uint32_t game_pid; // the DLL's own pid (sanity check) + std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet) // Input-path diagnostics: which focus-gated mechanism (if any) the game uses. std::uint32_t raw_input_registered; // process has any Raw Input registration @@ -141,8 +133,8 @@ struct HookStatus // Audio render-hook diagnostics. Stream counting runs whenever the DLL is // injected, independent of whether audio mirroring is enabled, so a // multi-stream game is visible before/without turning the mirror on. - std::uint32_t audio_streams_seen; // distinct render clients ever created - AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary + std::uint32_t audio_streams_seen; // distinct render clients ever created + AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary // Hook registry: every individual hook the DLL has installed, with a running // call count. Lets the Injection panel list exactly what's hooked and how busy. @@ -163,8 +155,7 @@ struct HookStatus // Host -> hook control channel. The host requests which hook subsystems should be // installed; the hook reconciles each tick. 0 = install (the zero-filled default, // so a fresh mapping installs everything as before), 1 = remove. -struct HookControl -{ +struct HookControl { std::atomic subsystem_disabled[HookSubsys_Count]; // Cursor handling for cursor-clipping games (part of the Focus subsystem). @@ -181,17 +172,16 @@ struct HookControl // is the sole writer. `generation` bumps on every published frame (0 = nothing // shared yet); width/height/format describe the currently shared texture, so the // host reopens it whenever they change. The keyed mutex uses key 0 on both sides. -struct VideoShare -{ +struct VideoShare { std::atomic generation; // bumps per published frame; 0 = none yet std::uint32_t width; // shared texture dimensions / DXGI format std::uint32_t height; - std::uint32_t format; // DXGI_FORMAT of the shared texture - std::uint64_t present_calls; // cumulative Present() detours (diagnostic) - std::int64_t present_qpc; // QueryPerformanceCounter at the last publish - std::uint64_t frames_dropped; // cumulative captures skipped because the shared - // keyed mutex was busy (host mid-copy) -- a frame - // the game produced that never reached the mirror + std::uint32_t format; // DXGI_FORMAT of the shared texture + std::uint64_t present_calls; // cumulative Present() detours (diagnostic) + std::int64_t present_qpc; // QueryPerformanceCounter at the last publish + std::uint64_t frames_dropped; // cumulative captures skipped because the shared + // keyed mutex was busy (host mid-copy) -- a frame + // the game produced that never reached the mirror // present_calls / frames_dropped stay plain uint64_t (POD layout) but are read/written via // std::atomic_ref so the host's cross-process read isn't torn (an x86 DLL stores 64 bits in two // halves). Kept as fields, not std::atomic, only so the layout/offset asserts stay simple. @@ -203,8 +193,7 @@ struct VideoShare // the matching window messages to the game, and maintains a synthesized state the // GetAsyncKeyState/GetKeyboardState/GetCursorPos hooks report to polling games. -enum MkbEventType : std::uint32_t -{ +enum MkbEventType : std::uint32_t { Mkb_KeyDown = 0, // code = Win32 virtual-key Mkb_KeyUp = 1, // code = Win32 virtual-key Mkb_Char = 2, // code = UTF-16 code unit (WM_CHAR) @@ -213,8 +202,7 @@ enum MkbEventType : std::uint32_t Mkb_Wheel = 5, // code = signed wheel delta (WHEEL_DELTA units); x,y = game client px }; -struct MkbEvent -{ +struct MkbEvent { std::uint32_t type; // MkbEventType std::uint32_t code; // see per-type meaning above std::int32_t x; // game-client x (mouse events) @@ -227,8 +215,7 @@ static_assert(sizeof(MkbEvent) == 16, "MkbEvent must stay byte-identical across inline constexpr std::uint32_t kMkbQueueSize = 128; // Lock-free SPSC ring: host produces, hook consumes. Free-running 32-bit indices. -struct MkbRing -{ +struct MkbRing { std::atomic head; // producer (host) write position std::atomic tail; // consumer (hook) read position MkbEvent events[kMkbQueueSize]; @@ -244,8 +231,7 @@ inline constexpr std::uint64_t kVideoMutexKey = 0; // Top-level shared block. The host is the sole writer of pad state; the hook is // the sole reader. A seqlock (even = stable, odd = write in progress) lets the // reader grab a torn-free snapshot without a kernel lock on the hot path. -struct SharedBlock -{ +struct SharedBlock { std::uint32_t magic; std::uint32_t version; std::uint32_t pad_count; // number of populated slots, <= kMaxPads @@ -297,20 +283,17 @@ static_assert(sizeof(MkbRing) == 2056, "MkbRing size changed -- wire-protocol ch // Writer side: publish a fresh set of pad states. Called from the host. inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count) { - if (count > kMaxPads) - { + if (count > kMaxPads) { count = kMaxPads; } const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed); block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins std::atomic_thread_fence(std::memory_order_release); block.pad_count = count; - for (std::uint32_t i = 0; i < count; ++i) - { + for (std::uint32_t i = 0; i < count; ++i) { block.pads[i] = pads[i]; } - for (std::uint32_t i = count; i < kMaxPads; ++i) - { + for (std::uint32_t i = count; i < kMaxPads; ++i) { block.pads[i] = CoopPadState{}; } block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done @@ -320,26 +303,21 @@ inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint // if a write is in flight; bounded so a crashed writer can't hang the game. inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count) { - for (int attempt = 0; attempt < 64; ++attempt) - { + for (int attempt = 0; attempt < 64; ++attempt) { const std::uint32_t before = block.sequence.load(std::memory_order_acquire); - if (before & 1u) - { + if (before & 1u) { continue; // writer mid-update, retry } std::uint32_t count = block.pad_count; - if (count > kMaxPads) - { + if (count > kMaxPads) { count = kMaxPads; } - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { out[i] = block.pads[i]; } std::atomic_thread_fence(std::memory_order_acquire); const std::uint32_t after = block.sequence.load(std::memory_order_acquire); - if (before == after) - { + if (before == after) { out_count = count; return true; } @@ -354,8 +332,7 @@ inline bool push_mkb_event(MkbRing& ring, const MkbEvent& ev) { const std::uint32_t head = ring.head.load(std::memory_order_relaxed); const std::uint32_t tail = ring.tail.load(std::memory_order_acquire); - if (head - tail >= kMkbQueueSize) - { + if (head - tail >= kMkbQueueSize) { return false; // full -> drop (host should always drain faster than it fills) } ring.events[head & (kMkbQueueSize - 1)] = ev; @@ -368,8 +345,7 @@ inline bool pop_mkb_event(MkbRing& ring, MkbEvent& out) { const std::uint32_t tail = ring.tail.load(std::memory_order_relaxed); const std::uint32_t head = ring.head.load(std::memory_order_acquire); - if (tail == head) - { + if (tail == head) { return false; // empty } out = ring.events[tail & (kMkbQueueSize - 1)]; diff --git a/common/include/coop/shared_memory.hpp b/common/include/coop/shared_memory.hpp index f119285..9e7b598 100644 --- a/common/include/coop/shared_memory.hpp +++ b/common/include/coop/shared_memory.hpp @@ -9,26 +9,20 @@ #include "coop/protocol.hpp" -namespace coop -{ +namespace coop { -class SharedMemory -{ -public: +class SharedMemory { + public: SharedMemory() = default; SharedMemory(const SharedMemory&) = delete; SharedMemory& operator=(const SharedMemory&) = delete; - SharedMemory(SharedMemory&& other) noexcept - { - *this = std::move(other); - } + SharedMemory(SharedMemory&& other) noexcept { *this = std::move(other); } SharedMemory& operator=(SharedMemory&& other) noexcept { - if (this != &other) - { + if (this != &other) { reset(); mapping_ = std::exchange(other.mapping_, nullptr); view_ = std::exchange(other.view_, nullptr); @@ -37,19 +31,15 @@ public: return *this; } - ~SharedMemory() - { - reset(); - } + ~SharedMemory() { reset(); } // Host side: create (or open if it already exists) the named section. bool create(const std::wstring& name, std::size_t size) { reset(); - mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, - static_cast(size), name.c_str()); - if (mapping_ == nullptr) - { + mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, static_cast(size), + name.c_str()); + if (mapping_ == nullptr) { return false; } return map(size); @@ -60,8 +50,7 @@ public: { reset(); mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str()); - if (mapping_ == nullptr) - { + if (mapping_ == nullptr) { return false; } return map(size); @@ -69,23 +58,18 @@ public: void reset() { - if (view_ != nullptr) - { + if (view_ != nullptr) { UnmapViewOfFile(view_); view_ = nullptr; } - if (mapping_ != nullptr) - { + if (mapping_ != nullptr) { CloseHandle(mapping_); mapping_ = nullptr; } size_ = 0; } - [[nodiscard]] bool valid() const - { - return view_ != nullptr; - } + [[nodiscard]] bool valid() const { return view_ != nullptr; } template [[nodiscard]] T* as() const @@ -93,22 +77,15 @@ public: return static_cast(view_); } - [[nodiscard]] void* data() const - { - return view_; - } + [[nodiscard]] void* data() const { return view_; } - [[nodiscard]] std::size_t size() const - { - return size_; - } + [[nodiscard]] std::size_t size() const { return size_; } -private: + private: bool map(std::size_t size) { view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size); - if (view_ == nullptr) - { + if (view_ == nullptr) { CloseHandle(mapping_); mapping_ = nullptr; return false; diff --git a/common/include/coop/tone_analysis.hpp b/common/include/coop/tone_analysis.hpp index bfa1252..c9fcadc 100644 --- a/common/include/coop/tone_analysis.hpp +++ b/common/include/coop/tone_analysis.hpp @@ -21,8 +21,7 @@ #include #include -namespace coop -{ +namespace coop { // WAVE_FORMAT_* tags we decode (kept local to avoid an mmreg.h dependency, matching audio_mix.hpp). inline constexpr std::uint32_t kToneFormatPcm = 1; @@ -30,37 +29,35 @@ inline constexpr std::uint32_t kToneFormatFloat = 3; // One channel's worth of measured fidelity. Fields are NaN/0 when not applicable // (e.g. pitch metrics need a known expected_hz > 0). -struct ToneReport -{ - bool valid = false; // enough samples to analyze - unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate) - std::size_t frames = 0; // mono frames analyzed +struct ToneReport { + bool valid = false; // enough samples to analyze + unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate) + std::size_t frames = 0; // mono frames analyzed double duration_sec = 0.0; // --- Level --- - double rms = 0.0; // 0..1 - double peak = 0.0; // 0..1 - double clipped_fraction = 0.0; // fraction of samples at >= 0.999 full-scale + double rms = 0.0; // 0..1 + double peak = 0.0; // 0..1 + double clipped_fraction = 0.0; // fraction of samples at >= 0.999 full-scale // --- Pitch (needs a known input tone frequency) --- - double expected_hz = 0.0; // the tone frequency that was played - double dominant_hz = 0.0; // the fundamental we recovered - double pitch_error_ratio = 0.0; // dominant / expected (1.0 = perfect) - double pitch_error_cents = 0.0; // 1200*log2(ratio); +/- ~10 cents starts to be audible + double expected_hz = 0.0; // the tone frequency that was played + double dominant_hz = 0.0; // the fundamental we recovered + double pitch_error_ratio = 0.0; // dominant / expected (1.0 = perfect) + double pitch_error_cents = 0.0; // 1200*log2(ratio); +/- ~10 cents starts to be audible // --- Spectral purity (tone mode) --- - double snr_db = 0.0; // fundamental power vs everything else (DC + harmonics excluded from "signal") - double thd_percent = 0.0; // harmonics 2..6 vs fundamental + double snr_db = 0.0; // fundamental power vs everything else (DC + harmonics excluded from "signal") + double thd_percent = 0.0; // harmonics 2..6 vs fundamental // --- Time-domain defects (content-agnostic) --- - unsigned glitch_count = 0; // discontinuity events (clicks): big isolated sample jumps + unsigned glitch_count = 0; // discontinuity events (clicks): big isolated sample jumps double glitch_rate_per_sec = 0.0; - unsigned dropout_count = 0; // gaps: stretches that fall near-silent mid-signal - double dropout_ms = 0.0; // total duration of those gaps + unsigned dropout_count = 0; // gaps: stretches that fall near-silent mid-signal + double dropout_ms = 0.0; // total duration of those gaps }; -namespace detail -{ +namespace detail { inline constexpr double kPi = 3.14159265358979323846; @@ -68,28 +65,22 @@ inline constexpr double kPi = 3.14159265358979323846; inline void fft(std::vector>& a) { const std::size_t n = a.size(); - for (std::size_t i = 1, j = 0; i < n; ++i) - { + for (std::size_t i = 1, j = 0; i < n; ++i) { std::size_t bit = n >> 1; - for (; (j & bit) != 0; bit >>= 1) - { + for (; (j & bit) != 0; bit >>= 1) { j ^= bit; } j ^= bit; - if (i < j) - { + if (i < j) { std::swap(a[i], a[j]); } } - for (std::size_t len = 2; len <= n; len <<= 1) - { + for (std::size_t len = 2; len <= n; len <<= 1) { const double ang = -2.0 * kPi / static_cast(len); const std::complex wlen(std::cos(ang), std::sin(ang)); - for (std::size_t i = 0; i < n; i += len) - { + for (std::size_t i = 0; i < n; i += len) { std::complex w(1.0, 0.0); - for (std::size_t k = 0; k < len / 2; ++k) - { + for (std::size_t k = 0; k < len / 2; ++k) { const std::complex u = a[i + k]; const std::complex v = a[i + k + len / 2] * w; a[i + k] = u + v; @@ -104,8 +95,7 @@ inline void fft(std::vector>& a) inline std::size_t floor_pow2(std::size_t n) { std::size_t p = 1; - while ((p << 1) != 0 && (p << 1) <= n) - { + while ((p << 1) != 0 && (p << 1) <= n) { p <<= 1; } return n == 0 ? 0 : p; @@ -120,27 +110,21 @@ inline std::vector decode_channel(const std::uint8_t* pcm, std::size_t by std::uint32_t bits, std::uint32_t channels, std::uint32_t channel = 0) { std::vector out; - if (pcm == nullptr || channels == 0 || channel >= channels) - { + if (pcm == nullptr || channels == 0 || channel >= channels) { return out; } - if (format_tag == kToneFormatFloat && bits == 32) - { + if (format_tag == kToneFormatFloat && bits == 32) { const std::size_t frames = bytes / (channels * 4); out.reserve(frames); const auto* f = reinterpret_cast(pcm); - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { out.push_back(f[i * channels + channel]); } - } - else if (format_tag == kToneFormatPcm && bits == 16) - { + } else if (format_tag == kToneFormatPcm && bits == 16) { const std::size_t frames = bytes / (channels * 2); out.reserve(frames); const auto* s = reinterpret_cast(pcm); - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { out.push_back(static_cast(s[i * channels + channel]) / 32768.0f); } } @@ -150,15 +134,13 @@ inline std::vector decode_channel(const std::uint8_t* pcm, std::size_t by // Analyze a single channel of normalized float samples. `expected_hz` is the known // input tone frequency (pass 0 to skip the pitch/SNR/THD metrics for non-tone audio; // the click/dropout/level metrics still apply). -inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate, - double expected_hz) +inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate, double expected_hz) { ToneReport r; r.sample_rate = sample_rate; r.frames = frames; r.expected_hz = expected_hz; - if (samples == nullptr || frames < 64 || sample_rate == 0) - { + if (samples == nullptr || frames < 64 || sample_rate == 0) { return r; } r.valid = true; @@ -168,14 +150,12 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne double sumsq = 0.0; double peak = 0.0; std::size_t clipped = 0; - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { const double x = samples[i]; sumsq += x * x; const double a = std::fabs(x); peak = std::max(peak, a); - if (a >= 0.999) - { + if (a >= 0.999) { ++clipped; } } @@ -188,11 +168,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne // step. Use the median |first difference| as a robust scale (immune to the tone's own // slope and to a few outliers), and flag steps beyond 8x it. Group samples within a // short refractory window into one event so a single click isn't counted many times. - if (frames >= 3) - { + if (frames >= 3) { std::vector diff(frames - 1); - for (std::size_t i = 1; i < frames; ++i) - { + for (std::size_t i = 1; i < frames; ++i) { diff[i - 1] = std::fabs(samples[i] - samples[i - 1]); } std::vector sorted(diff); @@ -202,12 +180,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne const std::size_t refractory = std::max(sample_rate / 1000, 8); // ~1 ms std::size_t last_event_end = 0; bool have_event = false; - for (std::size_t i = 0; i < diff.size(); ++i) - { - if (diff[i] > thresh) - { - if (!have_event || i > last_event_end) - { + for (std::size_t i = 0; i < diff.size(); ++i) { + if (diff[i] > thresh) { + if (!have_event || i > last_event_end) { ++r.glitch_count; } have_event = true; @@ -220,43 +195,35 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne // --- Dropout detection: stretches that fall near-silent in an otherwise active signal --- // Slide a ~5 ms window; flag windows whose RMS drops below 8% of the global RMS. Only // meaningful when the signal is actually present (global RMS above a small floor). - if (r.rms > 1e-4) - { + if (r.rms > 1e-4) { const std::size_t win = std::max(sample_rate * 5 / 1000, 16); // ~5 ms const std::size_t hop = std::max(win / 2, 1); const double silence_thresh = 0.08 * r.rms; bool in_gap = false; - std::size_t gap_first = 0; // first silent window's start sample - std::size_t gap_last = 0; // last silent window's end sample + std::size_t gap_first = 0; // first silent window's start sample + std::size_t gap_last = 0; // last silent window's end sample std::size_t total_silent_samples = 0; auto close_gap = [&]() { - if (in_gap) - { + if (in_gap) { total_silent_samples += (gap_last - gap_first); in_gap = false; } }; - for (std::size_t start = 0; start + win <= frames; start += hop) - { + for (std::size_t start = 0; start + win <= frames; start += hop) { double ws = 0.0; - for (std::size_t i = 0; i < win; ++i) - { + for (std::size_t i = 0; i < win; ++i) { const double x = samples[start + i]; ws += x * x; } const double wr = std::sqrt(ws / static_cast(win)); - if (wr < silence_thresh) - { - if (!in_gap) - { + if (wr < silence_thresh) { + if (!in_gap) { ++r.dropout_count; gap_first = start; in_gap = true; } gap_last = start + win; - } - else - { + } else { close_gap(); } } @@ -267,23 +234,19 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne } // --- Spectral analysis (pitch / SNR / THD), Hann-windowed FFT --- - if (expected_hz > 0.0) - { + if (expected_hz > 0.0) { std::size_t n = detail::floor_pow2(frames); n = std::min(n, std::size_t(1) << 18); // cap cost (~5 s @ 48k) - if (n >= 1024) - { + if (n >= 1024) { std::vector> buf(n); - for (std::size_t i = 0; i < n; ++i) - { + for (std::size_t i = 0; i < n; ++i) { const double w = 0.5 - 0.5 * std::cos(2.0 * detail::kPi * i / (n - 1)); // Hann buf[i] = std::complex(samples[i] * w, 0.0); } detail::fft(buf); const std::size_t half = n / 2; std::vector mag(half); - for (std::size_t i = 0; i < half; ++i) - { + for (std::size_t i = 0; i < half; ++i) { mag[i] = std::abs(buf[i]); } const double bin_hz = static_cast(sample_rate) / static_cast(n); @@ -291,31 +254,26 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne // Peak bin, ignoring DC/very low bins (skip < 20 Hz). std::size_t lo = std::max(static_cast(20.0 / bin_hz), 1); std::size_t peak_bin = lo; - for (std::size_t i = lo; i < half; ++i) - { - if (mag[i] > mag[peak_bin]) - { + for (std::size_t i = lo; i < half; ++i) { + if (mag[i] > mag[peak_bin]) { peak_bin = i; } } // Quadratic (parabolic) interpolation on log-magnitude for a sub-bin estimate // (accurate for a Hann-windowed peak). double delta = 0.0; - if (peak_bin > 0 && peak_bin + 1 < half) - { + if (peak_bin > 0 && peak_bin + 1 < half) { const double a = std::log(mag[peak_bin - 1] + 1e-30); const double b = std::log(mag[peak_bin] + 1e-30); const double c = std::log(mag[peak_bin + 1] + 1e-30); const double denom = (a - 2.0 * b + c); - if (std::fabs(denom) > 1e-30) - { + if (std::fabs(denom) > 1e-30) { delta = 0.5 * (a - c) / denom; delta = std::max(-0.5, std::min(0.5, delta)); } } r.dominant_hz = (static_cast(peak_bin) + delta) * bin_hz; - if (r.dominant_hz > 0.0) - { + if (r.dominant_hz > 0.0) { r.pitch_error_ratio = r.dominant_hz / expected_hz; r.pitch_error_cents = 1200.0 * std::log2(r.pitch_error_ratio); } @@ -328,10 +286,8 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne auto lobe_power = [&](double hz, long half_w) { const long center = static_cast(std::lround(hz / bin_hz)); double p = 0.0; - for (long k = center - half_w; k <= center + half_w; ++k) - { - if (k >= 0 && static_cast(k) < half) - { + for (long k = center - half_w; k <= center + half_w; ++k) { + if (k >= 0 && static_cast(k) < half) { p += mag[k] * mag[k]; } } @@ -339,8 +295,7 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne }; double total_power = 0.0; - for (std::size_t i = lo; i < half; ++i) - { + for (std::size_t i = lo; i < half; ++i) { total_power += mag[i] * mag[i]; } const double fund_power = lobe_power(r.dominant_hz, 8); @@ -348,11 +303,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne r.snr_db = 10.0 * std::log10(std::max(fund_power, 1e-30) / residual); double harm_power = 0.0; - for (int h = 2; h <= 6; ++h) - { + for (int h = 2; h <= 6; ++h) { const double hz = r.dominant_hz * h; - if (hz < (sample_rate / 2.0)) - { + if (hz < (sample_rate / 2.0)) { harm_power += lobe_power(hz, 3); } } diff --git a/common/include/coop/tool_paths.hpp b/common/include/coop/tool_paths.hpp index ab669ce..876ac82 100644 --- a/common/include/coop/tool_paths.hpp +++ b/common/include/coop/tool_paths.hpp @@ -9,8 +9,7 @@ #include -namespace coop -{ +namespace coop { // Directory of the current executable, with a trailing separator. inline std::wstring exe_directory() @@ -28,21 +27,17 @@ inline std::wstring exe_directory() inline std::wstring deployed_artifact_path(const wchar_t* name) { const std::wstring here = exe_directory() + name; - if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) { return here; } std::wstring dir = exe_directory(); - if (!dir.empty()) - { + if (!dir.empty()) { dir.pop_back(); // drop the trailing separator before going up a level } const std::size_t slash = dir.find_last_of(L"\\/"); - if (slash != std::wstring::npos) - { + if (slash != std::wstring::npos) { const std::wstring up = dir.substr(0, slash + 1) + name; - if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES) { return up; } } diff --git a/common/include/coop/wav.hpp b/common/include/coop/wav.hpp index fc38c18..2559b66 100644 --- a/common/include/coop/wav.hpp +++ b/common/include/coop/wav.hpp @@ -12,20 +12,17 @@ #include #include -namespace coop -{ +namespace coop { -struct WavData -{ +struct WavData { std::uint32_t sample_rate = 0; std::uint32_t channels = 0; std::uint32_t bits = 0; - std::uint32_t format_tag = 0; // 1 = PCM, 3 = IEEE float + std::uint32_t format_tag = 0; // 1 = PCM, 3 = IEEE float std::vector pcm; // interleaved frames }; -namespace detail -{ +namespace detail { inline void wav_put_u32(std::vector& b, std::uint32_t v) { b.push_back(v & 0xFF); @@ -75,12 +72,11 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt detail::wav_put_u32(hdr, static_cast(bytes)); FILE* f = nullptr; - if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr) - { + if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr) { return false; } - const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size() && - (bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes); + const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size() + && (bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes); std::fclose(f); return ok; } @@ -89,44 +85,37 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt inline bool wav_read(const std::wstring& path, WavData& out) { FILE* f = nullptr; - if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr) - { + if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr) { return false; } std::fseek(f, 0, SEEK_END); const long size = std::ftell(f); std::fseek(f, 0, SEEK_SET); - if (size < 44) - { + if (size < 44) { std::fclose(f); return false; } std::vector all(static_cast(size)); const bool read_ok = std::fread(all.data(), 1, all.size(), f) == all.size(); std::fclose(f); - if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0) - { + if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0) { return false; } // Walk chunks for "fmt " and "data". std::size_t pos = 12; bool have_fmt = false, have_data = false; - while (pos + 8 <= all.size()) - { + while (pos + 8 <= all.size()) { const std::uint8_t* p = all.data() + pos; const std::uint32_t chunk_size = detail::wav_get_u32(p + 4); const std::size_t body = pos + 8; - if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size()) - { + if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size()) { out.format_tag = detail::wav_get_u16(all.data() + body + 0); out.channels = detail::wav_get_u16(all.data() + body + 2); out.sample_rate = detail::wav_get_u32(all.data() + body + 4); out.bits = detail::wav_get_u16(all.data() + body + 14); have_fmt = true; - } - else if (std::memcmp(p, "data", 4) == 0) - { + } else if (std::memcmp(p, "data", 4) == 0) { const std::size_t avail = all.size() - body; const std::size_t n = std::min(chunk_size, avail); out.pcm.assign(all.begin() + body, all.begin() + body + n); @@ -136,8 +125,7 @@ inline bool wav_read(const std::wstring& path, WavData& out) // wrap `pos` on a 32-bit size_t (x86) and spin the loop on garbage, and there's nothing valid // past a chunk that claims more than the file holds anyway. const std::size_t advance = static_cast(chunk_size) + (chunk_size & 1); - if (advance > all.size() - body) - { + if (advance > all.size() - body) { break; } pos = body + advance; diff --git a/hook/src/audio_hook.cpp b/hook/src/audio_hook.cpp index e8d5838..0739a68 100644 --- a/hook/src/audio_hook.cpp +++ b/hook/src/audio_hook.cpp @@ -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 g_client_formats; // Streams we track (frame counting + per-stream capture). Index 0 is primary. -struct TrackedStream -{ +struct TrackedStream { std::atomic client{nullptr}; std::atomic frames{0}; - std::atomic block_align{0}; // hot-path frame size for this stream - std::atomic assumed_format{0}; // 1 = channels/bits guessed -> clamp copies safely + std::atomic block_align{0}; // hot-path frame size for this stream + std::atomic 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 g_streams_seen{0}; // total distinct clients ever seen std::atomic 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(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(mbi.BaseAddress); - const auto avail = static_cast((base + mbi.RegionSize) - - static_cast(ptr)); - if (avail < want) - { + const auto avail = static_cast((base + mbi.RegionSize) - static_cast(ptr)); + if (avail < want) { return static_cast(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()(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()( - self, num_frames, flags | AUDCLNT_BUFFERFLAGS_SILENT); + return g_vh_releasebuffer.original()(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(sizeof(num_frames)) + want) - { + if (readable_bytes(t_gb_data, want) == want + && audio_ring_free_space(*vring) >= static_cast(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 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()(self, mode, flags, buffer_duration, - periodicity, format, session); + const HRESULT hr = + g_vh_initialize.original()(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(flags), static_cast(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(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(&hk_Initialize)); g_vh_getservice.install(ac, kIdx_IAudioClient_GetService, reinterpret_cast(&hk_GetService)); g_audioclient_hooked = (static_cast(g_vh_initialize) && static_cast(g_vh_getservice)); - logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac, - static_cast(g_vh_initialize) ? 1 : 0, static_cast(g_vh_getservice) ? 1 : 0); + logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac, static_cast(g_vh_initialize) ? 1 : 0, + static_cast(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()(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(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(*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(&enumerator)))) - { + if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&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(&g_self_client)); - if (SUCCEEDED(ah) && g_self_client != nullptr) - { + HRESULT ah = + device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&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(&self_render)); + if (SUCCEEDED(ih)) { + ih = g_self_client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&self_render)); } logf("build_probe: client init=0x%08lX render=%p mix=%uHz/%uch/%ubit tag=%u", static_cast(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(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(&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(&hk_Initialize)); - g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService, - reinterpret_cast(&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(&hk_Initialize)); + g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService, reinterpret_cast(&hk_GetService)); g_vh_getbuffer.install(sr, kIdx_IAudioRenderClient_GetBuffer, reinterpret_cast(&hk_GetBuffer)); g_vh_releasebuffer.install(sr, kIdx_IAudioRenderClient_ReleaseBuffer, reinterpret_cast(&hk_ReleaseBuffer)); @@ -755,10 +678,9 @@ void install_detours_locked() hook_set_installed(g_id_getservice, static_cast(g_vh_getservice)); hook_set_installed(g_id_getbuffer, static_cast(g_vh_getbuffer)); hook_set_installed(g_id_releasebuffer, static_cast(g_vh_releasebuffer)); - logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d", - static_cast(g_vh_activate) ? 1 : 0, static_cast(g_vh_initialize) ? 1 : 0, - static_cast(g_vh_getservice) ? 1 : 0, static_cast(g_vh_getbuffer) ? 1 : 0, - static_cast(g_vh_releasebuffer) ? 1 : 0); + logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d", static_cast(g_vh_activate) ? 1 : 0, + static_cast(g_vh_initialize) ? 1 : 0, static_cast(g_vh_getservice) ? 1 : 0, + static_cast(g_vh_getbuffer) ? 1 : 0, static_cast(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; } diff --git a/hook/src/audio_hook.hpp b/hook/src/audio_hook.hpp index fd24a87..b2a1e0a 100644 --- a/hook/src/audio_hook.hpp +++ b/hook/src/audio_hook.hpp @@ -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 diff --git a/hook/src/d3d9_hook.cpp b/hook/src/d3d9_hook.cpp index d5bc6df..67a5762 100644 --- a/hook/src/d3d9_hook.cpp +++ b/hook/src/d3d9_hook.cpp @@ -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(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(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(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(lr.pBits) + static_cast(y) * lr.Pitch; + for (UINT y = 0; y < h; ++y) { + const unsigned char* src = + static_cast(lr.pBits) + static_cast(y) * lr.Pitch; unsigned char* out = g_rgba.data() + static_cast(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(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(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(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; } diff --git a/hook/src/d3d9_hook.hpp b/hook/src/d3d9_hook.hpp index 2108e31..63f1690 100644 --- a/hook/src/d3d9_hook.hpp +++ b/hook/src/d3d9_hook.hpp @@ -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. diff --git a/hook/src/debug_log.cpp b/hook/src/debug_log.cpp index 4b46e23..52d2f13 100644 --- a/hook/src/debug_log.cpp +++ b/hook/src/debug_log.cpp @@ -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 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); } } diff --git a/hook/src/debug_log.hpp b/hook/src/debug_log.hpp index 90d6945..ab6217f 100644 --- a/hook/src/debug_log.hpp +++ b/hook/src/debug_log.hpp @@ -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. diff --git a/hook/src/dllmain.cpp b/hook/src/dllmain.cpp index 851477f..7fa9313 100644 --- a/hook/src/dllmain.cpp +++ b/hook/src/dllmain.cpp @@ -27,8 +27,7 @@ #include "vk_hook.hpp" #include "xinput_hook.hpp" -namespace -{ +namespace { coop::hook::IpcClient g_ipc; std::atomic 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(); - 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_[_]); 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(); - 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(); diff --git a/hook/src/find_window.hpp b/hook/src/find_window.hpp index d0c5157..bbd0bc7 100644 --- a/hook/src/find_window.hpp +++ b/hook/src/find_window.hpp @@ -5,13 +5,11 @@ #include -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(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; } diff --git a/hook/src/focus_spoof.cpp b/hook/src/focus_spoof.cpp index 3304ae6..7a4d661 100644 --- a/hook/src/focus_spoof.cpp +++ b/hook/src/focus_spoof.cpp @@ -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(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(GetProcAddress(module, name))) - { + if (void* target = reinterpret_cast(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(GetWindowLongPtrW(hwnd, GWLP_WNDPROC)) : reinterpret_cast(GetWindowLongPtrA(hwnd, GWLP_WNDPROC)); - if (g_unicode) - { + if (g_unicode) { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast(&subclass_proc)); - } - else - { + } else { SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast(&subclass_proc)); } hook_set_installed(g_id_wndproc, true); - if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) - { - hook_export(user32, "GetForegroundWindow", reinterpret_cast(&hk_GetForegroundWindow), - g_id_foreground); + if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) { + hook_export(user32, "GetForegroundWindow", reinterpret_cast(&hk_GetForegroundWindow), g_id_foreground); hook_export(user32, "GetActiveWindow", reinterpret_cast(&hk_GetActiveWindow), g_id_active); hook_export(user32, "GetFocus", reinterpret_cast(&hk_GetFocus), g_id_focus); - if (void* clip = reinterpret_cast(GetProcAddress(user32, "ClipCursor"))) - { + if (void* clip = reinterpret_cast(GetProcAddress(user32, "ClipCursor"))) { install_inline(g_hk_clipcursor, clip, &hk_ClipCursor); hook_set_installed(g_id_clipcursor, static_cast(g_hk_clipcursor)); } - if (void* setpos = reinterpret_cast(GetProcAddress(user32, "SetCursorPos"))) - { + if (void* setpos = reinterpret_cast(GetProcAddress(user32, "SetCursorPos"))) { install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos); hook_set_installed(g_id_setcursorpos, static_cast(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 devices(count); const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE)); - if (got != static_cast(-1)) - { + if (got != static_cast(-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(g_orig_proc)); - } - else - { + } else { SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast(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; diff --git a/hook/src/focus_spoof.hpp b/hook/src/focus_spoof.hpp index ff4fa32..1fe358f 100644 --- a/hook/src/focus_spoof.hpp +++ b/hook/src/focus_spoof.hpp @@ -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 diff --git a/hook/src/hook_guard.hpp b/hook/src/hook_guard.hpp index 818ae0b..d6131f9 100644 --- a/hook/src/hook_guard.hpp +++ b/hook/src/hook_guard.hpp @@ -26,28 +26,19 @@ #include -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 m_active{0}; }; @@ -92,8 +78,7 @@ private: template 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"); } } diff --git a/hook/src/hook_install.hpp b/hook/src/hook_install.hpp index 82524c5..5ac63e7 100644 --- a/hook/src/hook_install.hpp +++ b/hook/src/hook_install.hpp @@ -23,8 +23,7 @@ #include -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"); } } diff --git a/hook/src/hook_registry.cpp b/hook/src/hook_registry.cpp index 41d7297..73a68e4 100644 --- a/hook/src/hook_registry.cpp +++ b/hook/src/hook_registry.cpp @@ -4,14 +4,11 @@ #include #include -namespace coop::hook -{ +namespace coop::hook { -namespace -{ +namespace { -struct Slot -{ +struct Slot { char name[40] = {}; std::atomic subsystem{0}; std::atomic 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(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(kMaxHookEntries)) - { + if (id >= 0 && id < static_cast(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(kMaxHookEntries)) - { + if (id >= 0 && id < static_cast(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); diff --git a/hook/src/hook_registry.hpp b/hook/src/hook_registry.hpp index 808ddfd..fe86772 100644 --- a/hook/src/hook_registry.hpp +++ b/hook/src/hook_registry.hpp @@ -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 diff --git a/hook/src/ipc_client.hpp b/hook/src/ipc_client.hpp index c582c9a..f878d90 100644 --- a/hook/src/ipc_client.hpp +++ b/hook/src/ipc_client.hpp @@ -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(); - 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; }; diff --git a/hook/src/mkb_hook.cpp b/hook/src/mkb_hook.cpp index abbf977..627fbc0 100644 --- a/hook/src/mkb_hook.cpp +++ b/hook/src/mkb_hook.cpp @@ -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 g_active{false}; -std::atomic g_key_down[256]; // by Win32 virtual-key (incl. VK_LBUTTON etc.) -std::atomic g_cursor_x{0}; // last forwarded mouse position (game client px) +std::atomic g_key_down[256]; // by Win32 virtual-key (incl. VK_LBUTTON etc.) +std::atomic g_cursor_x{0}; // last forwarded mouse position (game client px) std::atomic g_cursor_y{0}; -std::atomic g_have_cursor{false}; // a mouse event has been forwarded at least once -std::atomic g_target{nullptr}; // game main window (HWND), resolved lazily +std::atomic g_have_cursor{false}; // a mouse event has been forwarded at least once +std::atomic 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(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(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(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(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(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()(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(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(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(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(-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(-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(-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(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(GetProcAddress(user32, name))) - { + if (void* target = reinterpret_cast(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(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(&g_di_probe), nullptr)) || - g_di_probe == nullptr) - { + reinterpret_cast(&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(&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(&hk_GetRawInputData), - g_hk_getrawinputdata, g_id_rawinput); + install_user32_hook(user32, "GetRawInputData", reinterpret_cast(&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(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; diff --git a/hook/src/mkb_hook.hpp b/hook/src/mkb_hook.hpp index 0118b6b..68fd695 100644 --- a/hook/src/mkb_hook.hpp +++ b/hook/src/mkb_hook.hpp @@ -10,8 +10,7 @@ #include "ipc_client.hpp" -namespace coop::hook -{ +namespace coop::hook { bool install_mkb_hooks(IpcClient& ipc); void remove_mkb_hooks(); diff --git a/hook/src/opengl_hook.cpp b/hook/src/opengl_hook.cpp index fa881ba..dd8e9e0 100644 --- a/hook/src/opengl_hook.cpp +++ b/hook/src/opengl_hook.cpp @@ -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 g_read_buf; // glReadPixels target (bottom-up) -std::vector g_flip_buf; // vertically flipped, uploaded to D3D +std::vector g_read_buf; // glReadPixels target (bottom-up) +std::vector 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(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(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(rc.right - rc.left); const UINT h = static_cast(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(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(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(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(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(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(GetProcAddress(gdi, "SwapBuffers"))) - { + if (HMODULE gdi = GetModuleHandleW(L"gdi32.dll")) { + if (void* fn = reinterpret_cast(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(GetProcAddress(gl, "wglSwapBuffers"))) - { + if (HMODULE gl = GetModuleHandleW(L"opengl32.dll")) { + if (void* fn = reinterpret_cast(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; } diff --git a/hook/src/opengl_hook.hpp b/hook/src/opengl_hook.hpp index 2734515..5d1c060 100644 --- a/hook/src/opengl_hook.hpp +++ b/hook/src/opengl_hook.hpp @@ -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. diff --git a/hook/src/present_hook.cpp b/hook/src/present_hook.cpp index d045355..182403b 100644 --- a/hook/src/present_hook.cpp +++ b/hook/src/present_hook.cpp @@ -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 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(&queue)); - if (FAILED(hr) || queue == nullptr) - { + if (FAILED(hr) || queue == nullptr) { logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast(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(hr)); queue->Release(); return false; } ID3D11On12Device* on12 = nullptr; hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast(&on12)); - if (FAILED(hr) || on12 == nullptr) - { + if (FAILED(hr) || on12 == nullptr) { logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast(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(&fence)); - if (FAILED(hr) || fence == nullptr) - { - logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)", - static_cast(hr)); + if (FAILED(hr) || fence == nullptr) { + logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)", static_cast(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(&sc3))) && sc3 != nullptr) - { + if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast(&sc3))) && sc3 != nullptr) { bb_index = sc3->GetCurrentBackBufferIndex(); sc3->Release(); } ID3D12Resource* bb = nullptr; - if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast(&bb))) || bb == nullptr) - { - if (!g_unsupported_logged) - { + if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast(&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(rd.Width), rd.Height, static_cast(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(rd.Width), rd.Height, static_cast(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(&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(&wrapped)); + if (SUCCEEDED(hr) && wrapped != nullptr) { g_on12->AcquireWrappedResources(&wrapped, 1); ID3D11Texture2D* wtex = nullptr; - if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D), - reinterpret_cast(&wtex))) && - wtex != nullptr) - { + if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(&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(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(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(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(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(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(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(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(&backbuf))) || - backbuf == nullptr) - { + if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&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(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(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(&bb10))) && bb10 != nullptr) - { + if (SUCCEEDED(sc->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast(&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(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(&dev))) || - dev == nullptr) - { + if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast(&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(&queue))) && - queue != nullptr) - { + if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast(&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(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(&sc1))) && - sc1 != nullptr) - { + if (SUCCEEDED(swapchain->QueryInterface(__uuidof(IDXGISwapChain1), reinterpret_cast(&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(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(g_hk_ecl)); - logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl, - static_cast(g_hk_ecl) ? 1 : 0); - } - else - { + logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl, static_cast(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(g_hk_present); diff --git a/hook/src/present_hook.hpp b/hook/src/present_hook.hpp index b84bfea..927003c 100644 --- a/hook/src/present_hook.hpp +++ b/hook/src/present_hook.hpp @@ -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. diff --git a/hook/src/rate_estimator.hpp b/hook/src/rate_estimator.hpp index de91a04..6802b7c 100644 --- a/hook/src/rate_estimator.hpp +++ b/hook/src/rate_estimator.hpp @@ -25,20 +25,17 @@ #include -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(window_seconds * static_cast(freq))) - { + if (dt < static_cast(window_seconds * static_cast(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(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(last_raw_ + 0.5); + const std::uint32_t best = last_snapped_ != 0 ? last_snapped_ : static_cast(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; diff --git a/hook/src/shared_video_texture.hpp b/hook/src/shared_video_texture.hpp index 89019d9..7ccfeb4 100644 --- a/hook/src/shared_video_texture.hpp +++ b/hook/src/shared_video_texture.hpp @@ -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(hr), w, h, static_cast(fmt)); + if (FAILED(hr) || m_tex == nullptr) { + logf("%s: CreateTexture2D(shared) failed hr=0x%08lX (%ux%u fmt=%d)", tag, static_cast(hr), w, + h, static_cast(fmt)); release(); return false; } IDXGIResource1* res = nullptr; hr = m_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast(&res)); - if (FAILED(hr) || res == nullptr) - { + if (FAILED(hr) || res == nullptr) { logf("%s: QI IDXGIResource1 failed hr=0x%08lX", tag, static_cast(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(hr)); release(); return false; } hr = m_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast(&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(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 diff --git a/hook/src/vk_capture.cpp b/hook/src/vk_capture.cpp index 4918cea..d5db31b 100644 --- a/hook/src/vk_capture.cpp +++ b/hook/src/vk_capture.cpp @@ -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(i); best_coherent = (f & coherent) != 0; } } - if (best < 0) - { + if (best < 0) { return false; } out_index = static_cast(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(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 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 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(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(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(y) * row; unsigned char* o = m_rgba.data() + static_cast(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(row), 0); m_d3d_ctx->Flush(); m_shared.mutex()->ReleaseSync(kVideoMutexKey); published = true; } - if (published) - { + if (published) { { std::lock_guard 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 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 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& out, std::uint32_t& w, std::uint32_t& h) { std::lock_guard lk(m_last_mutex); - if (m_last.empty()) - { + if (m_last.empty()) { return false; } out = m_last; diff --git a/hook/src/vk_capture.hpp b/hook/src/vk_capture.hpp index b725311..da77cf0 100644 --- a/hook/src/vk_capture.hpp +++ b/hook/src/vk_capture.hpp @@ -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 on_frame); + void init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns, unsigned long pid, + std::function 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& 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; diff --git a/hook/src/vk_hook.cpp b/hook/src/vk_hook.cpp index 988c690..706a1ee 100644 --- a/hook/src/vk_hook.cpp +++ b/hook/src/vk_hook.cpp @@ -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(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(real_gipa(g_instance, "vkGetDeviceProcAddr")); - g_real_create_swapchain = - reinterpret_cast(g_real_gdpa(*out, "vkCreateSwapchainKHR")); + g_real_create_swapchain = reinterpret_cast(g_real_gdpa(*out, "vkCreateSwapchainKHR")); g_real_present = reinterpret_cast(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(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(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(&hk_vkQueuePresentKHR); } - if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) - { + if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) { return reinterpret_cast(&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(&hk_vkGetInstanceProcAddr); } - if (std::strcmp(name, "vkCreateInstance") == 0) - { + if (std::strcmp(name, "vkCreateInstance") == 0) { return reinterpret_cast(&hk_vkCreateInstance); } - if (std::strcmp(name, "vkCreateDevice") == 0) - { + if (std::strcmp(name, "vkCreateDevice") == 0) { return reinterpret_cast(&hk_vkCreateDevice); } - if (std::strcmp(name, "vkGetDeviceProcAddr") == 0) - { + if (std::strcmp(name, "vkGetDeviceProcAddr") == 0) { return reinterpret_cast(&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(&hk_vkQueuePresentKHR); } - if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) - { + if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) { return reinterpret_cast(&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(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; diff --git a/hook/src/vk_hook.hpp b/hook/src/vk_hook.hpp index ae6ba6c..2009ab1 100644 --- a/hook/src/vk_hook.hpp +++ b/hook/src/vk_hook.hpp @@ -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 diff --git a/hook/src/vtable_hook.hpp b/hook/src/vtable_hook.hpp index 5ea29bf..31bfe01 100644 --- a/hook/src/vtable_hook.hpp +++ b/hook/src/vtable_hook.hpp @@ -11,8 +11,7 @@ #include -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(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(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 Fn original() const { return reinterpret_cast(m_original); } + template + Fn original() const + { + return reinterpret_cast(m_original); + } explicit operator bool() const { return m_vtable != nullptr; } -private: + private: void** m_vtable = nullptr; unsigned m_index = 0; void* m_original = nullptr; diff --git a/hook/src/xinput_hook.cpp b/hook/src/xinput_hook.cpp index ec3529d..834f410 100644 --- a/hook/src/xinput_hook.cpp +++ b/hook/src/xinput_hook.cpp @@ -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 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(GetProcAddress(module, name))) - { + if (void* target = reinterpret_cast(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(&hk_XInputGetState), g_id_getstate); hook_export(module, MAKEINTRESOURCEA(100), reinterpret_cast(&hk_XInputGetStateEx), g_id_getstateex); // XInputGetStateEx is exported by ordinal only - hook_export(module, "XInputGetCapabilities", reinterpret_cast(&hk_XInputGetCapabilities), - g_id_getcaps); + hook_export(module, "XInputGetCapabilities", reinterpret_cast(&hk_XInputGetCapabilities), g_id_getcaps); hook_export(module, "XInputSetState", reinterpret_cast(&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; diff --git a/hook/src/xinput_hook.hpp b/hook/src/xinput_hook.hpp index 325eaaf..7f84ffa 100644 --- a/hook/src/xinput_hook.hpp +++ b/hook/src/xinput_hook.hpp @@ -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 diff --git a/host/src/audio/audio_format_verifier.cpp b/host/src/audio/audio_format_verifier.cpp index a26b6d8..120331a 100644 --- a/host/src/audio/audio_format_verifier.cpp +++ b/host/src/audio/audio_format_verifier.cpp @@ -11,14 +11,11 @@ #include "audio/process_loopback_capture.hpp" #include "coop/audio_correlate.hpp" -namespace coop -{ -namespace -{ +namespace coop { +namespace { // Resolve a (possibly EXTENSIBLE) WAVEFORMATEX to scalar channels / bits / tag. -struct ScalarFormat -{ +struct ScalarFormat { unsigned rate = 0; unsigned channels = 0; unsigned bits = 0; @@ -32,15 +29,11 @@ ScalarFormat resolve(const WAVEFORMATEX* wfx) f.channels = wfx->nChannels; f.bits = wfx->wBitsPerSample; f.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(wfx); - if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) - { + if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) { f.tag = WAVE_FORMAT_IEEE_FLOAT; - } - else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) - { + } else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) { f.tag = WAVE_FORMAT_PCM; } } @@ -58,8 +51,7 @@ std::vector to_mono(const std::vector& bytes, const ScalarFormat& f void drain_ring(AudioRingHeader& ring, std::vector& scratch) { - while (audio_ring_pop(ring, scratch.data(), static_cast(scratch.size())) > 0) - { + while (audio_ring_pop(ring, scratch.data(), static_cast(scratch.size())) > 0) { } } @@ -70,19 +62,16 @@ ChunkedCapture parse_chunks(const std::vector& raw, unsigned stride) { ChunkedCapture cap; cap.stride = stride; - if (stride == 0) - { + if (stride == 0) { return cap; } std::size_t off = 0; - while (off + sizeof(std::uint32_t) <= raw.size()) - { + while (off + sizeof(std::uint32_t) <= raw.size()) { std::uint32_t count = 0; std::memcpy(&count, raw.data() + off, sizeof(count)); off += sizeof(count); const std::size_t payload = static_cast(count) * stride; - if (count == 0 || off + payload > raw.size()) - { + if (count == 0 || off + payload > raw.size()) { break; // truncated or garbled -> stop } cap.counts.push_back(count); @@ -97,14 +86,12 @@ ChunkedCapture parse_chunks(const std::vector& raw, unsigned stride) FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsigned window_ms, bool recover_layout) { FormatVerification result; - if (ring == nullptr) - { + if (ring == nullptr) { return result; } WAVEFORMATEX* dev_wfx = default_render_format(); - if (dev_wfx == nullptr) - { + if (dev_wfx == nullptr) { return result; } const ScalarFormat dev = resolve(dev_wfx); @@ -120,12 +107,10 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign ProcessLoopbackCapture loop; const std::uint32_t loop_block = dev_wfx->nBlockAlign; if (!loop.start(pid, dev_wfx, [&](const BYTE* data, std::uint32_t frames, bool silent) { - if (!silent && data != nullptr) - { + if (!silent && data != nullptr) { loop_bytes.insert(loop_bytes.end(), data, data + static_cast(frames) * loop_block); } - })) - { + })) { // Distinguish "couldn't activate process loopback" from "captured fine but didn't correlate": // without the ground-truth post-mix path there's nothing to correlate against, so bail now // (don't burn the window capturing only the hook side) and leave the diagnostic visible. @@ -137,18 +122,15 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign // Pull the hook's pre-mix bytes out of the ring across the window. std::vector hook_bytes; const DWORD end = GetTickCount() + window_ms; - while (GetTickCount() < end) - { + while (GetTickCount() < end) { std::uint32_t n = 0; - while ((n = audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) - { + while ((n = audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) { hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n); } Sleep(10); } std::uint32_t n = 0; - while ((n = audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) - { + while ((n = audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) { hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n); } @@ -166,19 +148,16 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign CoTaskMemFree(dev_wfx); char dbg[2] = {}; - if (GetEnvironmentVariableA("COOP_VERIFY_DEBUG", dbg, sizeof(dbg)) > 0 && dbg[0] == '1') - { + if (GetEnvironmentVariableA("COOP_VERIFY_DEBUG", dbg, sizeof(dbg)) > 0 && dbg[0] == '1') { std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n", dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(), recover_layout ? 1 : 0); } - if (loop_mono.size() < need || hook_frames < need) - { + if (loop_mono.size() < need || hook_frames < need) { return result; // not enough non-silent audio captured (game quiet, or stream wasn't a guess) } - if (recover_layout) - { + if (recover_layout) { // Recover channels + bit depth too, by trying candidate de-interleavings of the // (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback. const FormatCorrelation fc = @@ -190,9 +169,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign result.channels = fc.channels; result.bits = fc.bits; result.format_tag = fc.tag; - } - else - { + } else { // Rate only, assuming the hook layout matches the device (common stereo case), so // the de-padded payload is already clean device-layout audio. const std::vector hook_mono = to_mono(cap.bytes, dev); diff --git a/host/src/audio/audio_format_verifier.hpp b/host/src/audio/audio_format_verifier.hpp index d38a709..9ece050 100644 --- a/host/src/audio/audio_format_verifier.hpp +++ b/host/src/audio/audio_format_verifier.hpp @@ -20,19 +20,17 @@ #include "coop/audio_ring.hpp" -namespace coop -{ +namespace coop { -struct FormatVerification -{ - bool ok = false; // a confident rate correlation was found - unsigned rate = 0; // recovered true sample rate (Hz) - double score = 0.0; // correlation score of the winning rate, [0,1] +struct FormatVerification { + bool ok = false; // a confident rate correlation was found + unsigned rate = 0; // recovered true sample rate (Hz) + double score = 0.0; // correlation score of the winning rate, [0,1] - bool layout_ok = false; // a confident channels/bit-depth correlation was found (step b) - unsigned channels = 0; // recovered channel count - unsigned bits = 0; // recovered bits per sample - unsigned format_tag = 0; // recovered WAVE_FORMAT_PCM / _IEEE_FLOAT + bool layout_ok = false; // a confident channels/bit-depth correlation was found (step b) + unsigned channels = 0; // recovered channel count + unsigned bits = 0; // recovered bits per sample + unsigned format_tag = 0; // recovered WAVE_FORMAT_PCM / _IEEE_FLOAT }; // One-shot: co-capture the hook (pre-mix, via the ring's verify_capture tap) and a parallel diff --git a/host/src/audio/audio_loopback.cpp b/host/src/audio/audio_loopback.cpp index 9261b1b..97cb7a3 100644 --- a/host/src/audio/audio_loopback.cpp +++ b/host/src/audio/audio_loopback.cpp @@ -14,15 +14,12 @@ #include "audio/process_loopback_capture.hpp" #include "audio/render_pacer.hpp" -namespace coop -{ -namespace -{ +namespace coop { +namespace { // Single-producer/single-consumer byte FIFO guarded by a mutex (the capture // thread pushes, the render thread pops). Overflow drops the oldest samples. -struct ByteRing -{ +struct ByteRing { std::mutex mutex; std::vector buf; size_t head = 0; @@ -37,8 +34,7 @@ struct ByteRing void drop_for(size_t incoming) { - if (count + incoming > buf.size()) - { + if (count + incoming > buf.size()) { const size_t drop = count + incoming - buf.size(); head = (head + drop) % buf.size(); count -= drop; @@ -48,10 +44,8 @@ struct ByteRing void push(const BYTE* data, size_t bytes, bool silent) { std::lock_guard lock(mutex); - if (bytes > buf.size()) - { - if (data) - { + if (bytes > buf.size()) { + if (data) { data += bytes - buf.size(); } bytes = buf.size(); @@ -59,29 +53,21 @@ struct ByteRing drop_for(bytes); const size_t tail = (head + count) % buf.size(); const size_t first = std::min(bytes, buf.size() - tail); - if (silent || !data) - { + if (silent || !data) { std::memset(&buf[tail], 0, first); - if (bytes > first) - { + if (bytes > first) { std::memset(&buf[0], 0, bytes - first); } - } - else - { + } else { std::memcpy(&buf[tail], data, first); - if (bytes > first) - { + if (bytes > first) { std::memcpy(&buf[0], data + first, bytes - first); } } count += bytes; } - size_t available() const - { - return count; - } + size_t available() const { return count; } // Copy up to `bytes` into `dst`; returns how many bytes were available. size_t pop(BYTE* dst, size_t bytes) @@ -90,8 +76,7 @@ struct ByteRing bytes = std::min(bytes, count); const size_t first = std::min(bytes, buf.size() - head); std::memcpy(dst, &buf[head], first); - if (bytes > first) - { + if (bytes > first) { std::memcpy(dst + first, &buf[0], bytes - first); } head = (head + bytes) % buf.size(); @@ -102,8 +87,7 @@ struct ByteRing // The default render endpoint plus an event-driven render client, shared by the hooked and // loopback mirror paths. RAII: everything acquired is released on destruction. -struct RenderEndpoint -{ +struct RenderEndpoint { IMMDeviceEnumerator* enumerator = nullptr; IMMDevice* endpoint = nullptr; IAudioClient* client = nullptr; @@ -117,24 +101,19 @@ struct RenderEndpoint ~RenderEndpoint() { - if (render) - { + if (render) { render->Release(); } - if (client) - { + if (client) { client->Release(); } - if (endpoint) - { + if (endpoint) { endpoint->Release(); } - if (enumerator) - { + if (enumerator) { enumerator->Release(); } - if (event) - { + if (event) { CloseHandle(event); } } @@ -144,28 +123,24 @@ struct RenderEndpoint template bool activate(Fail&& fail) { - HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); - if (FAILED(hr)) - { + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&enumerator)); + if (FAILED(hr)) { fail("CoCreateInstance(MMDeviceEnumerator)", hr); return false; } hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("GetDefaultAudioEndpoint", hr); return false; } hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&client)); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("Activate render client", hr); return false; } event = CreateEventW(nullptr, FALSE, FALSE, nullptr); - if (!event) - { + if (!event) { fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError())); return false; } @@ -185,20 +160,17 @@ struct RenderEndpoint bool wire(Fail&& fail) { HRESULT hr = client->SetEventHandle(event); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("Render SetEventHandle", hr); return false; } hr = client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render)); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("GetService(RenderClient)", hr); return false; } hr = client->GetBufferSize(&buffer_frames); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("GetBufferSize", hr); return false; } @@ -246,20 +218,17 @@ void AudioMirror::set_fallback_reason(std::string s) void AudioMirror::enable_capture(AudioRingHeader* const* rings, bool on) { - for (unsigned i = 0; i < kMaxAudioStreams; ++i) - { - if (rings[i] != nullptr) - { + for (unsigned i = 0; i < kMaxAudioStreams; ++i) { + if (rings[i] != nullptr) { rings[i]->capture_enabled.store(on ? 1u : 0u, std::memory_order_release); } } } void AudioMirror::request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t channels, - std::uint32_t bits, std::uint32_t format_tag) + std::uint32_t bits, std::uint32_t format_tag) { - if (slot >= kMaxAudioStreams) - { + if (slot >= kMaxAudioStreams) { return; } std::lock_guard lock(ops_mutex_); @@ -273,11 +242,9 @@ void AudioMirror::drain_ops() std::lock_guard lock(ops_mutex_); ops.swap(pending_ops_); } - for (const PendingOp& op : ops) - { + for (const PendingOp& op : ops) { AudioRingHeader* ring = (op.slot < kMaxAudioStreams) ? session_rings_[op.slot] : nullptr; - if (ring != nullptr) - { + if (ring != nullptr) { audio_ring_post_op(*ring, op.kind, op.rate, op.channels, op.bits, op.format_tag); } } @@ -286,14 +253,12 @@ void AudioMirror::drain_ops() bool AudioMirror::start(DWORD pid) { stop(); - if (!pid) - { + if (!pid) { set_status("No target process."); return false; } stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!stop_event_) - { + if (!stop_event_) { set_status("CreateEvent failed."); return false; } @@ -305,21 +270,17 @@ bool AudioMirror::start(DWORD pid) void AudioMirror::stop() { - if (stop_event_) - { + if (stop_event_) { SetEvent(stop_event_); } - if (thread_.joinable()) - { + if (thread_.joinable()) { thread_.join(); } - if (stop_event_) - { + if (stop_event_) { CloseHandle(stop_event_); stop_event_ = nullptr; } - for (auto& shm : audio_ring_shm_) - { + for (auto& shm : audio_ring_shm_) { shm.reset(); } running_.store(false, std::memory_order_release); @@ -337,18 +298,14 @@ bool AudioMirror::stop_requested() const bool AudioMirror::wait_for_format(AudioRingHeader* ring, DWORD timeout_ms) { const DWORD end = GetTickCount() + timeout_ms; - for (;;) - { - if (audio_ring_format_ready(*ring)) - { + for (;;) { + if (audio_ring_format_ready(*ring)) { return true; } - if (stop_event_ && WaitForSingleObject(stop_event_, 25) == WAIT_OBJECT_0) - { + if (stop_event_ && WaitForSingleObject(stop_event_, 25) == WAIT_OBJECT_0) { return false; // stopping } - if (GetTickCount() >= end) - { + if (GetTickCount() >= end) { return false; // hook never published a format -> fall back to loopback } } @@ -363,10 +320,8 @@ void AudioMirror::thread_main(DWORD pid) // live the whole session and promote loopback -> hooked the moment a format appears. AudioRingHeader* rings[kMaxAudioStreams] = {}; bool created_primary = false; - for (unsigned i = 0; i < kMaxAudioStreams; ++i) - { - if (audio_ring_shm_[i].create(audio_ring_name(pid, i), audio_ring_total_size(kAudioRingCapacity))) - { + for (unsigned i = 0; i < kMaxAudioStreams; ++i) { + if (audio_ring_shm_[i].create(audio_ring_name(pid, i), audio_ring_total_size(kAudioRingCapacity))) { rings[i] = audio_ring_shm_[i].as(); audio_ring_init(*rings[i], kAudioRingCapacity); created_primary = created_primary || (i == 0); @@ -374,17 +329,13 @@ void AudioMirror::thread_main(DWORD pid) session_rings_[i] = rings[i]; // visible to drain_ops on this (audio) thread } - if (!created_primary) - { + if (!created_primary) { // Couldn't create the hook's ring -> loopback only (no promote target). set_fallback_reason("Couldn't create the audio ring; using loopback (echo)."); - if (!stop_requested()) - { + if (!stop_requested()) { run_loopback(pid, nullptr); } - } - else - { + } else { // Prefer the hooked (no-echo) path. While it isn't ready, run loopback (echo) so // guests still hear audio, but watch the ring and promote to hooked the instant the // hook publishes a format. A short wait first catches the fast cases (exact format / @@ -393,37 +344,29 @@ void AudioMirror::thread_main(DWORD pid) // that gap and the promote hands off seamlessly. constexpr DWORD kHookWaitMs = 1200; bool format_verified = false; // run the two-path correlation verify/correct at most once - for (;;) - { - if (stop_requested()) - { + for (;;) { + if (stop_requested()) { break; } set_status("Waiting for render-hook…"); bool watch_for_promote = true; - if (wait_for_format(rings[0], kHookWaitMs)) - { + if (wait_for_format(rings[0], kHookWaitMs)) { set_fallback_reason({}); // hooked path is taking over const HookedResult r = run_hooked(rings); - if (r == HookedResult::Stopped) - { + if (r == HookedResult::Stopped) { break; // ran to a clean stop } - if (r == HookedResult::Reinit) - { + if (r == HookedResult::Reinit) { continue; // hook re-published (re-measure / override) -> re-read the new format } - if (stop_requested()) - { + if (stop_requested()) { break; } // run_hooked failed to initialize (the game's format isn't renderable here). // That won't fix itself, so don't bounce back to it -- stay on loopback. set_fallback_reason("Render-hook format isn't renderable on this endpoint; using loopback (echo)."); watch_for_promote = false; - } - else if (!stop_requested()) - { + } else if (!stop_requested()) { set_fallback_reason( "Render-hook hasn't published a format yet; using loopback (echo) -- will switch to " "hooked automatically once it does."); @@ -433,8 +376,7 @@ void AudioMirror::thread_main(DWORD pid) // and correct it through the existing override channel -- this hardens the cadence // method's intermittent pitch-shift. It's hidden inside the measurement gap loopback // already covers, so exact streams (format published immediately) never pay for it. - if (!format_verified) - { + if (!format_verified) { format_verified = true; // recover_layout: correlate the full format (rate AND channels/bit-depth), so a // game rendering a different layout than the device is corrected too, not just @@ -442,8 +384,7 @@ void AudioMirror::thread_main(DWORD pid) // a silent game, or a genuinely ambiguous identical-channel layout). const FormatVerification fv = verify_stream_format(pid, rings[0], /*window_ms=*/900, /*recover_layout=*/true); - if (fv.ok) - { + if (fv.ok) { set_status("Verified render-hook format by correlation."); audio_ring_post_op(*rings[0], AudioRingOp_Override, fv.rate, fv.channels, fv.bits, fv.format_tag); @@ -452,8 +393,7 @@ void AudioMirror::thread_main(DWORD pid) } enable_capture(rings, false); // game audible locally so loopback can capture it - if (!run_loopback(pid, watch_for_promote ? rings[0] : nullptr)) - { + if (!run_loopback(pid, watch_for_promote ? rings[0] : nullptr)) { break; // stopped (not a promote) } // Promoted: a format appeared -> loop and try the hooked path again. @@ -461,24 +401,20 @@ void AudioMirror::thread_main(DWORD pid) } enable_capture(rings, false); - for (unsigned i = 0; i < kMaxAudioStreams; ++i) - { + for (unsigned i = 0; i < kMaxAudioStreams; ++i) { session_rings_[i] = nullptr; // audio thread owns this; cleared before unmapping } - for (auto& shm : audio_ring_shm_) - { + for (auto& shm : audio_ring_shm_) { shm.reset(); } - if (running_.load(std::memory_order_acquire)) - { + if (running_.load(std::memory_order_acquire)) { running_.store(false, std::memory_order_release); set_status("Stopped."); } source_.store(Source::None, std::memory_order_relaxed); - if (com_ok) - { + if (com_ok) { CoUninitialize(); } } @@ -501,8 +437,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) const unsigned bits = primary->bits; const unsigned tag = primary->format_tag; const unsigned block_align = primary->block_align ? primary->block_align : channels * (bits / 8); - if (rate == 0 || channels == 0 || block_align == 0) - { + if (rate == 0 || channels == 0 || block_align == 0) { enable_capture(rings, false); // let the game play locally again return HookedResult::Failed; } @@ -515,8 +450,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) wfx.Format.wBitsPerSample = static_cast(bits); wfx.Format.nBlockAlign = static_cast(block_align); wfx.Format.nAvgBytesPerSec = block_align * rate; - if (channels > 2 || bits > 16) - { + if (channels > 2 || bits > 16) { wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; wfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); wfx.Samples.wValidBitsPerSample = static_cast(bits); @@ -532,11 +466,8 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) wfx.dwChannelMask = (channels >= 32) ? 0xFFFFFFFFu : ((1u << channels) - 1u); break; } - wfx.SubFormat = - (tag == WAVE_FORMAT_IEEE_FLOAT) ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; - } - else - { + wfx.SubFormat = (tag == WAVE_FORMAT_IEEE_FLOAT) ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; + } else { wfx.Format.wFormatTag = static_cast(tag ? tag : WAVE_FORMAT_PCM); wfx.Format.cbSize = 0; } @@ -547,22 +478,18 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) HookedResult result = HookedResult::Stopped; auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); }; - do - { - if (!ep.activate(fail)) - { + do { + if (!ep.activate(fail)) { break; } - const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | - AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; - if (FAILED(ep.initialize(fmt, flags))) - { + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM + | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + if (FAILED(ep.initialize(fmt, flags))) { // The game's format isn't renderable here (rare). Bail to loopback. break; } started = true; // past the point where falling back is clean - if (!ep.wire(fail)) - { + if (!ep.wire(fail)) { break; } IAudioClient* render_client = ep.client; @@ -582,8 +509,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) std::vector temp(static_cast(render_frames) * frame_bytes); std::vector acc(static_cast(render_frames) * channels); - if (const HRESULT hr = render_client->Start(); FAILED(hr)) - { + if (const HRESULT hr = render_client->Start(); FAILED(hr)) { set_error("Render Start", hr); break; } @@ -595,23 +521,19 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) running_.store(true, std::memory_order_release); HANDLE waits[2] = {stop_event_, ep.event}; - for (;;) - { + for (;;) { const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200); - if (w == WAIT_OBJECT_0) - { + if (w == WAIT_OBJECT_0) { break; // stop requested } drain_ops(); // post any queued operator ops (re-measure / override) to the hook - if (primary->format_generation.load(std::memory_order_acquire) != start_gen) - { + if (primary->format_generation.load(std::memory_order_acquire) != start_gen) { result = HookedResult::Reinit; // hook re-published -> re-read the new format break; } UINT32 padding = 0; - if (FAILED(render_client->GetCurrentPadding(&padding))) - { + if (FAILED(render_client->GetCurrentPadding(&padding))) { continue; } const UINT32 avail = render_frames - padding; @@ -622,43 +544,35 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) const UINT32 have = static_cast(ring_bytes / frame_bytes); const UINT32 to_write = pacer.pump(avail, have, padding); { - if (to_write > 0) - { + if (to_write > 0) { // Active streams = same format as primary (so they can be summed). // Streams with a different format are still silenced by the hook (no // echo) but can't be mixed here without resampling -> skipped. unsigned active[kMaxAudioStreams]; unsigned n_active = 0; - for (unsigned i = 0; i < kMaxAudioStreams; ++i) - { + for (unsigned i = 0; i < kMaxAudioStreams; ++i) { AudioRingHeader* r = rings[i]; - if (r == nullptr) - { + if (r == nullptr) { continue; } - if (i == 0 || (audio_ring_format_ready(*r) && r->sample_rate == rate && - r->channels == channels && r->bits == bits && r->format_tag == tag)) - { + if (i == 0 + || (audio_ring_format_ready(*r) && r->sample_rate == rate && r->channels == channels + && r->bits == bits && r->format_tag == tag)) { active[n_active++] = i; } } BYTE* dst = nullptr; - if (SUCCEEDED(render->GetBuffer(to_write, &dst))) - { + if (SUCCEEDED(render->GetBuffer(to_write, &dst))) { const std::uint32_t want_bytes = to_write * static_cast(frame_bytes); - if (n_active <= 1 || !mixer_ok) - { + if (n_active <= 1 || !mixer_ok) { // Single stream (the common case) or an unmixable format: // passthrough the primary, byte-for-byte (no mixer overhead). audio_ring_pop(*primary, dst, want_bytes); - } - else - { + } else { const std::uint32_t samples = to_write * channels; std::fill(acc.begin(), acc.begin() + samples, 0.0f); - for (unsigned k = 0; k < n_active; ++k) - { + for (unsigned k = 0; k < n_active; ++k) { std::memset(temp.data(), 0, want_bytes); // zero-fill short reads audio_ring_pop(*rings[active[k]], temp.data(), want_bytes); mix_add(acc.data(), temp.data(), samples, tag, bits); @@ -677,13 +591,11 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) // On a re-init (format changed) keep capturing so the rebuilt render client picks up // seamlessly; otherwise free the game's local playback (stop / fall back to loopback). - if (result != HookedResult::Reinit) - { + if (result != HookedResult::Reinit) { enable_capture(rings, false); } - if (!started) - { + if (!started) { // Never got a working render client; let the caller try loopback. Capture is // already disabled above so loopback hears the game. return HookedResult::Failed; @@ -701,17 +613,14 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) ProcessLoopbackCapture capture; auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); }; - do - { - if (!ep.activate(fail)) - { + do { + if (!ep.activate(fail)) { break; } // Capture and render share one format (the output endpoint's mix format); // WASAPI converts the captured process audio into it. HRESULT hr = ep.client->GetMixFormat(&fmt); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("GetMixFormat", hr); break; } @@ -719,13 +628,11 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) channels_.store(fmt->nChannels, std::memory_order_relaxed); hr = ep.initialize(fmt, AUDCLNT_STREAMFLAGS_EVENTCALLBACK); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("Render Initialize", hr); break; } - if (!ep.wire(fail)) - { + if (!ep.wire(fail)) { break; } IAudioClient* render_client = ep.client; @@ -743,14 +650,12 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) // Capture pushes packets straight into the render ring. if (!capture.start(pid, fmt, [&ring, frame_bytes](const BYTE* data, UINT32 frames, bool silent) { ring.push(data, static_cast(frames) * frame_bytes, silent); - })) - { + })) { fail("Capture start", E_FAIL); break; } - if (FAILED(hr = render_client->Start())) - { + if (FAILED(hr = render_client->Start())) { fail("Render Start", hr); break; } @@ -762,44 +667,36 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) running_.store(true, std::memory_order_release); HANDLE waits[2] = {stop_event_, ep.event}; - for (;;) - { + for (;;) { const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200); - if (w == WAIT_OBJECT_0) - { + if (w == WAIT_OBJECT_0) { break; } - if (!capture.running()) - { + if (!capture.running()) { set_status(capture.status()); break; } drain_ops(); // operator ops (re-measure / override) reach the hook even on loopback // Auto-promote: the hook published a format -> hand back so the caller switches // to the no-echo hooked path (the rings stayed live the whole time). - if (promote_ring != nullptr && audio_ring_format_ready(*promote_ring)) - { + if (promote_ring != nullptr && audio_ring_format_ready(*promote_ring)) { set_status("Render-hook ready -- switching to hooked (no echo)…"); promote = true; break; } UINT32 padding = 0; - if (FAILED(render_client->GetCurrentPadding(&padding))) - { + if (FAILED(render_client->GetCurrentPadding(&padding))) { continue; } const UINT32 avail = render_frames - padding; - buffered_ms_.store( - static_cast(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec), - std::memory_order_relaxed); + buffered_ms_.store(static_cast(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec), + std::memory_order_relaxed); const UINT32 have = static_cast(ring.available() / frame_bytes); const UINT32 to_write = pacer.pump(avail, have, padding); - if (to_write > 0) - { + if (to_write > 0) { BYTE* dst = nullptr; - if (SUCCEEDED(render->GetBuffer(to_write, &dst))) - { + if (SUCCEEDED(render->GetBuffer(to_write, &dst))) { ring.pop(dst, static_cast(to_write) * frame_bytes); render->ReleaseBuffer(to_write, 0); } @@ -810,8 +707,7 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) } while (false); capture.stop(); - if (fmt) - { + if (fmt) { CoTaskMemFree(fmt); } return promote; // true = hook caught up, caller should switch to hooked diff --git a/host/src/audio/audio_loopback.hpp b/host/src/audio/audio_loopback.hpp index e62e909..ca4b032 100644 --- a/host/src/audio/audio_loopback.hpp +++ b/host/src/audio/audio_loopback.hpp @@ -22,12 +22,10 @@ #include "coop/protocol.hpp" // kMaxAudioStreams #include "coop/shared_memory.hpp" -namespace coop -{ +namespace coop { -class AudioMirror -{ -public: +class AudioMirror { + public: AudioMirror() = default; ~AudioMirror(); @@ -42,49 +40,29 @@ public: // True once the audio thread is actively mirroring (false while starting or // after a failure). - [[nodiscard]] bool running() const - { - return running_.load(std::memory_order_acquire); - } + [[nodiscard]] bool running() const { return running_.load(std::memory_order_acquire); } // The process currently targeted (0 if stopped). Updated synchronously by // start()/stop() so the UI can detect target changes without races. - [[nodiscard]] DWORD target_pid() const - { - return pid_; - } + [[nodiscard]] DWORD target_pid() const { return pid_; } - [[nodiscard]] unsigned sample_rate() const - { - return sample_rate_.load(std::memory_order_relaxed); - } - [[nodiscard]] unsigned channels() const - { - return channels_.load(std::memory_order_relaxed); - } + [[nodiscard]] unsigned sample_rate() const { return sample_rate_.load(std::memory_order_relaxed); } + [[nodiscard]] unsigned channels() const { return channels_.load(std::memory_order_relaxed); } // Audio currently buffered between capture and the output device, in ms — a // health/latency proxy (rises if the consumer can't keep up). 0 when stopped. - [[nodiscard]] unsigned buffered_ms() const - { - return buffered_ms_.load(std::memory_order_relaxed); - } + [[nodiscard]] unsigned buffered_ms() const { return buffered_ms_.load(std::memory_order_relaxed); } // Which capture path is active, for the UI's source indicator. - enum class Source - { + enum class Source { None, Hooked, // shared audio ring from the render-hook (no echo) Loopback, // WASAPI process loopback (echo) }; - [[nodiscard]] Source source() const - { - return source_.load(std::memory_order_relaxed); - } + [[nodiscard]] Source source() const { return source_.load(std::memory_order_relaxed); } [[nodiscard]] const char* source_name() const { - switch (source()) - { + switch (source()) { case Source::Hooked: return "Hooked"; // echo depends on the format provenance; the panel shows it case Source::Loopback: @@ -106,11 +84,10 @@ public: void request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate = 0, std::uint32_t channels = 0, std::uint32_t bits = 0, std::uint32_t format_tag = 0); -private: + private: void thread_main(DWORD pid); // Outcome of a hooked render session. - enum class HookedResult - { + enum class HookedResult { Stopped, // clean stop (mirror stopping) -> done Failed, // setup failed (format not renderable) -> caller falls back to loopback Reinit, // the hook re-published the format (re-measure/override) -> re-read and retry @@ -135,13 +112,12 @@ private: HANDLE stop_event_ = nullptr; DWORD pid_ = 0; - SharedMemory audio_ring_shm_[kMaxAudioStreams]; // per-stream rings (coop_audio_[_]) + SharedMemory audio_ring_shm_[kMaxAudioStreams]; // per-stream rings (coop_audio_[_]) AudioRingHeader* session_rings_[kMaxAudioStreams] = {}; // set on the audio thread for the session // Operator ops queued by request_op (any thread) and applied to the rings on the // audio thread (which owns the mappings). Guarded by ops_mutex_. - struct PendingOp - { + struct PendingOp { unsigned slot; std::uint32_t kind, rate, channels, bits, format_tag; }; diff --git a/host/src/audio/audio_mix.hpp b/host/src/audio/audio_mix.hpp index ef59a37..12d00b2 100644 --- a/host/src/audio/audio_mix.hpp +++ b/host/src/audio/audio_mix.hpp @@ -6,8 +6,7 @@ #include #include -namespace coop -{ +namespace coop { // WAVE_FORMAT_* values used here (kept local to avoid an mmreg.h dependency). inline constexpr std::uint32_t kWaveFormatPcm = 1; @@ -31,19 +30,14 @@ inline float soft_clip(float x) inline void mix_add(float* acc, const std::uint8_t* src, std::uint32_t samples, std::uint32_t format_tag, std::uint32_t bits) { - if (format_tag == kWaveFormatFloat && bits == 32) - { + if (format_tag == kWaveFormatFloat && bits == 32) { const auto* f = reinterpret_cast(src); - for (std::uint32_t i = 0; i < samples; ++i) - { + for (std::uint32_t i = 0; i < samples; ++i) { acc[i] += f[i]; } - } - else if (format_tag == kWaveFormatPcm && bits == 16) - { + } else if (format_tag == kWaveFormatPcm && bits == 16) { const auto* s = reinterpret_cast(src); - for (std::uint32_t i = 0; i < samples; ++i) - { + for (std::uint32_t i = 0; i < samples; ++i) { acc[i] += static_cast(s[i]) / 32768.0f; } } @@ -54,19 +48,14 @@ inline void mix_add(float* acc, const std::uint8_t* src, std::uint32_t samples, inline void mix_store(std::uint8_t* dst, const float* acc, std::uint32_t samples, std::uint32_t format_tag, std::uint32_t bits) { - if (format_tag == kWaveFormatFloat && bits == 32) - { + if (format_tag == kWaveFormatFloat && bits == 32) { auto* f = reinterpret_cast(dst); - for (std::uint32_t i = 0; i < samples; ++i) - { + for (std::uint32_t i = 0; i < samples; ++i) { f[i] = soft_clip(acc[i]); } - } - else if (format_tag == kWaveFormatPcm && bits == 16) - { + } else if (format_tag == kWaveFormatPcm && bits == 16) { auto* s = reinterpret_cast(dst); - for (std::uint32_t i = 0; i < samples; ++i) - { + for (std::uint32_t i = 0; i < samples; ++i) { int v = static_cast(soft_clip(acc[i]) * 32767.0f); v = v > 32767 ? 32767 : (v < -32768 ? -32768 : v); s[i] = static_cast(v); diff --git a/host/src/audio/audio_overrides.cpp b/host/src/audio/audio_overrides.cpp index 04b2c43..0c03a7f 100644 --- a/host/src/audio/audio_overrides.cpp +++ b/host/src/audio/audio_overrides.cpp @@ -11,14 +11,11 @@ #include "coop/tool_paths.hpp" #include "util/utf8.hpp" -namespace coop -{ -namespace -{ +namespace coop { +namespace { std::wstring to_lower(std::wstring s) { - for (wchar_t& c : s) - { + for (wchar_t& c : s) { c = static_cast(::towlower(c)); } return s; @@ -29,8 +26,7 @@ std::wstring to_lower(std::wstring s) AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path)) { - if (path_.empty()) - { + if (path_.empty()) { path_ = exe_directory() + L"coop_audio_overrides.ini"; } } @@ -45,46 +41,38 @@ void AudioOverrideStore::load() { map_.clear(); std::ifstream f(path_.c_str()); - if (!f) - { + if (!f) { return; } std::string line; - while (std::getline(f, line)) - { + while (std::getline(f, line)) { // " = "; skip blank lines and # comments. const std::size_t hash = line.find('#'); - if (hash != std::string::npos) - { + if (hash != std::string::npos) { line.resize(hash); } const std::size_t eq = line.find('='); - if (eq == std::string::npos) - { + if (eq == std::string::npos) { continue; } std::string name = line.substr(0, eq); // trim trailing/leading whitespace from the name - while (!name.empty() && std::isspace(static_cast(name.back()))) - { + while (!name.empty() && std::isspace(static_cast(name.back()))) { name.pop_back(); } std::size_t b = 0; - while (b < name.size() && std::isspace(static_cast(name[b]))) - { + while (b < name.size() && std::isspace(static_cast(name[b]))) { ++b; } name = name.substr(b); - if (name.empty()) - { + if (name.empty()) { continue; } std::istringstream vs(line.substr(eq + 1)); AudioFormatOverride fmt; std::string tag; vs >> fmt.rate >> fmt.channels >> fmt.bits >> tag; - if (!fmt.valid()) - { + if (!fmt.valid()) { continue; } fmt.format_tag = (tag == "float") ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM; @@ -95,8 +83,7 @@ void AudioOverrideStore::load() bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverride& out) const { const auto it = map_.find(key_of(image_name)); - if (it == map_.end()) - { + if (it == map_.end()) { return false; } out = it->second; @@ -106,8 +93,7 @@ bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverrid void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed) { const std::wstring key = key_of(image_name); - if (differed != nullptr) - { + if (differed != nullptr) { const auto it = map_.find(key); *differed = (it != map_.end() && it->second != fmt); } @@ -118,14 +104,12 @@ void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOv void AudioOverrideStore::save() const { std::ofstream f(path_.c_str(), std::ios::trunc); - if (!f) - { + if (!f) { return; } f << "# CoopAllTheThings per-game audio format overrides (auto-managed)\n"; f << "# = \n"; - for (const auto& [name, fmt] : map_) - { + for (const auto& [name, fmt] : map_) { f << narrow(name) << " = " << fmt.rate << ' ' << fmt.channels << ' ' << fmt.bits << ' ' << (fmt.format_tag == WAVE_FORMAT_IEEE_FLOAT ? "float" : "pcm") << '\n'; } diff --git a/host/src/audio/audio_overrides.hpp b/host/src/audio/audio_overrides.hpp index f965a9f..5139da6 100644 --- a/host/src/audio/audio_overrides.hpp +++ b/host/src/audio/audio_overrides.hpp @@ -13,33 +13,24 @@ #include #include -namespace coop -{ +namespace coop { -struct AudioFormatOverride -{ +struct AudioFormatOverride { std::uint32_t rate = 0; std::uint32_t channels = 0; std::uint32_t bits = 0; std::uint32_t format_tag = 0; // WAVE_FORMAT_PCM (1) / WAVE_FORMAT_IEEE_FLOAT (3) - [[nodiscard]] bool valid() const - { - return rate != 0 && channels != 0 && bits != 0; - } + [[nodiscard]] bool valid() const { return rate != 0 && channels != 0 && bits != 0; } bool operator==(const AudioFormatOverride& o) const { return rate == o.rate && channels == o.channels && bits == o.bits && format_tag == o.format_tag; } - bool operator!=(const AudioFormatOverride& o) const - { - return !(*this == o); - } + bool operator!=(const AudioFormatOverride& o) const { return !(*this == o); } }; -class AudioOverrideStore -{ -public: +class AudioOverrideStore { + public: // `path` empty -> default (exe_dir/coop_audio_overrides.ini). Does not load yet. explicit AudioOverrideStore(std::wstring path = {}); @@ -52,12 +43,9 @@ public: // existing entry for that game differed from `fmt` (caller warns the operator). void set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed = nullptr); - [[nodiscard]] const std::wstring& path() const - { - return path_; - } + [[nodiscard]] const std::wstring& path() const { return path_; } -private: + private: static std::wstring key_of(const std::wstring& image_name); // lowercased basename void save() const; diff --git a/host/src/audio/process_loopback_capture.cpp b/host/src/audio/process_loopback_capture.cpp index 76d81c7..5a51c20 100644 --- a/host/src/audio/process_loopback_capture.cpp +++ b/host/src/audio/process_loopback_capture.cpp @@ -6,16 +6,13 @@ #include #include -namespace coop -{ -namespace -{ +namespace coop { +namespace { // Completion handler for ActivateAudioInterfaceAsync. The call is async even when // used synchronously: it signals `done`, and the caller waits on it. -class ActivateHandler : public IActivateAudioInterfaceCompletionHandler -{ -public: +class ActivateHandler : public IActivateAudioInterfaceCompletionHandler { + public: HANDLE done = CreateEventW(nullptr, FALSE, FALSE, nullptr); HRESULT result = E_FAIL; IAudioClient* client = nullptr; @@ -25,16 +22,13 @@ public: HRESULT activate_hr = E_FAIL; IUnknown* punk = nullptr; HRESULT hr = op->GetActivateResult(&activate_hr, &punk); - if (SUCCEEDED(hr)) - { + if (SUCCEEDED(hr)) { hr = activate_hr; } - if (SUCCEEDED(hr) && punk) - { + if (SUCCEEDED(hr) && punk) { hr = punk->QueryInterface(__uuidof(IAudioClient), reinterpret_cast(&client)); } - if (punk) - { + if (punk) { punk->Release(); } result = hr; @@ -44,16 +38,14 @@ public: STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override { - if (riid == __uuidof(IUnknown) || riid == __uuidof(IActivateAudioInterfaceCompletionHandler)) - { + if (riid == __uuidof(IUnknown) || riid == __uuidof(IActivateAudioInterfaceCompletionHandler)) { *ppv = static_cast(this); AddRef(); return S_OK; } // Mark the handler agile; ActivateAudioInterfaceAsync requires an agile // completion handler and otherwise rejects the call (E_ILLEGAL_METHOD_CALL). - if (riid == __uuidof(IAgileObject)) - { + if (riid == __uuidof(IAgileObject)) { *ppv = static_cast(this); AddRef(); return S_OK; @@ -61,25 +53,20 @@ public: *ppv = nullptr; return E_NOINTERFACE; } - STDMETHODIMP_(ULONG) AddRef() override - { - return ++ref_; - } + STDMETHODIMP_(ULONG) AddRef() override { return ++ref_; } STDMETHODIMP_(ULONG) Release() override { const ULONG r = --ref_; - if (r == 0) - { + if (r == 0) { delete this; } return r; } -private: + private: ~ActivateHandler() { - if (done) - { + if (done) { CloseHandle(done); } } @@ -100,26 +87,20 @@ HRESULT activate_loopback_client(DWORD pid, IAudioClient** out) auto* handler = new ActivateHandler(); HRESULT hr = E_FAIL; - if (handler->done) - { + if (handler->done) { IActivateAudioInterfaceAsyncOperation* op = nullptr; - hr = ActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient), - &pv, handler, &op); - if (SUCCEEDED(hr)) - { + hr = ActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient), &pv, handler, + &op); + if (SUCCEEDED(hr)) { WaitForSingleObject(handler->done, INFINITE); hr = handler->result; - if (SUCCEEDED(hr)) - { + if (SUCCEEDED(hr)) { *out = handler->client; // transfer the QueryInterface reference - } - else if (handler->client) - { + } else if (handler->client) { handler->client->Release(); } } - if (op) - { + if (op) { op->Release(); } } @@ -132,19 +113,16 @@ HRESULT activate_loopback_client(DWORD pid, IAudioClient** out) WAVEFORMATEX* default_render_format() { IMMDeviceEnumerator* enumerator = nullptr; - if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)))) - { + if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&enumerator)))) { return nullptr; } IMMDevice* endpoint = nullptr; WAVEFORMATEX* fmt = nullptr; - if (SUCCEEDED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint))) - { + if (SUCCEEDED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint))) { IAudioClient* client = nullptr; - if (SUCCEEDED(endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(&client)))) - { + if (SUCCEEDED( + endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&client)))) { client->GetMixFormat(&fmt); client->Release(); } @@ -174,14 +152,12 @@ void ProcessLoopbackCapture::set_status(std::string s) bool ProcessLoopbackCapture::start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink) { stop(); - if (!pid || !format) - { + if (!pid || !format) { set_status("No target/format."); return false; } stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!stop_event_) - { + if (!stop_event_) { set_status("CreateEvent failed."); return false; } @@ -192,23 +168,19 @@ bool ProcessLoopbackCapture::start(DWORD pid, const WAVEFORMATEX* format, FrameS std::memcpy(fmt_copy.data(), format, fmt_copy.size()); set_status("Starting…"); - thread_ = std::thread(&ProcessLoopbackCapture::thread_main, this, pid, std::move(fmt_copy), - std::move(sink)); + thread_ = std::thread(&ProcessLoopbackCapture::thread_main, this, pid, std::move(fmt_copy), std::move(sink)); return true; } void ProcessLoopbackCapture::stop() { - if (stop_event_) - { + if (stop_event_) { SetEvent(stop_event_); } - if (thread_.joinable()) - { + if (thread_.joinable()) { thread_.join(); } - if (stop_event_) - { + if (stop_event_) { CloseHandle(stop_event_); stop_event_ = nullptr; } @@ -231,18 +203,15 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector format, Fr set_status(buf); }; - do - { + do { HRESULT hr = activate_loopback_client(pid, &client); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("Process loopback activate", hr); break; } capture_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); - if (!capture_event) - { + if (!capture_event) { fail("CreateEvent(capture)", HRESULT_FROM_WIN32(GetLastError())); break; } @@ -250,27 +219,22 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector format, Fr // Process loopback requires shared mode, the LOOPBACK + EVENTCALLBACK flags, // and zero buffer/periodicity (there is no device period to query). hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, - fmt, nullptr); - if (FAILED(hr)) - { + AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, fmt, nullptr); + if (FAILED(hr)) { fail("Capture Initialize", hr); break; } hr = client->SetEventHandle(capture_event); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("Capture SetEventHandle", hr); break; } hr = client->GetService(__uuidof(IAudioCaptureClient), reinterpret_cast(&capture)); - if (FAILED(hr)) - { + if (FAILED(hr)) { fail("GetService(CaptureClient)", hr); break; } - if (FAILED(hr = client->Start())) - { + if (FAILED(hr = client->Start())) { fail("Capture Start", hr); break; } @@ -279,47 +243,38 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector format, Fr running_.store(true, std::memory_order_release); HANDLE waits[2] = {stop_event_, capture_event}; - for (;;) - { + for (;;) { const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200); - if (w == WAIT_OBJECT_0) - { + if (w == WAIT_OBJECT_0) { break; } UINT32 packet = 0; - while (SUCCEEDED(capture->GetNextPacketSize(&packet)) && packet > 0) - { + while (SUCCEEDED(capture->GetNextPacketSize(&packet)) && packet > 0) { BYTE* data = nullptr; UINT32 frames = 0; DWORD flags = 0; - if (FAILED(capture->GetBuffer(&data, &frames, &flags, nullptr, nullptr))) - { + if (FAILED(capture->GetBuffer(&data, &frames, &flags, nullptr, nullptr))) { break; } const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; frames_captured_.fetch_add(frames, std::memory_order_relaxed); - if (!silent && frames > 0) - { + if (!silent && frames > 0) { // Count frames that carry any non-zero sample. const BYTE* p = data; const BYTE* end = data + static_cast(frames) * frame_bytes; bool any = false; - for (; p < end; ++p) - { - if (*p != 0) - { + for (; p < end; ++p) { + if (*p != 0) { any = true; break; } } - if (any) - { + if (any) { nonsilent_frames_.fetch_add(frames, std::memory_order_relaxed); } } - if (sink) - { + if (sink) { sink(data, frames, silent); } capture->ReleaseBuffer(frames); @@ -329,26 +284,21 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector format, Fr client->Stop(); } while (false); - if (running_.load(std::memory_order_acquire)) - { + if (running_.load(std::memory_order_acquire)) { running_.store(false, std::memory_order_release); set_status("Stopped."); } - if (capture) - { + if (capture) { capture->Release(); } - if (client) - { + if (client) { client->Release(); } - if (capture_event) - { + if (capture_event) { CloseHandle(capture_event); } - if (com_ok) - { + if (com_ok) { CoUninitialize(); } } diff --git a/host/src/audio/process_loopback_capture.hpp b/host/src/audio/process_loopback_capture.hpp index 9209b6c..89f90b3 100644 --- a/host/src/audio/process_loopback_capture.hpp +++ b/host/src/audio/process_loopback_capture.hpp @@ -15,16 +15,14 @@ #include // WAVEFORMATEX -namespace coop -{ +namespace coop { // Default render endpoint mix format (caller owns the returned pointer; free with // CoTaskMemFree). Returns nullptr on failure. Requires a COM-initialized thread. WAVEFORMATEX* default_render_format(); -class ProcessLoopbackCapture -{ -public: +class ProcessLoopbackCapture { + public: // Called on the capture thread for each delivered packet. `silent` means the // engine flagged the packet as silence (data may be undefined). using FrameSink = std::function; @@ -41,22 +39,13 @@ public: bool start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink); void stop(); - [[nodiscard]] bool running() const - { - return running_.load(std::memory_order_acquire); - } + [[nodiscard]] bool running() const { return running_.load(std::memory_order_acquire); } [[nodiscard]] std::string status() const; - [[nodiscard]] std::uint64_t frames_captured() const - { - return frames_captured_.load(std::memory_order_relaxed); - } - [[nodiscard]] std::uint64_t nonsilent_frames() const - { - return nonsilent_frames_.load(std::memory_order_relaxed); - } + [[nodiscard]] std::uint64_t frames_captured() const { return frames_captured_.load(std::memory_order_relaxed); } + [[nodiscard]] std::uint64_t nonsilent_frames() const { return nonsilent_frames_.load(std::memory_order_relaxed); } -private: + private: void thread_main(DWORD pid, std::vector format, FrameSink sink); void set_status(std::string s); diff --git a/host/src/audio/render_pacer.hpp b/host/src/audio/render_pacer.hpp index c192d1c..6942aad 100644 --- a/host/src/audio/render_pacer.hpp +++ b/host/src/audio/render_pacer.hpp @@ -22,11 +22,9 @@ #include #include -namespace coop -{ +namespace coop { -struct RenderPacer -{ +struct RenderPacer { std::uint32_t prime_frames = 0; // cushion to (re)build before playback resumes bool primed = false; @@ -37,28 +35,22 @@ struct RenderPacer // Returns the frame count to write (0 while still priming or when the ring is empty). std::uint32_t pump(std::uint32_t avail, std::uint32_t have, std::uint32_t padding) { - if (!primed && have >= prime_frames) - { + if (!primed && have >= prime_frames) { primed = true; } - if (!primed) - { + if (!primed) { return 0; // still building the initial / post-starvation cushion } const std::uint32_t to_write = std::min(avail, have); // Genuine starvation only: the device emptied and the ring has nothing to give. // A partial fill (have < avail) is normal jitter and must NOT trigger a re-prime. - if (padding == 0 && have == 0) - { + if (padding == 0 && have == 0) { primed = false; } return to_write; } - void reset() - { - primed = false; - } + void reset() { primed = false; } }; } // namespace coop diff --git a/host/src/audio_panel.cpp b/host/src/audio_panel.cpp index b662e72..bcafc2b 100644 --- a/host/src/audio_panel.cpp +++ b/host/src/audio_panel.cpp @@ -10,11 +10,9 @@ #include "ui/app_chrome.hpp" #include "util/utf8.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kAmber(1.0f, 0.8f, 0.3f, 1.0f); @@ -22,8 +20,7 @@ const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f); const char* format_tag_name(std::uint32_t tag) { - switch (tag) - { + switch (tag) { case WAVE_FORMAT_PCM: return "PCM"; case WAVE_FORMAT_IEEE_FLOAT: @@ -38,8 +35,7 @@ const char* format_tag_name(std::uint32_t tag) // How the hooked backend learned a stream's format (drives the pitch correctness). const char* audio_format_state_name(std::uint32_t state) { - switch (state) - { + switch (state) { case AudioFormat_Exact: return "known (from game)"; case AudioFormat_Measuring: @@ -57,8 +53,7 @@ const char* audio_format_state_name(std::uint32_t state) ImVec4 audio_format_state_color(std::uint32_t state) { - switch (state) - { + switch (state) { case AudioFormat_Exact: case AudioFormat_Measured: case AudioFormat_Override: @@ -81,20 +76,17 @@ std::string image_basename(const std::wstring& image_path) std::wstring AudioPanel::image_name_from_pid(DWORD pid) { - if (pid == 0) - { + if (pid == 0) { return {}; } HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if (h == nullptr) - { + if (h == nullptr) { return {}; } wchar_t buf[MAX_PATH] = {}; DWORD n = MAX_PATH; std::wstring name; - if (QueryFullProcessImageNameW(h, 0, buf, &n)) - { + if (QueryFullProcessImageNameW(h, 0, buf, &n)) { name.assign(buf, n); } CloseHandle(h); @@ -110,24 +102,20 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid) override_applied_ = false; exact_saved_ = false; } - if (pid == 0 || target_image_.empty() || !mirror_.running() || status.audio_streams_seen == 0) - { + if (pid == 0 || target_image_.empty() || !mirror_.running() || status.audio_streams_seen == 0) { return; } const AudioStreamInfo& s = status.audio_streams[0]; // the primary (mirrored) stream const std::uint32_t st = s.format_state; - if (st == AudioFormat_Exact) - { + if (st == AudioFormat_Exact) { // Ground truth: persist it as this game's override (so a later late-attach is fixed). - if (!exact_saved_) - { + if (!exact_saved_) { exact_saved_ = true; const AudioFormatOverride fmt{s.sample_rate, s.channels, s.bits, s.format_tag}; bool differed = false; overrides_.set(target_image_, fmt, &differed); - if (differed && logger_) - { + if (differed && logger_) { char msg[160]; std::snprintf(msg, sizeof(msg), "%s: exact format %uHz/%uch/%ubit caught -> replaced a DIFFERING saved override", @@ -135,19 +123,14 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid) logger_(LogLevel_Warn, msg); } } - } - else if (st == AudioFormat_Measuring || st == AudioFormat_Measured || st == AudioFormat_LowConfidence) - { + } else if (st == AudioFormat_Measuring || st == AudioFormat_Measured || st == AudioFormat_LowConfidence) { // A guessed stream: if we have a saved override for this game, apply it. - if (!override_applied_) - { + if (!override_applied_) { override_applied_ = true; AudioFormatOverride ov; - if (overrides_.find(target_image_, ov)) - { + if (overrides_.find(target_image_, ov)) { mirror_.request_op(0, AudioRingOp_Override, ov.rate, ov.channels, ov.bits, ov.format_tag); - if (logger_) - { + if (logger_) { char msg[160]; std::snprintf(msg, sizeof(msg), "%s: applied saved audio override %uHz/%uch/%ubit", image_basename(target_image_).c_str(), ov.rate, ov.channels, ov.bits); @@ -161,12 +144,10 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid) void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) { DWORD pid = 0; - if (target_ != nullptr && IsWindow(target_)) - { + if (target_ != nullptr && IsWindow(target_)) { GetWindowThreadProcessId(target_, &pid); } - if (dev_pid_ != 0) - { + if (dev_pid_ != 0) { pid = dev_pid_; // test harness: a windowless target (e.g. coop_tone) has no HWND } const bool have_target = pid != 0; @@ -175,26 +156,20 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) ImGui::Begin("Audio mirror"); ImGui::BeginDisabled(!have_target); - if (ImGui::Checkbox("Mirror game audio", &enabled_)) - { - if (!enabled_) - { + if (ImGui::Checkbox("Mirror game audio", &enabled_)) { + if (!enabled_) { mirror_.stop(); } } ImGui::EndDisabled(); - if (!have_target) - { + if (!have_target) { ImGui::TextDisabled("Inject into a game first (its audio is the source)."); } // Start when enabled and the target process changes; stop if it disappears. - if (enabled_ && pid != 0 && mirror_.target_pid() != pid) - { + if (enabled_ && pid != 0 && mirror_.target_pid() != pid) { mirror_.start(pid); - } - else if (enabled_ && pid == 0 && mirror_.target_pid() != 0) - { + } else if (enabled_ && pid == 0 && mirror_.target_pid() != 0) { mirror_.stop(); } @@ -214,14 +189,12 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) demo_ ? std::string("render-hook did not publish a format in time; using WASAPI process loopback.") : mirror_.fallback_reason(); - if (running) - { + if (running) { const bool hooked = src == AudioMirror::Source::Hooked; ImGui::TextColored(kGreen, "Mirroring %u Hz, %u ch", m_rate, m_ch); ImGui::Text("Source:"); ImGui::SameLine(); - if (hooked) - { + if (hooked) { // The hooked path only silences (no echo) an EXACT / override format, whose frame // size is known. A guessed stream is captured but not silenced (silencing a guessed // buffer could over-write it), so the game stays audible -- an echo. Make that clear. @@ -229,9 +202,7 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) const bool no_echo = (st == AudioFormat_Exact || st == AudioFormat_Override); ImGui::TextColored(no_echo ? kGreen : kAmber, "Hooked (%s)", no_echo ? "no echo" : "echo -- guessed format"); - } - else - { + } else { ImGui::TextColored(kAmber, "%s", demo_ ? "Loopback (echo)" : mirror_.source_name()); } @@ -240,26 +211,21 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) // captured post-mix at the device endpoint format, so it's always known-correct. ImGui::Text("Format:"); ImGui::SameLine(); - if (hooked) - { + if (hooked) { const std::uint32_t st = status.audio_streams[0].format_state; // [0] is the primary ImGui::TextColored(audio_format_state_color(st), "%s", audio_format_state_name(st)); - } - else - { + } else { ImGui::TextColored(kGreen, "device endpoint (known, post-mix)"); } ImGui::Text("Buffered: %4u ms", m_buffered); } - if (!mirror_status.empty()) - { + if (!mirror_status.empty()) { ImGui::TextWrapped("%s", mirror_status.c_str()); } // Why we're on loopback instead of the no-echo hooked path (empty when hooked). Amber // because it's a degraded-but-working state that auto-resolves when the hook catches up. - if (!reason.empty()) - { + if (!reason.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, kAmber); ImGui::TextWrapped("Why loopback: %s", reason.c_str()); ImGui::PopStyleColor(); @@ -267,25 +233,18 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) // Only the loopback path leaves the game audible locally (the echo); the // hooked path silences it, so don't warn there. - if (src == AudioMirror::Source::Loopback) - { + if (src == AudioMirror::Source::Loopback) { bool audio_hook_on = false; - const std::uint32_t hn = - status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries; - for (std::uint32_t i = 0; i < hn; ++i) - { - if (status.hook_entries[i].subsystem == HookSubsys_Audio && status.hook_entries[i].installed) - { + const std::uint32_t hn = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries; + for (std::uint32_t i = 0; i < hn; ++i) { + if (status.hook_entries[i].subsystem == HookSubsys_Audio && status.hook_entries[i].installed) { audio_hook_on = true; break; } } - if (audio_hook_on) - { + if (audio_hook_on) { ImGui::TextDisabled("Game audio also plays locally (echo)."); - } - else - { + } else { ImGui::TextDisabled("Audio render-hook is off -> loopback (echo). Enable it in the Injection panel."); } } @@ -296,14 +255,12 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) // (debug details) shows each stream's format/provenance/activity. ImGui::Separator(); ImGui::Text("Render streams: %u", status.audio_streams_seen); - if (status.audio_streams_seen > kMaxAudioStreams) - { + if (status.audio_streams_seen > kMaxAudioStreams) { ImGui::SameLine(); ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams); } - if (!debug_details) - { + if (!debug_details) { record_panel_fit("Audio"); ImGui::End(); return; // the per-stream table below is diagnostic detail @@ -312,9 +269,7 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) const std::uint32_t rows = std::min(status.audio_streams_seen, kMaxAudioStreams); const double now = ImGui::GetTime(); const bool resample = (now - rate_base_time_) >= 0.5; // recompute frames/s ~2x a second - if (rows > 0 && - ImGui::BeginTable("audio_streams", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) - { + if (rows > 0 && ImGui::BeginTable("audio_streams", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("#"); ImGui::TableSetupColumn("role"); ImGui::TableSetupColumn("format"); @@ -322,23 +277,19 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) ImGui::TableSetupColumn("frames"); ImGui::TableSetupColumn("live"); ImGui::TableHeadersRow(); - for (std::uint32_t i = 0; i < rows; ++i) - { + for (std::uint32_t i = 0; i < rows; ++i) { const AudioStreamInfo& s = status.audio_streams[i]; // Debounced activity: remember when this stream last advanced, and call it // live for a short window afterwards so bursty releases don't flicker. - if (s.frames_rendered > prev_frames_[i]) - { + if (s.frames_rendered > prev_frames_[i]) { last_active_[i] = now; } prev_frames_[i] = s.frames_rendered; const bool live = last_active_[i] > 0.0 && (now - last_active_[i]) < 0.4; - if (resample) - { + if (resample) { const double dt = now - rate_base_time_; - frames_per_s_[i] = - dt > 0.0 ? static_cast(s.frames_rendered - rate_base_frames_[i]) / dt : 0.0; + frames_per_s_[i] = dt > 0.0 ? static_cast(s.frames_rendered - rate_base_frames_[i]) / dt : 0.0; rate_base_frames_[i] = s.frames_rendered; } @@ -346,43 +297,35 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) ImGui::TableNextColumn(); ImGui::Text("%u", i); ImGui::TableNextColumn(); - ImGui::TextColored(s.is_primary ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f), - "%s", s.is_primary ? "primary" : "extra"); + ImGui::TextColored(s.is_primary ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", + s.is_primary ? "primary" : "extra"); ImGui::TableNextColumn(); - ImGui::Text("%u Hz %uch %u-bit %s", s.sample_rate, s.channels, s.bits, - format_tag_name(s.format_tag)); + ImGui::Text("%u Hz %uch %u-bit %s", s.sample_rate, s.channels, s.bits, format_tag_name(s.format_tag)); ImGui::TableNextColumn(); - ImGui::TextColored(audio_format_state_color(s.format_state), "%s", - audio_format_state_name(s.format_state)); + ImGui::TextColored(audio_format_state_color(s.format_state), "%s", audio_format_state_name(s.format_state)); ImGui::TableNextColumn(); ImGui::Text("%llu", static_cast(s.frames_rendered)); ImGui::TableNextColumn(); - if (live) - { + if (live) { ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "live"); ImGui::SameLine(); ImGui::TextDisabled("%6.0f/s", frames_per_s_[i]); - } - else - { + } else { ImGui::TextDisabled("idle"); } } ImGui::EndTable(); } - if (resample) - { + if (resample) { rate_base_time_ = now; } // --- Operator controls: re-measure / override the primary stream's format ----- // For when detection is wrong (re-measure) or unrecoverable (override the channels/ // bit-depth the hook had to assume). Only meaningful while mirroring is active. - if (running) - { + if (running) { ImGui::SeparatorText("Fix the primary stream (debug)"); - if (ImGui::Button("Re-measure rate")) - { + if (ImGui::Button("Re-measure rate")) { mirror_.request_op(0, AudioRingOp_Remeasure); } ImGui::SameLine(); @@ -395,25 +338,26 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) ImGui::InputInt("ch", &ov_channels_, 0, 0); ImGui::SameLine(); ImGui::SetNextItemWidth(90.0f); - ImGui::Combo("##ovbits", &ov_bits_idx_, "16-bit\0" "32-bit\0"); + ImGui::Combo("##ovbits", &ov_bits_idx_, + "16-bit\0" + "32-bit\0"); ImGui::SameLine(); ImGui::SetNextItemWidth(80.0f); - ImGui::Combo("##ovfmt", &ov_fmt_idx_, "PCM\0" "float\0"); + ImGui::Combo("##ovfmt", &ov_fmt_idx_, + "PCM\0" + "float\0"); ImGui::SameLine(); - if (ImGui::Button("Override")) - { + if (ImGui::Button("Override")) { ov_rate_ = std::clamp(ov_rate_, 8000, 384000); ov_channels_ = std::clamp(ov_channels_, 1, 8); const std::uint32_t bits = ov_bits_idx_ == 0 ? 16u : 32u; - const std::uint32_t tag = - ov_fmt_idx_ == 1 ? static_cast(WAVE_FORMAT_IEEE_FLOAT) - : static_cast(WAVE_FORMAT_PCM); + const std::uint32_t tag = ov_fmt_idx_ == 1 ? static_cast(WAVE_FORMAT_IEEE_FLOAT) + : static_cast(WAVE_FORMAT_PCM); const AudioFormatOverride fmt{static_cast(ov_rate_), static_cast(ov_channels_), bits, tag}; mirror_.request_op(0, AudioRingOp_Override, fmt.rate, fmt.channels, fmt.bits, fmt.format_tag); // Persist it for this game so the correction sticks across launches. - if (!target_image_.empty()) - { + if (!target_image_.empty()) { overrides_.set(target_image_, fmt); override_applied_ = true; // don't let manage_overrides re-apply an older saved value } diff --git a/host/src/audio_panel.hpp b/host/src/audio_panel.hpp index fef52f6..e310144 100644 --- a/host/src/audio_panel.hpp +++ b/host/src/audio_panel.hpp @@ -13,30 +13,19 @@ #include "audio/audio_overrides.hpp" #include "ipc/ipc_server.hpp" -namespace coop -{ +namespace coop { -class AudioPanel -{ -public: - AudioPanel() - { - overrides_.load(); - } +class AudioPanel { + public: + AudioPanel() { overrides_.load(); } // The window whose process audio to mirror (0 if none); typically the // injected game's HWND. - void set_target(HWND target) - { - target_ = target; - } + void set_target(HWND target) { target_ = target; } // Wire a sink for host-side log lines (override-overwrite warnings etc.). main // connects this to the injection panel's Log-window channel. - void set_logger(std::function logger) - { - logger_ = std::move(logger); - } + void set_logger(std::function logger) { logger_ = std::move(logger); } // `status` is the hook's back-channel, for the render-stream view. With // `debug_details` on, the per-stream table is shown. @@ -46,50 +35,26 @@ public: // loopback mirror were running with long status/reason strings -- without a live // AudioMirror, so the fit test can measure the panel's worst-case size. Never set in // the shipping host (the render path is identical, just fed synthetic values). - void dev_set_demo(bool on) - { - demo_ = on; - } + void dev_set_demo(bool on) { demo_ = on; } #ifdef COOP_TEST_HARNESS // Test-harness hooks (debug builds only): drive the real audio code paths and read // state back, incl. targeting a windowless process by pid (coop_tone has no window). - void dev_set_enabled(bool on) - { - enabled_ = on; - } - void dev_set_pid(DWORD pid) - { - dev_pid_ = pid; - } - void dev_request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t ch, - std::uint32_t bits, std::uint32_t tag) + void dev_set_enabled(bool on) { enabled_ = on; } + void dev_set_pid(DWORD pid) { dev_pid_ = pid; } + void dev_request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t ch, std::uint32_t bits, + std::uint32_t tag) { mirror_.request_op(slot, kind, rate, ch, bits, tag); } - [[nodiscard]] bool dev_running() const - { - return mirror_.running(); - } - [[nodiscard]] unsigned dev_rate() const - { - return mirror_.sample_rate(); - } - [[nodiscard]] unsigned dev_channels() const - { - return mirror_.channels(); - } - [[nodiscard]] std::string dev_source() const - { - return mirror_.source_name(); - } - [[nodiscard]] std::string dev_reason() const - { - return mirror_.fallback_reason(); - } + [[nodiscard]] bool dev_running() const { return mirror_.running(); } + [[nodiscard]] unsigned dev_rate() const { return mirror_.sample_rate(); } + [[nodiscard]] unsigned dev_channels() const { return mirror_.channels(); } + [[nodiscard]] std::string dev_source() const { return mirror_.source_name(); } + [[nodiscard]] std::string dev_reason() const { return mirror_.fallback_reason(); } #endif -private: + private: // Auto-apply a stored override over a guessed stream, and auto-save a format the hook // caught exactly at Initialize (warning if it overwrites a differing stored value). void manage_overrides(const HookStatusView& status, DWORD pid); @@ -114,7 +79,7 @@ private: // flickers. Instead we remember when each stream last advanced and debounce the // live/idle indicator over a short window, plus a ~2 Hz frames/s estimate. std::uint64_t prev_frames_[kMaxAudioStreams] = {}; - double last_active_[kMaxAudioStreams] = {}; // ImGui time a stream last advanced + double last_active_[kMaxAudioStreams] = {}; // ImGui time a stream last advanced std::uint64_t rate_base_frames_[kMaxAudioStreams] = {}; double frames_per_s_[kMaxAudioStreams] = {}; double rate_base_time_ = 0.0; diff --git a/host/src/capture/dxgi_format.hpp b/host/src/capture/dxgi_format.hpp index 8cd2b9a..d8c3656 100644 --- a/host/src/capture/dxgi_format.hpp +++ b/host/src/capture/dxgi_format.hpp @@ -3,8 +3,7 @@ #include -namespace coop -{ +namespace coop { // Map an sRGB DXGI format to its plain UNORM sibling (same byte layout / type // group), leaving non-sRGB formats unchanged. @@ -19,8 +18,7 @@ namespace coop // (the producer's sRGB texture -> the host's UNORM copy) is allowed. inline DXGI_FORMAT srgb_to_unorm(DXGI_FORMAT format) { - switch (format) - { + switch (format) { case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: diff --git a/host/src/capture/frame_renderer.cpp b/host/src/capture/frame_renderer.cpp index 6d1c069..20a8476 100644 --- a/host/src/capture/frame_renderer.cpp +++ b/host/src/capture/frame_renderer.cpp @@ -6,11 +6,9 @@ using Microsoft::WRL::ComPtr; -namespace coop -{ +namespace coop { -namespace -{ +namespace { // Fullscreen triangle generated from SV_VertexID -- no vertex/index buffers // needed. Samples the source texture across the [0,1] UV range. @@ -38,10 +36,10 @@ ComPtr compile(const char* entry, const char* target) { ComPtr blob; ComPtr errors; - const HRESULT hr = D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "frame_renderer", nullptr, nullptr, entry, - target, D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, blob.GetAddressOf(), errors.GetAddressOf()); - if (FAILED(hr)) - { + const HRESULT hr = + D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "frame_renderer", nullptr, nullptr, entry, target, + D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, blob.GetAddressOf(), errors.GetAddressOf()); + if (FAILED(hr)) { return nullptr; } return blob; @@ -53,18 +51,15 @@ bool FrameRenderer::init(ID3D11Device* device) { ComPtr vs_blob = compile("vs_main", "vs_5_0"); ComPtr ps_blob = compile("ps_main", "ps_5_0"); - if (vs_blob == nullptr || ps_blob == nullptr) - { + if (vs_blob == nullptr || ps_blob == nullptr) { return false; } if (FAILED(device->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), nullptr, - vs_.GetAddressOf()))) - { + vs_.GetAddressOf()))) { return false; } if (FAILED(device->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), nullptr, - ps_.GetAddressOf()))) - { + ps_.GetAddressOf()))) { return false; } @@ -74,8 +69,7 @@ bool FrameRenderer::init(ID3D11Device* device) sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; sd.ComparisonFunc = D3D11_COMPARISON_NEVER; - if (FAILED(device->CreateSamplerState(&sd, sampler_.GetAddressOf()))) - { + if (FAILED(device->CreateSamplerState(&sd, sampler_.GetAddressOf()))) { return false; } return true; @@ -84,8 +78,7 @@ bool FrameRenderer::init(ID3D11Device* device) void FrameRenderer::draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w, std::uint32_t src_h, std::uint32_t dst_w, std::uint32_t dst_h) { - if (srv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0) - { + if (srv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0) { return; } diff --git a/host/src/capture/frame_renderer.hpp b/host/src/capture/frame_renderer.hpp index 91da5da..e99efd5 100644 --- a/host/src/capture/frame_renderer.hpp +++ b/host/src/capture/frame_renderer.hpp @@ -7,12 +7,10 @@ #include #include -namespace coop -{ +namespace coop { -class FrameRenderer -{ -public: +class FrameRenderer { + public: bool init(ID3D11Device* device); // Draws `srv` (a srcW x srcH image) centered and scaled to fit within a @@ -21,7 +19,7 @@ public: void draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w, std::uint32_t src_h, std::uint32_t dst_w, std::uint32_t dst_h); -private: + private: Microsoft::WRL::ComPtr vs_; Microsoft::WRL::ComPtr ps_; Microsoft::WRL::ComPtr sampler_; diff --git a/host/src/capture/keyed_mutex.hpp b/host/src/capture/keyed_mutex.hpp index a89942b..9b9c3dc 100644 --- a/host/src/capture/keyed_mutex.hpp +++ b/host/src/capture/keyed_mutex.hpp @@ -4,8 +4,7 @@ #include // HRESULT, S_OK, WAIT_ABANDONED, WAIT_TIMEOUT -namespace coop -{ +namespace coop { // True when an IDXGIKeyedMutex::AcquireSync result means we now HOLD the mutex and must copy + then // release it. S_OK is the normal case. WAIT_ABANDONED is success-with-recovery: a previous owner diff --git a/host/src/capture/shared_texture.cpp b/host/src/capture/shared_texture.cpp index df1da44..7f7264a 100644 --- a/host/src/capture/shared_texture.cpp +++ b/host/src/capture/shared_texture.cpp @@ -5,13 +5,11 @@ #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" -namespace coop -{ +namespace coop { bool SharedTextureSource::init(ID3D11Device* device) { - if (device == nullptr || FAILED(device->QueryInterface(IID_PPV_ARGS(&device_)))) - { + if (device == nullptr || FAILED(device->QueryInterface(IID_PPV_ARGS(&device_)))) { return false; } device_->GetImmediateContext(&ctx_); @@ -40,21 +38,17 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share) width_ = height_ = format_ = 0; pid_ = pid; - if (pid == 0 || share.width == 0 || share.height == 0) - { + if (pid == 0 || share.width == 0 || share.height == 0) { return false; // the hook hasn't shared a backbuffer yet } const std::wstring name = video_share_name(pid); - if (FAILED(device_->OpenSharedResourceByName(name.c_str(), - DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, - IID_PPV_ARGS(&shared_))) || - shared_ == nullptr) - { + if (FAILED(device_->OpenSharedResourceByName(name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, + IID_PPV_ARGS(&shared_))) + || shared_ == nullptr) { return false; } - if (FAILED(shared_.As(&mutex_)) || mutex_ == nullptr) - { + if (FAILED(shared_.As(&mutex_)) || mutex_ == nullptr) { shared_.Reset(); return false; } @@ -72,14 +66,12 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share) desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - if (FAILED(device_->CreateTexture2D(&desc, nullptr, &private_)) || private_ == nullptr) - { + if (FAILED(device_->CreateTexture2D(&desc, nullptr, &private_)) || private_ == nullptr) { mutex_.Reset(); shared_.Reset(); return false; } - if (FAILED(device_->CreateShaderResourceView(private_.Get(), nullptr, &srv_))) - { + if (FAILED(device_->CreateShaderResourceView(private_.Get(), nullptr, &srv_))) { srv_.Reset(); private_.Reset(); mutex_.Reset(); @@ -96,8 +88,7 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share) bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr& staging, D3D11_MAPPED_SUBRESOURCE& map, D3D11_TEXTURE2D_DESC& desc) { - if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr) - { + if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr) { return false; } private_->GetDesc(&desc); @@ -106,8 +97,7 @@ bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtrCreateTexture2D(&staging_desc, nullptr, &staging))) - { + if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging))) { return false; } ctx_->CopyResource(staging.Get(), private_.Get()); @@ -119,15 +109,13 @@ bool SharedTextureSource::read_frame(std::vector& out, std::uint32 Microsoft::WRL::ComPtr staging; D3D11_MAPPED_SUBRESOURCE map{}; D3D11_TEXTURE2D_DESC desc{}; - if (!map_staging_copy(staging, map, desc)) - { + if (!map_staging_copy(staging, map, desc)) { return false; } w = desc.Width; h = desc.Height; out.resize(static_cast(w) * h * 4); - for (std::uint32_t y = 0; y < h; ++y) - { + for (std::uint32_t y = 0; y < h; ++y) { memcpy(out.data() + static_cast(y) * w * 4, static_cast(map.pData) + static_cast(y) * map.RowPitch, static_cast(w) * 4); @@ -141,17 +129,15 @@ bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint Microsoft::WRL::ComPtr staging; D3D11_MAPPED_SUBRESOURCE map{}; D3D11_TEXTURE2D_DESC desc{}; - if (!map_staging_copy(staging, map, desc)) - { + if (!map_staging_copy(staging, map, desc)) { return false; } - if (x >= desc.Width || y >= desc.Height) - { + if (x >= desc.Width || y >= desc.Height) { ctx_->Unmap(staging.Get(), 0); return false; } - const auto* px = static_cast(map.pData) + static_cast(y) * map.RowPitch + - static_cast(x) * 4; // R8G8B8A8_UNORM + const auto* px = static_cast(map.pData) + static_cast(y) * map.RowPitch + + static_cast(x) * 4; // R8G8B8A8_UNORM out[0] = px[0]; out[1] = px[1]; out[2] = px[2]; @@ -162,43 +148,36 @@ bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint bool SharedTextureSource::update(const VideoShareView& share, unsigned long pid) { - if (device_ == nullptr || pid == 0) - { + if (device_ == nullptr || pid == 0) { reset(); return false; } // (Re)open whenever the target or the published backbuffer geometry changes. - if (pid != pid_ || share.width != width_ || share.height != height_ || share.format != format_) - { - if (!reopen(pid, share)) - { + if (pid != pid_ || share.width != width_ || share.height != height_ || share.format != format_) { + if (!reopen(pid, share)) { return srv_ != nullptr; // couldn't open yet; keep any prior frame } last_generation_ = 0; // force a copy of the current frame } - if (shared_ == nullptr || mutex_ == nullptr) - { + if (shared_ == nullptr || mutex_ == nullptr) { return srv_ != nullptr; } - if (share.generation == last_generation_) - { + if (share.generation == last_generation_) { return srv_ != nullptr; // no new frame; keep showing the last copy } // Bounded wait so a stalled producer can't hang the host's render thread. WAIT_ABANDONED (a prior // owner died holding the mutex -- e.g. a host that crashed and reconnected) counts as acquired: // recover by copying + releasing rather than skipping, which would hold it forever and freeze. - if (keyed_mutex_acquired(mutex_->AcquireSync(kVideoMutexKey, 8))) - { + if (keyed_mutex_acquired(mutex_->AcquireSync(kVideoMutexKey, 8))) { ctx_->CopyResource(private_.Get(), shared_.Get()); mutex_->ReleaseSync(kVideoMutexKey); // Generations between the last copy and this one were published but never shown // (we only ever copy the newest). last_generation_ == 0 is the first copy after a // (re)open, where the gap to a large generation is meaningless, so skip it. - if (last_generation_ != 0 && share.generation > last_generation_ + 1) - { + if (last_generation_ != 0 && share.generation > last_generation_ + 1) { frames_missed_ += share.generation - last_generation_ - 1; } last_generation_ = share.generation; diff --git a/host/src/capture/shared_texture.hpp b/host/src/capture/shared_texture.hpp index 6d782e0..05ce22d 100644 --- a/host/src/capture/shared_texture.hpp +++ b/host/src/capture/shared_texture.hpp @@ -14,12 +14,10 @@ #include "ipc/ipc_server.hpp" -namespace coop -{ +namespace coop { -class SharedTextureSource -{ -public: +class SharedTextureSource { + public: // Binds to the host's device (must support ID3D11Device1). Returns false if not. bool init(ID3D11Device* device); @@ -43,30 +41,15 @@ public: // Returns false if no frame has been copied yet or the readback failed. bool read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4]); - [[nodiscard]] ID3D11ShaderResourceView* srv() const - { - return srv_.Get(); - } - [[nodiscard]] std::uint32_t width() const - { - return width_; - } - [[nodiscard]] std::uint32_t height() const - { - return height_; - } - [[nodiscard]] std::uint64_t frames_copied() const - { - return frames_copied_; - } + [[nodiscard]] ID3D11ShaderResourceView* srv() const { return srv_.Get(); } + [[nodiscard]] std::uint32_t width() const { return width_; } + [[nodiscard]] std::uint32_t height() const { return height_; } + [[nodiscard]] std::uint64_t frames_copied() const { return frames_copied_; } // Cumulative published frames the host never displayed because the generation // advanced by more than one between copies (host render rate < hook publish rate). - [[nodiscard]] std::uint64_t frames_missed() const - { - return frames_missed_; - } + [[nodiscard]] std::uint64_t frames_missed() const { return frames_missed_; } -private: + private: bool reopen(unsigned long pid, const VideoShareView& share); // Copy the private texture into a fresh CPU staging texture and map it (shared body of // read_frame/read_pixel). On success the caller reads via `map` and must Unmap `staging`. diff --git a/host/src/capture/window_capture.cpp b/host/src/capture/window_capture.cpp index 0999312..00d170d 100644 --- a/host/src/capture/window_capture.cpp +++ b/host/src/capture/window_capture.cpp @@ -12,19 +12,16 @@ using Microsoft::WRL::ComPtr; -namespace winrt -{ +namespace winrt { using namespace Windows::Graphics; using namespace Windows::Graphics::Capture; using namespace Windows::Graphics::DirectX; using namespace Windows::Graphics::DirectX::Direct3D11; } // namespace winrt -namespace coop -{ +namespace coop { -namespace -{ +namespace { constexpr auto kPixelFormat = winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized; @@ -33,8 +30,7 @@ ComPtr texture_from_surface(winrt::IDirect3DSurface const& surf { auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>(); ComPtr texture; - if (access) - { + if (access) { access->GetInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(texture.GetAddressOf())); } return texture; @@ -50,24 +46,20 @@ WindowCapture::~WindowCapture() bool WindowCapture::start(HWND target, ID3D11Device* device) { stop(); - if (target == nullptr || device == nullptr || !IsWindow(target)) - { + if (target == nullptr || device == nullptr || !IsWindow(target)) { return false; } - try - { + try { device_ = device; // Wrap our D3D11 device as the WinRT device the frame pool renders on. ComPtr dxgi_device; - if (FAILED(device->QueryInterface(IID_PPV_ARGS(dxgi_device.GetAddressOf())))) - { + if (FAILED(device->QueryInterface(IID_PPV_ARGS(dxgi_device.GetAddressOf())))) { return false; } winrt::com_ptr<::IInspectable> inspectable; - if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()))) - { + if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()))) { return false; } winrt_device_ = inspectable.as(); @@ -75,40 +67,30 @@ bool WindowCapture::start(HWND target, ID3D11Device* device) // Create a capture item for the target window via the interop factory. auto interop = winrt::get_activation_factory(); if (FAILED(interop->CreateForWindow(target, winrt::guid_of(), - winrt::put_abi(item_)))) - { + winrt::put_abi(item_)))) { return false; } pool_size_ = item_.Size(); - frame_pool_ = - winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(winrt_device_, kPixelFormat, 2, pool_size_); + frame_pool_ = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(winrt_device_, kPixelFormat, 2, pool_size_); session_ = frame_pool_.CreateCaptureSession(item_); frame_token_ = frame_pool_.FrameArrived({this, &WindowCapture::on_frame_arrived}); // Best-effort: hide the cursor and the yellow capture border (the border // API requires a recent Windows build, hence the guard). - try - { + try { session_.IsCursorCaptureEnabled(false); + } catch (...) { } - catch (...) - { - } - try - { + try { session_.IsBorderRequired(false); - } - catch (...) - { + } catch (...) { } session_.StartCapture(); target_ = target; return true; - } - catch (...) - { + } catch (...) { stop(); return false; } @@ -116,18 +98,15 @@ bool WindowCapture::start(HWND target, ID3D11Device* device) void WindowCapture::stop() { - if (frame_pool_ != nullptr && frame_token_) - { + if (frame_pool_ != nullptr && frame_token_) { frame_pool_.FrameArrived(frame_token_); frame_token_ = {}; } - if (session_ != nullptr) - { + if (session_ != nullptr) { session_.Close(); session_ = nullptr; } - if (frame_pool_ != nullptr) - { + if (frame_pool_ != nullptr) { frame_pool_.Close(); frame_pool_ = nullptr; } @@ -152,8 +131,7 @@ void WindowCapture::on_frame_arrived(winrt::Direct3D11CaptureFramePool const& po auto frame = pool.TryGetNextFrame(); std::lock_guard lock(mutex_); ++frames_arrived_; // capture-rate metric (this is the WGC delivery cadence) - if (pending_ != nullptr) - { + if (pending_ != nullptr) { pending_.Close(); // drop the un-consumed previous frame back to the pool } pending_ = frame; @@ -169,15 +147,12 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct pending_ = nullptr; } - if (frame != nullptr) - { - if (ComPtr src = texture_from_surface(frame.Surface())) - { + if (frame != nullptr) { + if (ComPtr src = texture_from_surface(frame.Surface())) { D3D11_TEXTURE2D_DESC desc = {}; src->GetDesc(&desc); - if (latest_ == nullptr || desc.Width != width_ || desc.Height != height_) - { + if (latest_ == nullptr || desc.Width != width_ || desc.Height != height_) { latest_srv_.Reset(); latest_.Reset(); @@ -186,16 +161,12 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct dst.BindFlags = D3D11_BIND_SHADER_RESOURCE; dst.CPUAccessFlags = 0; dst.MiscFlags = 0; - if (SUCCEEDED(device_->CreateTexture2D(&dst, nullptr, latest_.GetAddressOf()))) - { - if (SUCCEEDED(device_->CreateShaderResourceView(latest_.Get(), nullptr, - latest_srv_.GetAddressOf()))) - { + if (SUCCEEDED(device_->CreateTexture2D(&dst, nullptr, latest_.GetAddressOf()))) { + if (SUCCEEDED( + device_->CreateShaderResourceView(latest_.Get(), nullptr, latest_srv_.GetAddressOf()))) { width_ = desc.Width; height_ = desc.Height; - } - else - { + } else { // Drop the texture so the (latest_ == nullptr) guard retries next frame instead // of leaving a null SRV (a silently black mirror) until the next resize. latest_.Reset(); @@ -203,8 +174,7 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct } } - if (latest_ != nullptr) - { + if (latest_ != nullptr) { ctx->CopyResource(latest_.Get(), src.Get()); } } @@ -212,15 +182,13 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct // If the window resized, the capture item changes size; re-fit the pool. const winrt::SizeInt32 size = item_.Size(); - if (size.Width != pool_size_.Width || size.Height != pool_size_.Height) - { + if (size.Width != pool_size_.Width || size.Height != pool_size_.Height) { pool_size_ = size; frame_pool_.Recreate(winrt_device_, kPixelFormat, 2, size); } } - if (latest_srv_ != nullptr) - { + if (latest_srv_ != nullptr) { renderer.draw(ctx, latest_srv_.Get(), width_, height_, dst_w, dst_h); } } diff --git a/host/src/capture/window_capture.hpp b/host/src/capture/window_capture.hpp index 8ff5dd8..3a2c064 100644 --- a/host/src/capture/window_capture.hpp +++ b/host/src/capture/window_capture.hpp @@ -14,14 +14,12 @@ #include #include -namespace coop -{ +namespace coop { class FrameRenderer; -class WindowCapture -{ -public: +class WindowCapture { + public: ~WindowCapture(); // Begins capturing `target`. Returns false if WGC is unavailable or the @@ -29,34 +27,19 @@ public: bool start(HWND target, ID3D11Device* device); void stop(); - [[nodiscard]] bool running() const - { - return session_ != nullptr; - } - [[nodiscard]] HWND target() const - { - return target_; - } - [[nodiscard]] std::uint32_t frame_width() const - { - return width_; - } - [[nodiscard]] std::uint32_t frame_height() const - { - return height_; - } + [[nodiscard]] bool running() const { return session_ != nullptr; } + [[nodiscard]] HWND target() const { return target_; } + [[nodiscard]] std::uint32_t frame_width() const { return width_; } + [[nodiscard]] std::uint32_t frame_height() const { return height_; } // Cumulative frames WGC has delivered (for the capture-rate metric). - [[nodiscard]] std::uint64_t frames_arrived() const - { - return frames_arrived_; - } + [[nodiscard]] std::uint64_t frames_arrived() const { return frames_arrived_; } // Render thread: consume the newest frame (if any) and draw it letterboxed // into a dst_w x dst_h target via `renderer`. void draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h); -private: + private: void on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& pool, winrt::Windows::Foundation::IInspectable const&); diff --git a/host/src/capture_panel.cpp b/host/src/capture_panel.cpp index d34bc89..fa7ae13 100644 --- a/host/src/capture_panel.cpp +++ b/host/src/capture_panel.cpp @@ -4,17 +4,14 @@ #include "injection_panel.hpp" #include "ui/app_chrome.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f); // One colored line for the multi-series perf graph. -struct GraphSeries -{ +struct GraphSeries { const char* name; const float* values; // oldest -> newest int count; @@ -37,16 +34,13 @@ void plot_multiseries(const char* id, const GraphSeries* series, int n_series, f const float range = (y_max > y_min) ? (y_max - y_min) : 1.0f; ImVec2 pts[256]; - for (int s = 0; s < n_series; ++s) - { + for (int s = 0; s < n_series; ++s) { const GraphSeries& g = series[s]; - if (g.count < 2) - { + if (g.count < 2) { continue; } int cnt = g.count > 256 ? 256 : g.count; - for (int i = 0; i < cnt; ++i) - { + for (int i = 0; i < cnt; ++i) { const float t = static_cast(i) / static_cast(cnt - 1); float norm = (g.values[i] - y_min) / range; norm = norm < 0.0f ? 0.0f : (norm > 1.0f ? 1.0f : norm); @@ -87,12 +81,10 @@ void CapturePanel::draw_ui(const FrameStats& stats) const bool have_source = source_ == Source_Hooked ? have_hook : have_wgc_target; ImGui::BeginDisabled(!have_source); - if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_) - { + if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_) { capture_.stop(); shared_.reset(); - if (injection_ != nullptr) - { + if (injection_ != nullptr) { injection_->request_video(false); // stop the in-game Present hook } } @@ -106,12 +98,10 @@ void CapturePanel::draw_ui(const FrameStats& stats) ImGui::RadioButton("WGC", &source_, Source_Wgc); ImGui::SameLine(); ImGui::RadioButton("Hooked (Present)", &source_, Source_Hooked); - if (source_ != prev_source) - { + if (source_ != prev_source) { capture_.stop(); shared_.reset(); - if (injection_ != nullptr) - { + if (injection_ != nullptr) { injection_->request_video(enabled_ && source_ == Source_Hooked); } } @@ -123,61 +113,44 @@ void CapturePanel::draw_ui(const FrameStats& stats) ImGui::BeginDisabled(source_ != Source_Hooked || !enabled_); ImGui::Checkbox("Sync flip to game frames", &frame_sync_); ImGui::EndDisabled(); - if (source_ != Source_Hooked) - { + if (source_ != Source_Hooked) { ImGui::SameLine(); ImGui::TextDisabled("(Hooked only)"); - } - else if (ImGui::IsItemHovered()) - { + } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Present in lockstep with the game instead of vsync."); } - if (!have_source) - { - ImGui::TextDisabled(source_ == Source_Hooked - ? "Inject into a game first (the Present hook is the source)." - : "Inject into a game first (its window is the source)."); + if (!have_source) { + ImGui::TextDisabled(source_ == Source_Hooked ? "Inject into a game first (the Present hook is the source)." + : "Inject into a game first (its window is the source)."); } - if (source_ == Source_Wgc) - { + if (source_ == Source_Wgc) { // Start/restart WGC capture when enabled and the target window changes. - if (enabled_ && have_wgc_target && capture_.target() != target_) - { - if (!capture_.start(target_, device_)) - { + if (enabled_ && have_wgc_target && capture_.target() != target_) { + if (!capture_.start(target_, device_)) { enabled_ = false; ImGui::TextColored(kRed, "Failed to start capture."); } } - if (capture_.running()) - { + if (capture_.running()) { ImGui::TextColored(kGreen, "Capturing %ux%u (WGC)", capture_.frame_width(), capture_.frame_height()); } - } - else // Source_Hooked + } else // Source_Hooked { - if (enabled_ && injection_ != nullptr) - { + if (enabled_ && injection_ != nullptr) { // Keep the subsystem requested (a fresh inject may have reset control). - if (!injection_->video_requested()) - { + if (!injection_->video_requested()) { injection_->request_video(true); } const VideoShareView share = injection_->video_share(); - if (shared_.frames_copied() > 0 && shared_.width() > 0) - { - ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(), - shared_.height(), static_cast(shared_.frames_copied())); - } - else if (share.present_calls > 0) - { + if (shared_.frames_copied() > 0 && shared_.width() > 0) { + ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(), shared_.height(), + static_cast(shared_.frames_copied())); + } else if (share.present_calls > 0) { ImGui::TextColored(kGreen, "Present hooked (%llu calls); opening shared texture...", static_cast(share.present_calls)); - } - else - { + } else { ImGui::TextDisabled("Waiting for hooked frames (the game may not render via DXGI)."); } } @@ -192,8 +165,7 @@ void CapturePanel::draw_ui(const FrameStats& stats) void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) { - if (!enabled_) - { + if (!enabled_) { return; } const double now = ImGui::GetTime(); @@ -202,8 +174,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) // thresholds (e.g. 99 -> 100) frame to frame. ImGui::Text("Tool render: %4.0f FPS (%6.2f ms)", stats.fps(), stats.avg_ms()); - if (source_ == Source_Hooked) - { + if (source_ == Source_Hooked) { const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{}; ImGui::Text("Game present: %5.0f /s", present_rate_.sample(v.present_calls, now)); ImGui::Text("Hook publish: %5.0f /s", capture_rate_.sample(v.generation, now)); @@ -218,8 +189,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) disp_skip); // On each newly published frame, measure now - present_qpc (system-wide clock). - if (v.generation != last_video_gen_ && v.present_qpc != 0 && qpc_freq_ > 0) - { + if (v.generation != last_video_gen_ && v.present_qpc != 0 && qpc_freq_ > 0) { last_video_gen_ = v.generation; LARGE_INTEGER now_qpc{}; QueryPerformanceCounter(&now_qpc); @@ -228,12 +198,10 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) if (ms >= 0.0 && ms < 1000.0) // ignore clock edge cases { lat_sum_ += ms; - if (lat_n_ == 0 || ms < lat_wmin_) - { + if (lat_n_ == 0 || ms < lat_wmin_) { lat_wmin_ = ms; } - if (ms > lat_wmax_) - { + if (ms > lat_wmax_) { lat_wmax_ = ms; } ++lat_n_; @@ -241,8 +209,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) } if (now - lat_window_start_ >= 1.0) // publish min/avg/max once a second { - if (lat_n_ > 0) - { + if (lat_n_ > 0) { lat_avg_ = static_cast(lat_sum_ / lat_n_); lat_min_ = static_cast(lat_wmin_); lat_max_ = static_cast(lat_wmax_); @@ -253,17 +220,12 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats) lat_wmax_ = 0.0; lat_window_start_ = now; } - if (lat_avg_ > 0.0f) - { + if (lat_avg_ > 0.0f) { ImGui::Text("Capture->display: avg %6.1f min %6.1f max %6.1f ms", lat_avg_, lat_min_, lat_max_); - } - else - { + } else { ImGui::TextDisabled("Capture->display latency: measuring..."); } - } - else - { + } else { ImGui::TextDisabled("Game present: n/a (WGC has no game frame timing)"); ImGui::Text("WGC capture: %5.0f /s", capture_rate_.sample(capture_.frames_arrived(), now)); ImGui::TextDisabled("Latency: n/a (WGC frames aren't game-timestamped)"); @@ -277,14 +239,11 @@ void CapturePanel::sample_graph_series(double now) const float dt = ImGui::GetIO().DeltaTime; tool_fps_.push(dt > 0.0f ? 1.0f / dt : 0.0f); - if (source_ == Source_Hooked) - { + if (source_ == Source_Hooked) { const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{}; game_fps_.push(game_edge_.sample(v.present_calls, now)); hook_fps_.push(hook_edge_.sample(v.generation, now)); - } - else - { + } else { wgc_fps_.push(wgc_edge_.sample(capture_.frames_arrived(), now)); } last_graph_time_ = now; @@ -310,8 +269,7 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/) const auto add = [&](const Series& s, const char* name, const ImVec4& col) { const int c = s.copy(fps[n]); - for (int i = 0; i < c; ++i) - { + for (int i = 0; i < c; ++i) { ms[n][i] = fps[n][i] > 1.0f ? 1000.0f / fps[n][i] : 0.0f; } names[n] = name; @@ -321,42 +279,34 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/) }; add(tool_fps_, "Tool", col_tool); - if (source_ == Source_Hooked) - { + if (source_ == Source_Hooked) { add(game_fps_, "Game", col_game); add(hook_fps_, "Hook", col_hook); - } - else - { + } else { add(wgc_fps_, "WGC", col_hook); } - if (counts[0] < 2) - { + if (counts[0] < 2) { ImGui::TextDisabled("Gathering samples..."); return; } // Legend: a colored label + the latest value of each line, so colors map to series. - for (int i = 0; i < n; ++i) - { + for (int i = 0; i < n; ++i) { ImGui::TextColored(cols[i], "%-4s %4.0f", names[i], counts[i] > 0 ? fps[i][counts[i] - 1] : 0.0f); - if (i + 1 < n) - { + if (i + 1 < n) { ImGui::SameLine(); } } GraphSeries gs[3]; - for (int i = 0; i < n; ++i) - { + for (int i = 0; i < n; ++i) { gs[i] = GraphSeries{names[i], fps[i], counts[i], cols[i]}; } ImGui::TextDisabled("FPS (0-144)"); plot_multiseries("##fps_multi", gs, n, 0.0f, 144.0f, 56.0f); - for (int i = 0; i < n; ++i) - { + for (int i = 0; i < n; ++i) { gs[i].values = ms[i]; } ImGui::TextDisabled("Frametime (0-33 ms)"); @@ -365,23 +315,17 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/) void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h) { - if (!enabled_) - { + if (!enabled_) { return; } - if (source_ == Source_Hooked) - { - if (injection_ == nullptr) - { + if (source_ == Source_Hooked) { + if (injection_ == nullptr) { return; } - if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr) - { + if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr) { renderer_.draw(ctx, shared_.srv(), shared_.width(), shared_.height(), dst_w, dst_h); } - } - else if (capture_.running()) - { + } else if (capture_.running()) { capture_.draw_latest(renderer_, ctx, dst_w, dst_h); } } diff --git a/host/src/capture_panel.hpp b/host/src/capture_panel.hpp index 03c4591..a83eee1 100644 --- a/host/src/capture_panel.hpp +++ b/host/src/capture_panel.hpp @@ -14,28 +14,20 @@ #include "capture/window_capture.hpp" #include "ui/app_chrome.hpp" -namespace coop -{ +namespace coop { class InjectionPanel; -class CapturePanel -{ -public: +class CapturePanel { + public: bool init(ID3D11Device* device); // The window to mirror via WGC (0 if none yet); typically the injected game's HWND. - void set_target(HWND target) - { - target_ = target; - } + void set_target(HWND target) { target_ = target; } // The injection panel supplies the target pid + Present-hook video channel and // lets this panel install/remove the video subsystem when the source is Hooked. - void set_injection(InjectionPanel* injection) - { - injection_ = injection; - } + void set_injection(InjectionPanel* injection) { injection_ = injection; } // `stats` are the host's render frame-timing, drawn as the mirror's // frametime / FPS graphs (this window is what the mirror renders into). @@ -46,17 +38,11 @@ public: // Whether the video mirror is on (mouse forwarding is gated on this, since the // operator can't aim clicks without seeing the game). - [[nodiscard]] bool mirroring() const - { - return enabled_; - } + [[nodiscard]] bool mirroring() const { return enabled_; } // True when the active source is the injected Present-hook (client/backbuffer); // false for WGC (whole-window). Drives the mouse coordinate mapping. - [[nodiscard]] bool source_hooked() const - { - return source_ == Source_Hooked; - } + [[nodiscard]] bool source_hooked() const { return source_ == Source_Hooked; } // True when the operator asked to pace the tool's flip to the game's published frames // (only meaningful with the Hooked source while mirroring) AND the target is alive and @@ -66,9 +52,8 @@ public: // since it needs the InjectionPanel definition for target liveness. [[nodiscard]] bool frame_sync_active() const; -private: - enum Source : int - { + private: + enum Source : int { Source_Wgc = 0, // Windows Graphics Capture Source_Hooked = 1, // injected Present-hook shared texture }; @@ -77,15 +62,13 @@ private: void draw_pipeline_metrics(const FrameStats& stats); // Turns a monotonic counter into a rate (recomputed ~2x/second). - struct RateTracker - { + struct RateTracker { std::uint64_t last_count = 0; double last_time = 0.0; double rate = 0.0; double sample(std::uint64_t count, double now) { - if (now - last_time >= 0.5) - { + if (now - last_time >= 0.5) { const double dt = now - last_time; rate = dt > 0.0 ? static_cast(count - last_count) / dt : 0.0; last_count = count; @@ -98,25 +81,20 @@ private: // Turns a monotonic counter into an instantaneous rate the moment it advances (so a // per-frame graph has real resolution instead of 0.5 s stair-steps); the rate is // held between advances. Used for the game-present / hook-publish graph series. - struct EdgeRate - { + struct EdgeRate { std::uint64_t last_count = 0; double last_time = 0.0; float fps = 0.0f; bool primed = false; float sample(std::uint64_t count, double now) { - if (!primed) - { + if (!primed) { last_count = count; last_time = now; primed = true; - } - else if (count != last_count) - { + } else if (count != last_count) { const double dt = now - last_time; - if (dt > 0.0) - { + if (dt > 0.0) { fps = static_cast(static_cast(count - last_count) / dt); } last_count = count; @@ -127,8 +105,7 @@ private: }; // Fixed-length rolling history of one FPS series, plotted in the perf graph. - struct Series - { + struct Series { static constexpr int kCap = 240; // ~2 s at 120 FPS, matches FrameStats float v[kCap] = {}; int pos = 0; @@ -137,8 +114,7 @@ private: { v[pos] = fps; pos = (pos + 1) % kCap; - if (count < kCap) - { + if (count < kCap) { ++count; } } @@ -146,16 +122,12 @@ private: int copy(float* out) const { const int start = (pos - count + kCap * 2) % kCap; - for (int i = 0; i < count; ++i) - { + for (int i = 0; i < count; ++i) { out[i] = v[(start + i) % kCap]; } return count; } - float latest() const - { - return count > 0 ? v[(pos - 1 + kCap) % kCap] : 0.0f; - } + float latest() const { return count > 0 ? v[(pos - 1 + kCap) % kCap] : 0.0f; } }; // Push one sample into each graph series for the current frame/source. @@ -178,13 +150,13 @@ private: RateTracker display_skip_rate_; // host-side published frames never displayed // Per-frame FPS history for the multi-series perf graph (colored per source). - Series tool_fps_; // host render rate - Series game_fps_; // game Present() rate (Hooked) - Series hook_fps_; // hook publish rate (Hooked) - Series wgc_fps_; // WGC frame-arrival rate (WGC) - EdgeRate game_edge_; // present_calls -> instantaneous fps - EdgeRate hook_edge_; // generation -> instantaneous fps - EdgeRate wgc_edge_; // frames_arrived -> instantaneous fps + Series tool_fps_; // host render rate + Series game_fps_; // game Present() rate (Hooked) + Series hook_fps_; // hook publish rate (Hooked) + Series wgc_fps_; // WGC frame-arrival rate (WGC) + EdgeRate game_edge_; // present_calls -> instantaneous fps + EdgeRate hook_edge_; // generation -> instantaneous fps + EdgeRate wgc_edge_; // frames_arrived -> instantaneous fps double last_graph_time_ = 0.0; std::uint32_t last_video_gen_ = 0; long long qpc_freq_ = 0; diff --git a/host/src/controllers_panel.cpp b/host/src/controllers_panel.cpp index 071e25f..67768e1 100644 --- a/host/src/controllers_panel.cpp +++ b/host/src/controllers_panel.cpp @@ -4,41 +4,36 @@ #include "ui/app_chrome.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 1.0f); -struct ButtonBit -{ +struct ButtonBit { std::uint16_t mask; const char* label; }; // XINPUT_GAMEPAD_* bit values (kept local so this file needn't include Xinput.h). constexpr ButtonBit kButtons[] = { - {0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"}, - {0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"}, - {0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"}, + {0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"}, + {0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"}, + {0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"}, }; void draw_pad(int index, const PadInfo& pad, bool debug_details) { ImGui::PushID(index); - if (!pad.connected) - { + if (!pad.connected) { ImGui::TextDisabled("Slot %d: disconnected", index); ImGui::PopID(); return; } ImGui::TextColored(kGreen, "Slot %d [%s]", index, pad.source.c_str()); - if (debug_details) - { + if (debug_details) { // Triggers on the slot line (saves a row); thumbsticks below. ImGui::SameLine(); ImGui::TextDisabled("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger); @@ -46,25 +41,21 @@ void draw_pad(int index, const PadInfo& pad, bool debug_details) bool first = true; ImGui::TextUnformatted("Buttons: "); - for (const ButtonBit& b : kButtons) - { - if ((pad.state.buttons & b.mask) != 0) - { + for (const ButtonBit& b : kButtons) { + if ((pad.state.buttons & b.mask) != 0) { ImGui::SameLine(); ImGui::TextColored(kGreen, "%s%s", first ? "" : ", ", b.label); first = false; } } - if (first) - { + if (first) { ImGui::SameLine(); ImGui::TextDisabled("(none)"); } - if (debug_details) - { - ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, - pad.state.thumb_rx, pad.state.thumb_ry); + if (debug_details) { + ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, pad.state.thumb_rx, + pad.state.thumb_ry); } ImGui::Separator(); ImGui::PopID(); @@ -81,15 +72,13 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st #ifdef COOP_WITH_STEAM ImGui::Checkbox("Use Steam Input (experimental)", &steam_requested_); - if (steam_requested_) - { + if (steam_requested_) { ImGui::SameLine(); ImGui::TextColored(steam_active_ ? kGreen : kGrey, steam_active_ ? "(active)" : "(starting...)"); ImGui::TextDisabled("Needs a controller bound to Steam Input for this app; otherwise"); ImGui::TextDisabled("XInput is hidden and no input arrives. Leave off for plain XInput."); } - if (steam_note_[0] != '\0') - { + if (steam_note_[0] != '\0') { ImGui::TextColored(kGrey, "%s", steam_note_); } #endif @@ -97,13 +86,11 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st // Synthetic test input (debug aid): drives the game with a non-human pattern so // forwarding can be proven without a real controller. Only meaningful once the // XInput hook is attached. - if (debug_details) - { + if (debug_details) { ImGui::BeginDisabled(!status.attached); ImGui::Checkbox("Forward synthetic test input", &test_input_); ImGui::EndDisabled(); - if (test_input_) - { + if (test_input_) { ImGui::SameLine(); ImGui::TextDisabled("(ignores your controller)"); } @@ -112,15 +99,13 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st // --- Guest pads the host receives from RPT ----------------------------- ImGui::SeparatorText("Incoming (host receives)"); const auto& pads = input.pads; - for (int i = 0; i < static_cast(pads.size()); ++i) - { + for (int i = 0; i < static_cast(pads.size()); ++i) { draw_pad(i, pads[i], debug_details); } // --- What the injected game reads back via the XInput hook -------------- ImGui::SeparatorText("Game polling (hook reports)"); - if (!status.attached) - { + if (!status.attached) { ImGui::TextDisabled("Not injected (no XInput hook)."); record_panel_fit("Controllers"); ImGui::End(); @@ -129,11 +114,9 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st // Convert the cumulative per-slot counters into rates every half second. const double now = ImGui::GetTime(); - if (now - last_sample_time_ >= 0.5) - { + if (now - last_sample_time_ >= 0.5) { const double dt = now - last_sample_time_; - for (int i = 0; i < static_cast(kMaxPads); ++i) - { + for (int i = 0; i < static_cast(kMaxPads); ++i) { const unsigned long long delta = status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0; state_rate_[i] = dt > 0.0 ? static_cast(delta) / dt : 0.0; @@ -143,16 +126,12 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st } double total_rate = 0.0; - for (int i = 0; i < static_cast(kMaxPads); ++i) - { + for (int i = 0; i < static_cast(kMaxPads); ++i) { total_rate += state_rate_[i]; } - if (total_rate > 0.0) - { + if (total_rate > 0.0) { ImGui::TextColored(kGreen, "Game reading controller: %5.0f polls/s", total_rate); - } - else - { + } else { ImGui::TextColored(kGrey, "Game reading controller: idle"); } @@ -160,9 +139,7 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st // (what we forwarded vs what the game read back through the hook). A round-trip mismatch // isolates a tool->game forwarding problem from an input->tool one. Merged into a single // table so the (debug) controller view stays inside its panel even with every slot busy. - if (debug_details && - ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) - { + if (debug_details && ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Slot"); ImGui::TableSetupColumn("Poll/s"); ImGui::TableSetupColumn("Polls"); @@ -170,20 +147,16 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st ImGui::TableSetupColumn("Game read btn/LX,LY"); ImGui::TableHeadersRow(); const auto& fwd = input.pads; - for (int i = 0; i < static_cast(kMaxPads); ++i) - { + for (int i = 0; i < static_cast(kMaxPads); ++i) { const CoopPadState& f = fwd[i].state; const CoopPadState& r = status.read_state[i]; ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%d", i); ImGui::TableNextColumn(); - if (state_rate_[i] > 0.0) - { + if (state_rate_[i] > 0.0) { ImGui::TextColored(kGreen, "%5.0f", state_rate_[i]); - } - else - { + } else { ImGui::TextDisabled("0"); } ImGui::TableNextColumn(); diff --git a/host/src/controllers_panel.hpp b/host/src/controllers_panel.hpp index 63aa801..40fadc2 100644 --- a/host/src/controllers_panel.hpp +++ b/host/src/controllers_panel.hpp @@ -11,12 +11,10 @@ #include "input/input_source.hpp" #include "ipc/ipc_server.hpp" -namespace coop -{ +namespace coop { -class ControllersPanel -{ -public: +class ControllersPanel { + public: // `input` is the input worker's latest snapshot (guest pads + active backend); // `status` is the hook's back-channel (per-slot poll counters); `debug_details` // reveals the raw axis values, the per-slot poll-rate table, and the synthetic @@ -25,10 +23,7 @@ public: // Whether the operator enabled "Forward synthetic test input" (a controller debug // aid). The host feeds this to InjectionPanel, which substitutes a synthetic pad. - [[nodiscard]] bool test_input() const - { - return test_input_; - } + [[nodiscard]] bool test_input() const { return test_input_; } #ifdef COOP_WITH_STEAM // Whether the operator has opted into Steam Input. It's off by default: simply @@ -36,14 +31,8 @@ public: // which hides controllers from XInput unless they're bound to our action set for // this app -- so it can silently break the (working) XInput path. main reconciles // this against the actual backend each frame. - [[nodiscard]] bool steam_input_requested() const - { - return steam_requested_; - } - void set_steam_active(bool active) - { - steam_active_ = active; - } + [[nodiscard]] bool steam_input_requested() const { return steam_requested_; } + void set_steam_active(bool active) { steam_active_ = active; } void on_steam_init_failed() { steam_requested_ = false; @@ -52,7 +41,7 @@ public: } #endif -private: + private: bool test_input_ = false; // "Forward synthetic test input" (debug aid, default off) // Sampled to turn the hook's cumulative per-slot counters into poll rates. diff --git a/host/src/d3d11_window.cpp b/host/src/d3d11_window.cpp index f4587a8..480728d 100644 --- a/host/src/d3d11_window.cpp +++ b/host/src/d3d11_window.cpp @@ -9,11 +9,9 @@ extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg using Microsoft::WRL::ComPtr; -namespace coop -{ +namespace coop { -namespace -{ +namespace { constexpr wchar_t kWindowClass[] = L"CoopAllTheThingsWindow"; // Encode a tightly-packed/row-pitched RGBA8 image to a PNG file via WIC. `src` is the @@ -24,42 +22,34 @@ bool write_rgba8_png(const std::wstring& path, UINT width, UINT height, const BY { ComPtr factory; if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(factory.GetAddressOf())))) - { + IID_PPV_ARGS(factory.GetAddressOf())))) { return false; } ComPtr bitmap; // wrap the back-buffer bytes (RGBA, matches the swap chain) if (FAILED(factory->CreateBitmapFromMemory(width, height, GUID_WICPixelFormat32bppRGBA, row_pitch, - row_pitch * height, const_cast(src), - bitmap.GetAddressOf()))) - { + row_pitch * height, const_cast(src), bitmap.GetAddressOf()))) { return false; } ComPtr stream; - if (FAILED(factory->CreateStream(stream.GetAddressOf())) || - FAILED(stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE))) - { + if (FAILED(factory->CreateStream(stream.GetAddressOf())) + || FAILED(stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE))) { return false; } ComPtr encoder; - if (FAILED(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.GetAddressOf())) || - FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache))) - { + if (FAILED(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.GetAddressOf())) + || FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache))) { return false; } ComPtr frame; ComPtr props; - if (FAILED(encoder->CreateNewFrame(frame.GetAddressOf(), props.GetAddressOf())) || - FAILED(frame->Initialize(props.Get())) || FAILED(frame->SetSize(width, height))) - { + if (FAILED(encoder->CreateNewFrame(frame.GetAddressOf(), props.GetAddressOf())) + || FAILED(frame->Initialize(props.Get())) || FAILED(frame->SetSize(width, height))) { return false; } // Let the encoder pick its native pixel format; WriteSource converts our RGBA to it. WICPixelFormatGUID fmt = GUID_WICPixelFormat32bppBGRA; frame->SetPixelFormat(&fmt); - if (FAILED(frame->WriteSource(bitmap.Get(), nullptr)) || FAILED(frame->Commit()) || - FAILED(encoder->Commit())) - { + if (FAILED(frame->WriteSource(bitmap.Get(), nullptr)) || FAILED(frame->Commit()) || FAILED(encoder->Commit())) { return false; } return true; @@ -69,8 +59,7 @@ bool write_rgba8_png(const std::wstring& path, UINT width, UINT height, const BY D3D11Window::~D3D11Window() { release_render_target(); - if (hwnd_ != nullptr) - { + if (hwnd_ != nullptr) { DestroyWindow(hwnd_); hwnd_ = nullptr; } @@ -88,8 +77,7 @@ bool D3D11Window::create(const wchar_t* title) wc.hInstance = instance; wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.lpszClassName = kWindowClass; - if (RegisterClassExW(&wc) == 0) - { + if (RegisterClassExW(&wc) == 0) { return false; } @@ -100,13 +88,11 @@ bool D3D11Window::create(const wchar_t* title) const int height = GetSystemMetrics(SM_CYSCREEN); hwnd_ = CreateWindowExW(0, kWindowClass, title, WS_POPUP, 0, 0, width, height, nullptr, nullptr, instance, this); - if (hwnd_ == nullptr) - { + if (hwnd_ == nullptr) { return false; } - if (!create_device()) - { + if (!create_device()) { return false; } @@ -136,30 +122,25 @@ bool D3D11Window::create_device() const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, levels, _countof(levels), - D3D11_SDK_VERSION, device_.GetAddressOf(), nullptr, context_.GetAddressOf()))) - { + D3D11_SDK_VERSION, device_.GetAddressOf(), nullptr, context_.GetAddressOf()))) { return false; } ComPtr dxgi_device; - if (FAILED(device_.As(&dxgi_device))) - { + if (FAILED(device_.As(&dxgi_device))) { return false; } ComPtr adapter; - if (FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf()))) - { + if (FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf()))) { return false; } ComPtr factory; - if (FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf())))) - { + if (FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf())))) { return false; } if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd_, &desc, nullptr, nullptr, - swap_chain_.GetAddressOf()))) - { + swap_chain_.GetAddressOf()))) { return false; } // Don't let DXGI swallow Alt+Enter into an exclusive-fullscreen transition. @@ -171,8 +152,7 @@ bool D3D11Window::create_device() bool D3D11Window::note_device_loss(HRESULT hr) { - if (hr != DXGI_ERROR_DEVICE_REMOVED && hr != DXGI_ERROR_DEVICE_RESET) - { + if (hr != DXGI_ERROR_DEVICE_REMOVED && hr != DXGI_ERROR_DEVICE_RESET) { return false; } // GetDeviceRemovedReason gives the specific cause (HUNG / driver internal / removed); a plain @@ -186,10 +166,8 @@ bool D3D11Window::note_device_loss(HRESULT hr) void D3D11Window::create_render_target() { ComPtr back_buffer; - if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf())))) - { - const HRESULT hr = - device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf()); + if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf())))) { + const HRESULT hr = device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf()); note_device_loss(hr); // a removed device surfaces here too; the render loop checks device_lost() } } @@ -201,14 +179,12 @@ void D3D11Window::release_render_target() void D3D11Window::handle_resize(UINT width, UINT height) { - if (swap_chain_ == nullptr || width == 0 || height == 0) - { + if (swap_chain_ == nullptr || width == 0 || height == 0) { return; } release_render_target(); const HRESULT hr = swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0); - if (note_device_loss(hr)) - { + if (note_device_loss(hr)) { return; // device gone; the render loop will see device_lost() and stop } create_render_target(); @@ -217,17 +193,14 @@ void D3D11Window::handle_resize(UINT width, UINT height) bool D3D11Window::pump_messages() { MSG msg; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) - { - if (msg.message == WM_QUIT) - { + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { return false; } TranslateMessage(&msg); DispatchMessageW(&msg); } - if (resize_pending_) - { + if (resize_pending_) { handle_resize(resize_width_, resize_height_); resize_pending_ = false; } @@ -240,17 +213,14 @@ void D3D11Window::render_frame(const RenderCallback& render, UINT sync_interval) context_->OMSetRenderTargets(1, rtv_.GetAddressOf(), nullptr); context_->ClearRenderTargetView(rtv_.Get(), clear); - if (render) - { + if (render) { render(); } // Screenshot (F10): capture after the overlay is drawn but before Present -- the // flip-model back buffer is undefined once presented. - if (!pending_screenshot_.empty()) - { - if (save_backbuffer_png(pending_screenshot_)) - { + if (!pending_screenshot_.empty()) { + if (save_backbuffer_png(pending_screenshot_)) { saved_screenshot_ = pending_screenshot_; } pending_screenshot_.clear(); @@ -277,8 +247,7 @@ std::wstring D3D11Window::take_screenshot_result() bool D3D11Window::save_backbuffer_png(const std::wstring& path) { ComPtr back; - if (FAILED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf())))) - { + if (FAILED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf())))) { return false; } D3D11_TEXTURE2D_DESC desc{}; @@ -291,44 +260,37 @@ bool D3D11Window::save_backbuffer_png(const std::wstring& path) staging.CPUAccessFlags = D3D11_CPU_ACCESS_READ; staging.MiscFlags = 0; ComPtr cpu; - if (FAILED(device_->CreateTexture2D(&staging, nullptr, cpu.GetAddressOf()))) - { + if (FAILED(device_->CreateTexture2D(&staging, nullptr, cpu.GetAddressOf()))) { return false; } context_->CopyResource(cpu.Get(), back.Get()); D3D11_MAPPED_SUBRESOURCE map{}; - if (FAILED(context_->Map(cpu.Get(), 0, D3D11_MAP_READ, 0, &map))) - { + if (FAILED(context_->Map(cpu.Get(), 0, D3D11_MAP_READ, 0, &map))) { return false; } // The swap chain is DXGI_FORMAT_R8G8B8A8_UNORM (see create_device), i.e. RGBA bytes. - const bool ok = - write_rgba8_png(path, desc.Width, desc.Height, static_cast(map.pData), map.RowPitch); + const bool ok = write_rgba8_png(path, desc.Width, desc.Height, static_cast(map.pData), map.RowPitch); context_->Unmap(cpu.Get(), 0); return ok; } LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { - if (msg == WM_NCCREATE) - { + if (msg == WM_NCCREATE) { auto* create = reinterpret_cast(lparam); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(create->lpCreateParams)); } - if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam)) - { + if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam)) { return true; } auto* self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); - switch (msg) - { + switch (msg) { case WM_SIZE: - if (self != nullptr && wparam != SIZE_MINIMIZED) - { + if (self != nullptr && wparam != SIZE_MINIMIZED) { self->resize_pending_ = true; self->resize_width_ = LOWORD(lparam); self->resize_height_ = HIWORD(lparam); @@ -338,13 +300,10 @@ LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA // Per-monitor-v2: the DPI of the display we're on changed. Resize to the rect Windows suggests // in lparam (its recommended handling; the resulting WM_SIZE repaints the swap chain via the // deferred-resize path above), then latch the new DPI for the overlay to rescale its font/style. - if (self != nullptr) - { - if (const auto* suggested = reinterpret_cast(lparam); suggested != nullptr) - { - SetWindowPos(hwnd, nullptr, suggested->left, suggested->top, - suggested->right - suggested->left, suggested->bottom - suggested->top, - SWP_NOZORDER | SWP_NOACTIVATE); + if (self != nullptr) { + if (const auto* suggested = reinterpret_cast(lparam); suggested != nullptr) { + SetWindowPos(hwnd, nullptr, suggested->left, suggested->top, suggested->right - suggested->left, + suggested->bottom - suggested->top, SWP_NOZORDER | SWP_NOACTIVATE); } self->dpi_pending_ = true; self->pending_dpi_ = HIWORD(wparam); // X and Y DPI are equal; HIWORD is the Y value @@ -354,8 +313,7 @@ LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA // F10 is our screenshot key; ImGui already saw this message (handler runs above), // so swallow it here to stop DefWindowProc from flicking into Win32 menu mode. // Alt+F4 (VK_F4) falls through to DefWindowProc so it still closes the window. - if (wparam == VK_F10) - { + if (wparam == VK_F10) { return 0; } break; diff --git a/host/src/d3d11_window.hpp b/host/src/d3d11_window.hpp index c1a8759..22ef200 100644 --- a/host/src/d3d11_window.hpp +++ b/host/src/d3d11_window.hpp @@ -10,12 +10,10 @@ #include #include -namespace coop -{ +namespace coop { -class D3D11Window -{ -public: +class D3D11Window { + public: using RenderCallback = std::function; D3D11Window() = default; @@ -50,15 +48,9 @@ public: // True once Present/ResizeBuffers reported DXGI_ERROR_DEVICE_REMOVED/RESET (a host-side TDR, // driver reset, or GPU hang). The render loop is expected to stop and surface the error rather // than spin forever on a dead device; full device re-creation is intentionally not attempted. - [[nodiscard]] bool device_lost() const - { - return device_lost_; - } + [[nodiscard]] bool device_lost() const { return device_lost_; } // The GetDeviceRemovedReason() HRESULT (or the originating error) when device_lost() is true. - [[nodiscard]] HRESULT device_lost_reason() const - { - return device_lost_reason_; - } + [[nodiscard]] HRESULT device_lost_reason() const { return device_lost_reason_; } // If a WM_DPICHANGED arrived since the last call (the window moved to a different-DPI monitor, or // the display scale changed at runtime), returns true and writes that monitor's DPI to `dpi`, @@ -66,8 +58,7 @@ public: // fonts/style. One-shot, mirroring the deferred-resize handling in pump_messages(). [[nodiscard]] bool take_dpi_change(unsigned& dpi) { - if (!dpi_pending_) - { + if (!dpi_pending_) { return false; } dpi = pending_dpi_; @@ -75,20 +66,11 @@ public: return true; } - [[nodiscard]] HWND hwnd() const - { - return hwnd_; - } - [[nodiscard]] ID3D11Device* device() const - { - return device_.Get(); - } - [[nodiscard]] ID3D11DeviceContext* context() const - { - return context_.Get(); - } + [[nodiscard]] HWND hwnd() const { return hwnd_; } + [[nodiscard]] ID3D11Device* device() const { return device_.Get(); } + [[nodiscard]] ID3D11DeviceContext* context() const { return context_.Get(); } -private: + private: static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); bool create_device(); @@ -106,8 +88,8 @@ private: bool resize_pending_ = false; UINT resize_width_ = 0; UINT resize_height_ = 0; - bool dpi_pending_ = false; // set by WM_DPICHANGED, consumed by take_dpi_change() - unsigned pending_dpi_ = 0; // the monitor DPI reported alongside that WM_DPICHANGED + bool dpi_pending_ = false; // set by WM_DPICHANGED, consumed by take_dpi_change() + unsigned pending_dpi_ = 0; // the monitor DPI reported alongside that WM_DPICHANGED bool device_lost_ = false; HRESULT device_lost_reason_ = S_OK; diff --git a/host/src/imgui_layer.cpp b/host/src/imgui_layer.cpp index 6eaf4f5..0748c0c 100644 --- a/host/src/imgui_layer.cpp +++ b/host/src/imgui_layer.cpp @@ -11,13 +11,11 @@ #include "ui/app_chrome.hpp" #include "util/utf8.hpp" -namespace coop -{ +namespace coop { ImGuiLayer::~ImGuiLayer() { - if (initialized_) - { + if (initialized_) { ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -42,12 +40,10 @@ bool ImGuiLayer::init(HWND hwnd, ID3D11Device* device, ID3D11DeviceContext* cont io.IniFilename = ini_path_.c_str(); set_layout_persisted(had_layout); - if (!ImGui_ImplWin32_Init(hwnd)) - { + if (!ImGui_ImplWin32_Init(hwnd)) { return false; } - if (!ImGui_ImplDX11_Init(device, context)) - { + if (!ImGui_ImplDX11_Init(device, context)) { return false; } initialized_ = true; @@ -78,8 +74,7 @@ void ImGuiLayer::apply_dpi(unsigned dpi) // Drop the DX11 backend's cached font texture so it rebuilds from the new atlas next frame. Before // the first frame nothing is built yet, so this is a harmless no-op during init(). - if (initialized_) - { + if (initialized_) { ImGui_ImplDX11_InvalidateDeviceObjects(); } dpi_scale_ = scale; @@ -87,8 +82,7 @@ void ImGuiLayer::apply_dpi(unsigned dpi) void ImGuiLayer::set_dpi(unsigned dpi) { - if (!initialized_ || dpi_scale_from(dpi) == dpi_scale_) - { + if (!initialized_ || dpi_scale_from(dpi) == dpi_scale_) { return; // not up yet, or the scale didn't actually change -- skip a needless atlas rebuild } apply_dpi(dpi); diff --git a/host/src/imgui_layer.hpp b/host/src/imgui_layer.hpp index eba8533..26f6d5e 100644 --- a/host/src/imgui_layer.hpp +++ b/host/src/imgui_layer.hpp @@ -6,12 +6,10 @@ #include #include -namespace coop -{ +namespace coop { -class ImGuiLayer -{ -public: +class ImGuiLayer { + public: ImGuiLayer() = default; ~ImGuiLayer(); @@ -27,7 +25,7 @@ public: // scale changes at runtime. No-op before init() or when the resulting scale is unchanged. void set_dpi(unsigned dpi); -private: + private: // Rebuild the font atlas at the DPI-scaled size and re-apply the scaled dark style. Used by both // init() (first apply) and set_dpi() (runtime change). void apply_dpi(unsigned dpi); diff --git a/host/src/inject/dll_probe.cpp b/host/src/inject/dll_probe.cpp index 9d02751..5c41dc1 100644 --- a/host/src/inject/dll_probe.cpp +++ b/host/src/inject/dll_probe.cpp @@ -7,8 +7,7 @@ #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" -namespace coop -{ +namespace coop { bool hook_dll_alive(unsigned long pid, int timeout_ms) { @@ -18,17 +17,14 @@ bool hook_dll_alive(unsigned long pid, int timeout_ms) // Poll rather than sample once: the worker only beats ~4x/s, so a single short read can straddle // a gap and miss it; return the instant a beat lands, and give up after the timeout. SharedMemory shm; - if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock))) - { + if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock))) { return false; } auto* block = shm.as(); const std::uint32_t h0 = block->status.heartbeat.load(std::memory_order_acquire); - for (int waited = 0; waited < timeout_ms; waited += 25) - { + for (int waited = 0; waited < timeout_ms; waited += 25) { Sleep(25); - if (block->status.heartbeat.load(std::memory_order_acquire) != h0) - { + if (block->status.heartbeat.load(std::memory_order_acquire) != h0) { return true; } } diff --git a/host/src/inject/dll_probe.hpp b/host/src/inject/dll_probe.hpp index aff5446..9ac40d4 100644 --- a/host/src/inject/dll_probe.hpp +++ b/host/src/inject/dll_probe.hpp @@ -3,8 +3,7 @@ // crash, since a connected DLL keeps the per-pid IPC section alive. #pragma once -namespace coop -{ +namespace coop { // True if `pid` already hosts a live coop_hook DLL: the per-pid IPC section exists and its heartbeat // advances within `timeout_ms` (the DLL's worker is still beating). Returns as soon as a beat lands, diff --git a/host/src/inject/injector.cpp b/host/src/inject/injector.cpp index 95b6d2b..efe9a7e 100644 --- a/host/src/inject/injector.cpp +++ b/host/src/inject/injector.cpp @@ -2,13 +2,11 @@ #include -namespace coop -{ +namespace coop { const char* to_string(InjectStatus status) { - switch (status) - { + switch (status) { case InjectStatus::Ok: return "OK"; case InjectStatus::OpenProcessFailed: @@ -33,8 +31,7 @@ const char* to_string(InjectStatus status) return "unknown"; } -namespace -{ +namespace { InjectResult fail(InjectStatus status) { @@ -46,16 +43,14 @@ bool is_wow64_process(HANDLE process) { USHORT process_machine = IMAGE_FILE_MACHINE_UNKNOWN; USHORT native_machine = IMAGE_FILE_MACHINE_UNKNOWN; - if (IsWow64Process2(process, &process_machine, &native_machine)) - { + if (IsWow64Process2(process, &process_machine, &native_machine)) { return process_machine != IMAGE_FILE_MACHINE_UNKNOWN; } // IsWow64Process2 failed -- fall back to the legacy query rather than guessing "native", since // guessing wrong sends the x64 DLL into a 32-bit target (which can't load it). Only if BOTH // queries fail do we fall back to permissive. BOOL wow64 = FALSE; - if (IsWow64Process(process, &wow64)) - { + if (IsWow64Process(process, &wow64)) { return wow64 != FALSE; } return false; // both queries failed; best-effort assume native @@ -75,9 +70,8 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path) { const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe"); const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll"); - if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES || - GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES + || GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) { return InjectResult{InjectStatus::HelperNotFound, 0}; } @@ -87,9 +81,8 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path) STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; - if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, - &si, &pi)) - { + if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, + &pi)) { return fail(InjectStatus::HelperFailed); } WaitForSingleObject(pi.hProcess, INFINITE); @@ -98,8 +91,7 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path) const DWORD err = got ? exit_code : GetLastError(); // on a failed query, surface the OS error CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - if (!got || exit_code != 0) - { + if (!got || exit_code != 0) { return InjectResult{InjectStatus::HelperFailed, err}; } return InjectResult{InjectStatus::Ok, 0}; @@ -109,63 +101,51 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path) InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path) { - if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { return fail(InjectStatus::DllNotFound); } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { return fail(InjectStatus::OpenProcessFailed); } - struct HandleGuard - { + struct HandleGuard { HANDLE h; ~HandleGuard() { - if (h != nullptr) - { + if (h != nullptr) { CloseHandle(h); } } } process_guard{process}; - if (is_wow64_process(process)) - { + if (is_wow64_process(process)) { // The x64 host can't inject a 32-bit target directly; delegate to the helper. return inject_via_helper(pid, dll_path); } const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - if (remote == nullptr) - { + if (remote == nullptr) { return fail(InjectStatus::AllocFailed); } InjectResult result{InjectStatus::Ok, 0}; - if (!WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) - { + if (!WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) { result = fail(InjectStatus::WriteFailed); - } - else - { + } else { // kernel32 is mapped at the same address in every process, so LoadLibraryW's // address in this process is valid as the remote thread's start routine. auto load_library = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr); - if (thread == nullptr) - { + if (thread == nullptr) { result = fail(InjectStatus::RemoteThreadFailed); - } - else - { + } else { WaitForSingleObject(thread, INFINITE); DWORD exit_code = 0; GetExitCodeThread(thread, &exit_code); @@ -173,8 +153,7 @@ InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path) // LoadLibraryW returns the module handle; 0 means it failed to load. // (The handle is truncated to 32 bits here, but zero vs non-zero is // all we need to distinguish success from failure.) - if (exit_code == 0) - { + if (exit_code == 0) { result = InjectResult{InjectStatus::RemoteLoadFailed, 0}; } } diff --git a/host/src/inject/injector.hpp b/host/src/inject/injector.hpp index 3e8b3f6..bda3011 100644 --- a/host/src/inject/injector.hpp +++ b/host/src/inject/injector.hpp @@ -4,11 +4,9 @@ #include -namespace coop -{ +namespace coop { -enum class InjectStatus -{ +enum class InjectStatus { Ok, OpenProcessFailed, // insufficient rights (try running the host as admin) BitnessMismatch, // 32-bit target; the x86 hook/helper aren't available @@ -21,8 +19,7 @@ enum class InjectStatus HelperFailed, // the x86 injector helper ran but reported failure }; -struct InjectResult -{ +struct InjectResult { InjectStatus status = InjectStatus::OpenProcessFailed; unsigned long os_error = 0; // GetLastError at the point of failure, if any }; diff --git a/host/src/inject/mkb_forward.cpp b/host/src/inject/mkb_forward.cpp index 03516ae..f963abf 100644 --- a/host/src/inject/mkb_forward.cpp +++ b/host/src/inject/mkb_forward.cpp @@ -6,80 +6,120 @@ #include "injection_panel.hpp" #include "inject/mkb_map.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { // Map an ImGui key to a Win32 virtual-key. Returns 0 for keys we don't forward. int imgui_key_to_vk(ImGuiKey k) { - if (k >= ImGuiKey_A && k <= ImGuiKey_Z) - { + if (k >= ImGuiKey_A && k <= ImGuiKey_Z) { return 'A' + (k - ImGuiKey_A); } - if (k >= ImGuiKey_0 && k <= ImGuiKey_9) - { + if (k >= ImGuiKey_0 && k <= ImGuiKey_9) { return '0' + (k - ImGuiKey_0); } - if (k >= ImGuiKey_Keypad0 && k <= ImGuiKey_Keypad9) - { + if (k >= ImGuiKey_Keypad0 && k <= ImGuiKey_Keypad9) { return VK_NUMPAD0 + (k - ImGuiKey_Keypad0); } - if (k >= ImGuiKey_F1 && k <= ImGuiKey_F12) - { + if (k >= ImGuiKey_F1 && k <= ImGuiKey_F12) { return VK_F1 + (k - ImGuiKey_F1); } - switch (k) - { - case ImGuiKey_Tab: return VK_TAB; - case ImGuiKey_LeftArrow: return VK_LEFT; - case ImGuiKey_RightArrow: return VK_RIGHT; - case ImGuiKey_UpArrow: return VK_UP; - case ImGuiKey_DownArrow: return VK_DOWN; - case ImGuiKey_PageUp: return VK_PRIOR; - case ImGuiKey_PageDown: return VK_NEXT; - case ImGuiKey_Home: return VK_HOME; - case ImGuiKey_End: return VK_END; - case ImGuiKey_Insert: return VK_INSERT; - case ImGuiKey_Delete: return VK_DELETE; - case ImGuiKey_Backspace: return VK_BACK; - case ImGuiKey_Space: return VK_SPACE; - case ImGuiKey_Enter: return VK_RETURN; - case ImGuiKey_Escape: return VK_ESCAPE; - case ImGuiKey_LeftCtrl: return VK_LCONTROL; - case ImGuiKey_LeftShift: return VK_LSHIFT; - case ImGuiKey_LeftAlt: return VK_LMENU; - case ImGuiKey_LeftSuper: return VK_LWIN; - case ImGuiKey_RightCtrl: return VK_RCONTROL; - case ImGuiKey_RightShift: return VK_RSHIFT; - case ImGuiKey_RightAlt: return VK_RMENU; - case ImGuiKey_RightSuper: return VK_RWIN; - case ImGuiKey_Menu: return VK_APPS; - case ImGuiKey_Apostrophe: return VK_OEM_7; - case ImGuiKey_Comma: return VK_OEM_COMMA; - case ImGuiKey_Minus: return VK_OEM_MINUS; - case ImGuiKey_Period: return VK_OEM_PERIOD; - case ImGuiKey_Slash: return VK_OEM_2; - case ImGuiKey_Semicolon: return VK_OEM_1; - case ImGuiKey_Equal: return VK_OEM_PLUS; - case ImGuiKey_LeftBracket: return VK_OEM_4; - case ImGuiKey_Backslash: return VK_OEM_5; - case ImGuiKey_RightBracket: return VK_OEM_6; - case ImGuiKey_GraveAccent: return VK_OEM_3; - case ImGuiKey_CapsLock: return VK_CAPITAL; - case ImGuiKey_ScrollLock: return VK_SCROLL; - case ImGuiKey_NumLock: return VK_NUMLOCK; - case ImGuiKey_PrintScreen: return VK_SNAPSHOT; - case ImGuiKey_Pause: return VK_PAUSE; - case ImGuiKey_KeypadDecimal: return VK_DECIMAL; - case ImGuiKey_KeypadDivide: return VK_DIVIDE; - case ImGuiKey_KeypadMultiply: return VK_MULTIPLY; - case ImGuiKey_KeypadSubtract: return VK_SUBTRACT; - case ImGuiKey_KeypadAdd: return VK_ADD; - case ImGuiKey_KeypadEnter: return VK_RETURN; - default: return 0; + switch (k) { + case ImGuiKey_Tab: + return VK_TAB; + case ImGuiKey_LeftArrow: + return VK_LEFT; + case ImGuiKey_RightArrow: + return VK_RIGHT; + case ImGuiKey_UpArrow: + return VK_UP; + case ImGuiKey_DownArrow: + return VK_DOWN; + case ImGuiKey_PageUp: + return VK_PRIOR; + case ImGuiKey_PageDown: + return VK_NEXT; + case ImGuiKey_Home: + return VK_HOME; + case ImGuiKey_End: + return VK_END; + case ImGuiKey_Insert: + return VK_INSERT; + case ImGuiKey_Delete: + return VK_DELETE; + case ImGuiKey_Backspace: + return VK_BACK; + case ImGuiKey_Space: + return VK_SPACE; + case ImGuiKey_Enter: + return VK_RETURN; + case ImGuiKey_Escape: + return VK_ESCAPE; + case ImGuiKey_LeftCtrl: + return VK_LCONTROL; + case ImGuiKey_LeftShift: + return VK_LSHIFT; + case ImGuiKey_LeftAlt: + return VK_LMENU; + case ImGuiKey_LeftSuper: + return VK_LWIN; + case ImGuiKey_RightCtrl: + return VK_RCONTROL; + case ImGuiKey_RightShift: + return VK_RSHIFT; + case ImGuiKey_RightAlt: + return VK_RMENU; + case ImGuiKey_RightSuper: + return VK_RWIN; + case ImGuiKey_Menu: + return VK_APPS; + case ImGuiKey_Apostrophe: + return VK_OEM_7; + case ImGuiKey_Comma: + return VK_OEM_COMMA; + case ImGuiKey_Minus: + return VK_OEM_MINUS; + case ImGuiKey_Period: + return VK_OEM_PERIOD; + case ImGuiKey_Slash: + return VK_OEM_2; + case ImGuiKey_Semicolon: + return VK_OEM_1; + case ImGuiKey_Equal: + return VK_OEM_PLUS; + case ImGuiKey_LeftBracket: + return VK_OEM_4; + case ImGuiKey_Backslash: + return VK_OEM_5; + case ImGuiKey_RightBracket: + return VK_OEM_6; + case ImGuiKey_GraveAccent: + return VK_OEM_3; + case ImGuiKey_CapsLock: + return VK_CAPITAL; + case ImGuiKey_ScrollLock: + return VK_SCROLL; + case ImGuiKey_NumLock: + return VK_NUMLOCK; + case ImGuiKey_PrintScreen: + return VK_SNAPSHOT; + case ImGuiKey_Pause: + return VK_PAUSE; + case ImGuiKey_KeypadDecimal: + return VK_DECIMAL; + case ImGuiKey_KeypadDivide: + return VK_DIVIDE; + case ImGuiKey_KeypadMultiply: + return VK_MULTIPLY; + case ImGuiKey_KeypadSubtract: + return VK_SUBTRACT; + case ImGuiKey_KeypadAdd: + return VK_ADD; + case ImGuiKey_KeypadEnter: + return VK_RETURN; + default: + return 0; } } @@ -95,10 +135,8 @@ bool g_key_down[256] = {}; // indexed by VK void release_held_keys(InjectionPanel& injection) { - for (int vk = 0; vk < 256; ++vk) - { - if (g_key_down[vk]) - { + for (int vk = 0; vk < 256; ++vk) { + if (g_key_down[vk]) { injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast(vk), 0, 0}); g_key_down[vk] = false; } @@ -107,10 +145,8 @@ void release_held_keys(InjectionPanel& injection) void release_held_mouse(InjectionPanel& injection) { - for (int b = 0; b < 3; ++b) - { - if (g_mouse_down[b]) - { + for (int b = 0; b < 3; ++b) { + if (g_mouse_down[b]) { injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast(b), g_last_gx, g_last_gy}); g_mouse_down[b] = false; } @@ -126,8 +162,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring // own desktop use isn't injected. If we can't forward for ANY reason -- subsystem off, we lost // focus, or the game is gone -- release everything we're still holding first, so a key/button // held at that moment doesn't stick down in the guest. - if (!injection.mkb_enabled() || GetForegroundWindow() != host_hwnd || game == nullptr || !IsWindow(game)) - { + if (!injection.mkb_enabled() || GetForegroundWindow() != host_hwnd || game == nullptr || !IsWindow(game)) { release_held_keys(injection); release_held_mouse(injection); return; @@ -136,35 +171,26 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring ImGuiIO& io = ImGui::GetIO(); // --- Keyboard (unless ImGui is using it for e.g. a text field -- then release what we hold) --- - if (io.WantCaptureKeyboard) - { + if (io.WantCaptureKeyboard) { release_held_keys(injection); - } - else - { - for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast(k + 1)) - { + } else { + for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast(k + 1)) { const int vk = imgui_key_to_vk(k); - if (vk == 0) - { + if (vk == 0) { continue; } - if (ImGui::IsKeyPressed(k, false)) - { + if (ImGui::IsKeyPressed(k, false)) { injection.push_mkb(MkbEvent{Mkb_KeyDown, static_cast(vk), 0, 0}); g_key_down[vk & 0xFF] = true; } - if (ImGui::IsKeyReleased(k)) - { + if (ImGui::IsKeyReleased(k)) { injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast(vk), 0, 0}); g_key_down[vk & 0xFF] = false; } } - for (int i = 0; i < io.InputQueueCharacters.Size; ++i) - { + for (int i = 0; i < io.InputQueueCharacters.Size; ++i) { const ImWchar c = io.InputQueueCharacters[i]; - if (c != 0) - { + if (c != 0) { injection.push_mkb(MkbEvent{Mkb_Char, static_cast(c), 0, 0}); } } @@ -173,8 +199,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring // --- Mouse (clicks + wheel only, and only while mirroring and ImGui isn't using the mouse) --- // When we're not forwarding the mouse, still release any button we hold (below), so it can't stick. const bool forwarding_mouse = mirroring && !io.WantCaptureMouse; - if (!forwarding_mouse) - { + if (!forwarding_mouse) { release_held_mouse(injection); return; } @@ -187,15 +212,12 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring m.host_y = static_cast(io.MousePos.y); m.dst_w = host_client.right; m.dst_h = host_client.bottom; - if (source_hooked) - { + if (source_hooked) { // Hooked capture mirrors the backbuffer (client area), no decorations. const VideoShareView v = injection.video_share(); m.src_w = m.client_w = static_cast(v.width); m.src_h = m.client_h = static_cast(v.height); - } - else - { + } else { // WGC captures the whole window; the client area sits at a decoration offset. RECT wr{}, cr{}; POINT client_origin{0, 0}; @@ -212,8 +234,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring int gx = 0, gy = 0; const bool on_game = map_host_to_game_client(m, gx, gy); - if (on_game) - { + if (on_game) { g_last_gx = gx; g_last_gy = gy; } @@ -222,8 +243,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring for (int button = 0; button < 3; ++button) // 0=left, 1=right, 2=middle { - if (on_game && ImGui::IsMouseClicked(button)) - { + if (on_game && ImGui::IsMouseClicked(button)) { injection.push_mkb(MkbEvent{Mkb_MouseDown, static_cast(button), mx, my}); g_mouse_down[button] = true; } @@ -233,8 +253,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring g_mouse_down[button] = false; } } - if (on_game && io.MouseWheel != 0.0f) - { + if (on_game && io.MouseWheel != 0.0f) { const int delta = static_cast(io.MouseWheel * WHEEL_DELTA); injection.push_mkb(MkbEvent{Mkb_Wheel, static_cast(delta), mx, my}); } diff --git a/host/src/inject/mkb_forward.hpp b/host/src/inject/mkb_forward.hpp index 8be24dc..40d6080 100644 --- a/host/src/inject/mkb_forward.hpp +++ b/host/src/inject/mkb_forward.hpp @@ -9,8 +9,7 @@ #include -namespace coop -{ +namespace coop { class InjectionPanel; diff --git a/host/src/inject/mkb_map.hpp b/host/src/inject/mkb_map.hpp index 91959de..dd82bc9 100644 --- a/host/src/inject/mkb_map.hpp +++ b/host/src/inject/mkb_map.hpp @@ -12,44 +12,38 @@ #include -namespace coop -{ +namespace coop { -struct MkbMapInput -{ - int host_x = 0, host_y = 0; // mouse in host-window client pixels - int dst_w = 0, dst_h = 0; // host window client size - int src_w = 0, src_h = 0; // captured frame size (WGC=window, Hooked=backbuffer) +struct MkbMapInput { + int host_x = 0, host_y = 0; // mouse in host-window client pixels + int dst_w = 0, dst_h = 0; // host window client size + int src_w = 0, src_h = 0; // captured frame size (WGC=window, Hooked=backbuffer) int client_off_x = 0, client_off_y = 0; // client-area top-left within the frame - int client_w = 0, client_h = 0; // game client size within the frame + int client_w = 0, client_h = 0; // game client size within the frame }; // Returns true and writes gx,gy (game client px) if the point lands on the game's // client area; false if it falls on a letterbox bar or the window decorations. inline bool map_host_to_game_client(const MkbMapInput& in, int& gx, int& gy) { - if (in.src_w <= 0 || in.src_h <= 0 || in.dst_w <= 0 || in.dst_h <= 0 || in.client_w <= 0 || in.client_h <= 0) - { + if (in.src_w <= 0 || in.src_h <= 0 || in.dst_w <= 0 || in.dst_h <= 0 || in.client_w <= 0 || in.client_h <= 0) { return false; } // Invert the letterbox: the frame is fit (aspect-preserved) and centered in dst. - const double scale = - std::min(static_cast(in.dst_w) / in.src_w, static_cast(in.dst_h) / in.src_h); + const double scale = std::min(static_cast(in.dst_w) / in.src_w, static_cast(in.dst_h) / in.src_h); const double ox = (in.dst_w - in.src_w * scale) * 0.5; const double oy = (in.dst_h - in.src_h * scale) * 0.5; const double fx = (in.host_x - ox) / scale; // position in captured-frame pixels const double fy = (in.host_y - oy) / scale; - if (fx < 0.0 || fy < 0.0 || fx >= in.src_w || fy >= in.src_h) - { + if (fx < 0.0 || fy < 0.0 || fx >= in.src_w || fy >= in.src_h) { return false; // on a letterbox bar } const double cx = fx - in.client_off_x; // into client space const double cy = fy - in.client_off_y; - if (cx < 0.0 || cy < 0.0 || cx >= in.client_w || cy >= in.client_h) - { + if (cx < 0.0 || cy < 0.0 || cx >= in.client_w || cy >= in.client_h) { return false; // on the window decorations } diff --git a/host/src/inject/process_list.cpp b/host/src/inject/process_list.cpp index 6436e2f..3ec8475 100644 --- a/host/src/inject/process_list.cpp +++ b/host/src/inject/process_list.cpp @@ -5,27 +5,22 @@ #include #include -namespace coop -{ +namespace coop { std::vector list_processes() { std::vector result; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot == INVALID_HANDLE_VALUE) - { + if (snapshot == INVALID_HANDLE_VALUE) { return result; } PROCESSENTRY32W entry = {}; entry.dwSize = sizeof(entry); - if (Process32FirstW(snapshot, &entry)) - { - do - { - if (entry.th32ProcessID == 0) - { + if (Process32FirstW(snapshot, &entry)) { + do { + if (entry.th32ProcessID == 0) { continue; } result.push_back(ProcessEntry{entry.th32ProcessID, entry.szExeFile}); diff --git a/host/src/inject/process_list.hpp b/host/src/inject/process_list.hpp index 34c034e..3d33641 100644 --- a/host/src/inject/process_list.hpp +++ b/host/src/inject/process_list.hpp @@ -4,11 +4,9 @@ #include #include -namespace coop -{ +namespace coop { -struct ProcessEntry -{ +struct ProcessEntry { unsigned long pid = 0; std::wstring exe_name; // image base name, e.g. "game.exe" }; diff --git a/host/src/inject/window_list.cpp b/host/src/inject/window_list.cpp index 304207f..febe66b 100644 --- a/host/src/inject/window_list.cpp +++ b/host/src/inject/window_list.cpp @@ -7,14 +7,11 @@ #include "inject/process_list.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { -struct EnumCtx -{ +struct EnumCtx { std::vector* out; const std::unordered_map* names; DWORD self_pid; @@ -25,23 +22,19 @@ BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lparam) auto* ctx = reinterpret_cast(lparam); // Keep only "alt-tab" windows: visible, titled, root-owner, non-tool, not ours. - if (!IsWindowVisible(hwnd) || GetAncestor(hwnd, GA_ROOTOWNER) != hwnd) - { + if (!IsWindowVisible(hwnd) || GetAncestor(hwnd, GA_ROOTOWNER) != hwnd) { return TRUE; } const int len = GetWindowTextLengthW(hwnd); - if (len <= 0) - { + if (len <= 0) { return TRUE; } - if ((GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) - { + if ((GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) { return TRUE; } DWORD pid = 0; GetWindowThreadProcessId(hwnd, &pid); - if (pid == 0 || pid == ctx->self_pid) - { + if (pid == 0 || pid == ctx->self_pid) { return TRUE; } @@ -49,8 +42,7 @@ BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lparam) GetWindowTextW(hwnd, title.data(), len + 1); std::wstring exe; - if (const auto it = ctx->names->find(pid); it != ctx->names->end()) - { + if (const auto it = ctx->names->find(pid); it != ctx->names->end()) { exe = it->second; } ctx->out->push_back(WindowEntry{pid, hwnd, std::move(title), std::move(exe)}); @@ -64,8 +56,7 @@ std::vector list_windows() // pid -> image name, so each window can show its owning process without a separate // OpenProcess per window. std::unordered_map names; - for (const ProcessEntry& p : list_processes()) - { + for (const ProcessEntry& p : list_processes()) { names.emplace(p.pid, p.exe_name); } diff --git a/host/src/inject/window_list.hpp b/host/src/inject/window_list.hpp index 23eda13..4fc93e4 100644 --- a/host/src/inject/window_list.hpp +++ b/host/src/inject/window_list.hpp @@ -6,15 +6,13 @@ #include #include -namespace coop -{ +namespace coop { -struct WindowEntry -{ - unsigned long pid = 0; // owning process id - void* hwnd = nullptr; // HWND (opaque here to keep windows.h out of the header) - std::wstring title; // window caption - std::wstring exe_name; // owning process image base name, e.g. "game.exe" +struct WindowEntry { + unsigned long pid = 0; // owning process id + void* hwnd = nullptr; // HWND (opaque here to keep windows.h out of the header) + std::wstring title; // window caption + std::wstring exe_name; // owning process image base name, e.g. "game.exe" }; // Snapshot of the visible, titled, non-tool top-level (alt-tab-style) windows, with diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 50d6723..809115f 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -12,11 +12,9 @@ #include "ui/text_match.hpp" #include "util/utf8.hpp" -namespace coop -{ +namespace coop { -namespace -{ +namespace { const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f); @@ -40,8 +38,7 @@ std::wstring hook_dll_path() const DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH); std::wstring path(buffer, len); const std::size_t slash = path.find_last_of(L"\\/"); - if (slash != std::wstring::npos) - { + if (slash != std::wstring::npos) { path.resize(slash + 1); } path += L"coop_hook.dll"; @@ -67,8 +64,7 @@ InjectionPanel::~InjectionPanel() // stays injected, dormant). Short timeout -- the flags persist in the section the DLL keeps // alive, so the unhook completes even if the process exits before it confirms. disconnect_graceful(/*timeout_ms=*/300); - if (vk_layer_enabled_) - { + if (vk_layer_enabled_) { unregister_vk_layer(); // don't leave the implicit layer registered after the tool closes } close_target_handle(); @@ -80,11 +76,9 @@ void InjectionPanel::disconnect_graceful(int timeout_ms) // (bounded) for it to confirm before we drop the channel. The flags persist in the section the // DLL keeps alive, so it unhooks even if we time out or exit first -- the wait just lets us // observe a clean game. The DLL is left injected (dormant) for a later reconnect; we never eject. - if (server_.running()) - { + if (server_.running()) { server_.request_unhook_all(); - for (int waited = 0; waited < timeout_ms && !server_.all_hooks_removed(); waited += 10) - { + for (int waited = 0; waited < timeout_ms && !server_.all_hooks_removed(); waited += 10) { Sleep(10); } } @@ -95,8 +89,7 @@ void InjectionPanel::disconnect_graceful(int timeout_ms) void InjectionPanel::close_target_handle() { - if (target_process_ != nullptr) - { + if (target_process_ != nullptr) { CloseHandle(target_process_); target_process_ = nullptr; } @@ -114,23 +107,19 @@ void InjectionPanel::tick() void InjectionPanel::auto_reattach_tick() { - if (!auto_reattach_ || target_state_ != TargetState::Terminated || selected_name_.empty()) - { + if (!auto_reattach_ || target_state_ != TargetState::Terminated || selected_name_.empty()) { return; } // Poll the process list a couple of times a second (cheap, and we want to catch the // relaunch early to read the exact audio format before the game creates its client). const double now = ImGui::GetTime(); - if (now - last_auto_poll_ < 0.5) - { + if (now - last_auto_poll_ < 0.5) { return; } last_auto_poll_ = now; refresh_processes(); - for (const ProcessEntry& e : processes_) - { - if (iequals_name(e.exe_name, selected_name_)) - { + for (const ProcessEntry& e : processes_) { + if (iequals_name(e.exe_name, selected_name_)) { // The same game relaunched -> tear down the stale channel and re-attach to it. server_.stop(); close_target_handle(); @@ -144,8 +133,7 @@ void InjectionPanel::auto_reattach_tick() void InjectionPanel::update_liveness() { - if (!injected_) - { + if (!injected_) { target_state_ = TargetState::NotInjected; return; } @@ -153,8 +141,7 @@ void InjectionPanel::update_liveness() // Process gone? The handle was opened with SYNCHRONIZE at inject time, so a // signaled wait means it exited. This is authoritative even if the heartbeat // happened to look alive a moment ago. - if (target_process_ != nullptr && WaitForSingleObject(target_process_, 0) == WAIT_OBJECT_0) - { + if (target_process_ != nullptr && WaitForSingleObject(target_process_, 0) == WAIT_OBJECT_0) { target_state_ = TargetState::Terminated; dll_alive_ = false; return; @@ -164,14 +151,11 @@ void InjectionPanel::update_liveness() // process whose heartbeat stalled for ~2 s is frozen, not gone -- a distinct state. const std::uint32_t hb = server_.hook_status().heartbeat; const double now = ImGui::GetTime(); - if (hb != last_heartbeat_) - { + if (hb != last_heartbeat_) { last_heartbeat_ = hb; last_heartbeat_time_ = now; dll_alive_ = true; - } - else if (now - last_heartbeat_time_ > 2.0) - { + } else if (now - last_heartbeat_time_ > 2.0) { dll_alive_ = false; } target_state_ = dll_alive_ ? TargetState::Alive : TargetState::Hung; @@ -211,23 +195,21 @@ void InjectionPanel::reconnect_selected() // Re-attach to a DLL that's already injected and alive (a prior session left it dormant after a // graceful disconnect, or the tool restarted): bring the channel back up on the SAME per-pid // section the DLL still holds and re-publish the desired subsystem state -- no re-injection. - if (!server_.start(selected_pid_)) - { + if (!server_.start(selected_pid_)) { status_ = "Failed to re-attach shared memory."; status_color_ = kRed; return; } publish_subsystem_state(); begin_liveness_tracking(); - status_ = "Reconnected to " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + - ") -- reused the injected DLL."; + status_ = "Reconnected to " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + + ") -- reused the injected DLL."; status_color_ = kGreen; } void InjectionPanel::inject_selected() { - if (selected_pid_ == 0) - { + if (selected_pid_ == 0) { status_ = "Select a target process first."; status_color_ = kRed; return; @@ -236,16 +218,14 @@ void InjectionPanel::inject_selected() // If our DLL is already injected and alive in this target (left dormant by a graceful disconnect, // or surviving a tool restart -- it keeps the section alive), reconnect to it instead of // injecting a second time. - if (hook_dll_alive(selected_pid_)) - { + if (hook_dll_alive(selected_pid_)) { reconnect_selected(); return; } // Bring up the shared-memory channel before injecting so the hook finds it // immediately on load. - if (!server_.start(selected_pid_)) - { + if (!server_.start(selected_pid_)) { status_ = "Failed to create shared memory."; status_color_ = kRed; return; @@ -254,18 +234,14 @@ void InjectionPanel::inject_selected() publish_subsystem_state(); const InjectResult result = inject_dll(selected_pid_, hook_dll_path()); - if (result.status == InjectStatus::Ok) - { + if (result.status == InjectStatus::Ok) { begin_liveness_tracking(); status_ = "Injected into " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + ")."; status_color_ = kGreen; - } - else - { + } else { server_.stop(); status_ = std::string("Injection failed: ") + to_string(result.status); - if (result.os_error != 0) - { + if (result.os_error != 0) { status_ += " [err " + std::to_string(result.os_error) + "]"; } status_color_ = kRed; @@ -274,31 +250,26 @@ void InjectionPanel::inject_selected() void InjectionPanel::reattach() { - if (selected_name_.empty()) - { + if (selected_name_.empty()) { return; } // Find live processes that share the original target's image name. refresh_processes(); std::vector matches; - for (const ProcessEntry& e : processes_) - { - if (iequals_name(e.exe_name, selected_name_)) - { + for (const ProcessEntry& e : processes_) { + if (iequals_name(e.exe_name, selected_name_)) { matches.push_back(e.pid); } } const std::string name = narrow(selected_name_); - if (matches.empty()) - { + if (matches.empty()) { status_ = "No running \"" + name + "\" to re-attach to."; status_color_ = kRed; return; } - if (matches.size() > 1) - { + if (matches.size() > 1) { // Don't guess which instance: filter the picker to the matches so the operator // chooses, then injects via the normal button. snprintf(filter_, sizeof(filter_), "%s", name.c_str()); @@ -320,10 +291,8 @@ void InjectionPanel::reattach() unsigned long InjectionPanel::dev_inject_by_name(const std::wstring& image_name) { refresh_processes(); - for (const ProcessEntry& e : processes_) - { - if (iequals_name(e.exe_name, image_name)) - { + for (const ProcessEntry& e : processes_) { + if (iequals_name(e.exe_name, image_name)) { selected_pid_ = e.pid; selected_name_ = e.exe_name; inject_selected(); @@ -336,8 +305,7 @@ unsigned long InjectionPanel::dev_inject_by_name(const std::wstring& image_name) void InjectionPanel::publish(const std::array& pads) { - if (!test_input_.load(std::memory_order_relaxed)) - { + if (!test_input_.load(std::memory_order_relaxed)) { server_.publish(pads); return; } @@ -356,8 +324,7 @@ void InjectionPanel::publish(const std::array& pads) pad.state.packet = static_cast(ms); pad.state.thumb_lx = static_cast(std::cos(t) * 30000.0); pad.state.thumb_ly = static_cast(std::sin(t) * 30000.0); - if ((ms / 1000) % 2 == 0) - { + if ((ms / 1000) % 2 == 0) { pad.state.buttons |= 0x1000; // XINPUT_GAMEPAD_A } server_.publish(synthetic); @@ -368,35 +335,28 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status) static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video", "MKB"}; const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries; - if (n == 0) - { + if (n == 0) { return; } - if (!ImGui::CollapsingHeader("Installed hooks", ImGuiTreeNodeFlags_DefaultOpen)) - { + if (!ImGui::CollapsingHeader("Installed hooks", ImGuiTreeNodeFlags_DefaultOpen)) { return; } - if (ImGui::BeginTable("hooks", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) - { + if (ImGui::BeginTable("hooks", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Hook"); ImGui::TableSetupColumn("On", ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("Calls", ImGuiTableColumnFlags_WidthFixed); ImGui::TableHeadersRow(); // Group rows by subsystem so related hooks sit together. - for (std::uint32_t sub = 0; sub < HookSubsys_Count; ++sub) - { + for (std::uint32_t sub = 0; sub < HookSubsys_Count; ++sub) { bool header_done = false; - for (std::uint32_t i = 0; i < n; ++i) - { + for (std::uint32_t i = 0; i < n; ++i) { const HookEntry& e = status.hook_entries[i]; - if (e.subsystem != sub) - { + if (e.subsystem != sub) { continue; } - if (!header_done) - { + if (!header_done) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextDisabled("%s", kSubsysName[sub < HookSubsys_Count ? sub : 0]); @@ -408,12 +368,9 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status) ImGui::TableNextColumn(); ImGui::TextUnformatted(e.name); ImGui::TableNextColumn(); - if (e.installed) - { + if (e.installed) { ImGui::TextColored(kGreen, "yes"); - } - else - { + } else { ImGui::TextDisabled("no"); } ImGui::TableNextColumn(); @@ -428,10 +385,8 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status) static bool subsystem_installed(const HookStatusView& status, std::uint32_t subsystem) { const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries; - for (std::uint32_t i = 0; i < n; ++i) - { - if (status.hook_entries[i].subsystem == subsystem && status.hook_entries[i].installed) - { + for (std::uint32_t i = 0; i < n; ++i) { + if (status.hook_entries[i].subsystem == subsystem && status.hook_entries[i].installed) { return true; } } @@ -442,8 +397,7 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status) { ImGui::SeparatorText("Subsystems (hook / unhook)"); - struct Row - { + struct Row { const char* label; std::uint32_t subsystem; bool* want; @@ -457,25 +411,19 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status) {"Mouse + keyboard forwarding", HookSubsys_Mkb, &want_mkb_, "clicks/keys reach the game"}, }; - for (const Row& r : rows) - { + for (const Row& r : rows) { ImGui::PushID(r.label); - if (ImGui::Checkbox(r.label, r.want)) - { + if (ImGui::Checkbox(r.label, r.want)) { server_.set_subsystem_enabled(r.subsystem, *r.want); } ImGui::SameLine(); const bool on = subsystem_installed(status, r.subsystem); - if (*r.want != on) - { + if (*r.want != on) { ImGui::TextColored(kGrey, "(%s...)", *r.want ? "installing" : "removing"); - } - else - { + } else { ImGui::TextColored(on ? kGreen : kGrey, on ? "installed" : "off"); } - if (!*r.want) - { + if (!*r.want) { ImGui::TextDisabled(" off: %s won't work", r.depends); } ImGui::PopID(); @@ -483,33 +431,28 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status) // Cursor release is a Focus sub-option for games that clip/recenter the mouse, // which would otherwise trap the operator. - if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_)) - { + if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_)) { server_.set_cursor_clip_allowed(!release_cursor_); } } void InjectionPanel::draw_hook_status(bool debug_details) { - if (!server_.running()) - { + if (!server_.running()) { return; } const HookStatusView status = server_.hook_status(); ImGui::SeparatorText("Hook status"); - if (!injected_) - { + if (!injected_) { ImGui::TextColored(kGrey, "Not injected."); return; } - switch (target_state_) - { + switch (target_state_) { case TargetState::Alive: - ImGui::TextColored(kGreen, "Hook DLL loaded in pid %lu (heartbeat %u)", server_.target_pid(), - status.heartbeat); + ImGui::TextColored(kGreen, "Hook DLL loaded in pid %lu (heartbeat %u)", server_.target_pid(), status.heartbeat); break; case TargetState::Hung: ImGui::TextColored(kRed, "Target not responding -- heartbeat stalled (frozen?)."); @@ -525,16 +468,14 @@ void InjectionPanel::draw_hook_status(bool debug_details) ImGui::BeginDisabled(target_state_ != TargetState::Alive); draw_subsystem_controls(status); ImGui::EndDisabled(); - if (target_state_ != TargetState::Alive) - { + if (target_state_ != TargetState::Alive) { ImGui::TextDisabled("(connect to a live game to change these)"); // why the toggles are locked } ImGui::TextDisabled("Controller poll rates are in the Controllers panel."); draw_hook_list(status); - if (!debug_details) - { + if (!debug_details) { return; // everything below is diagnostic detail } @@ -548,17 +489,12 @@ void InjectionPanel::draw_hook_status(bool debug_details) // Input-path diagnostics: a focus-gated detection path would explain a game // that only accepts the controller when it has true focus. ImGui::SeparatorText("Input path"); - if (status.raw_input_gamepad) - { + if (status.raw_input_gamepad) { ImGui::TextColored(status.raw_input_gamepad_sink ? kGreen : kRed, "Raw Input gamepad: yes (INPUTSINK %s)", status.raw_input_gamepad_sink ? "set -> bg ok" : "MISSING -> focus-gated!"); - } - else if (status.raw_input_registered) - { + } else if (status.raw_input_registered) { ImGui::TextColored(kGrey, "Raw Input: registered, but not for a gamepad usage"); - } - else - { + } else { ImGui::TextColored(kGrey, "Raw Input: not registered"); } ImGui::TextColored(status.dinput_loaded ? kRed : kGrey, "DirectInput dll loaded: %s", @@ -570,22 +506,15 @@ void InjectionPanel::draw(bool debug_details) apply_panel_layout(Panel::Injection); ImGui::Begin("Injection"); - if (server_.running()) - { - if (target_state_ == TargetState::Terminated) - { + if (server_.running()) { + if (target_state_ == TargetState::Terminated) { ImGui::TextColored(kRed, "Target (pid %lu) has terminated.", server_.target_pid()); - } - else if (target_state_ == TargetState::Hung) - { + } else if (target_state_ == TargetState::Hung) { ImGui::TextColored(kRed, "Target (pid %lu) is not responding.", server_.target_pid()); - } - else - { + } else { ImGui::TextColored(kGreen, "Connected to pid %lu", server_.target_pid()); } - if (ImGui::Button("Disconnect")) - { + if (ImGui::Button("Disconnect")) { // Leave the game vanilla: unhook everything before dropping the channel. The DLL stays // injected (dormant), so it can be reconnected later without re-injecting. disconnect_graceful(/*timeout_ms=*/700); @@ -594,11 +523,9 @@ void InjectionPanel::draw(bool debug_details) } // A relaunched game has a new pid; re-attach by image name without hunting for // it in the list. Only offered once the old target is gone. - if (target_state_ == TargetState::Terminated && !selected_name_.empty()) - { + if (target_state_ == TargetState::Terminated && !selected_name_.empty()) { ImGui::SameLine(); - if (ImGui::Button("Re-attach")) - { + if (ImGui::Button("Re-attach")) { reattach(); } ImGui::SameLine(); @@ -609,32 +536,25 @@ void InjectionPanel::draw(bool debug_details) // Session options for the selected game, shown whether or not we're connected -- so they can be set // up before launching the game, and an enabled auto re-attach is never hidden after a disconnect. - if (!selected_name_.empty()) - { + if (!selected_name_.empty()) { // Session-only auto re-attach: tick it, then kill + relaunch the game and it re-injects itself // early -- the kill+relaunch fix for a wrong audio format, without picking a target again. ImGui::Checkbox("Auto re-attach this game on relaunch", &auto_reattach_); - if (auto_reattach_ && target_state_ == TargetState::Terminated) - { + if (auto_reattach_ && target_state_ == TargetState::Terminated) { ImGui::SameLine(); ImGui::TextColored(kGrey, "(watching for %s...)", narrow(selected_name_).c_str()); } // Opt-in Vulkan capture layer: for Vulkan games that initialize Vulkan immediately (where even // auto-attach injects too late -- see the red banner), register a per-user implicit layer scoped // to this game so the next launch is captured from the first frame. Removed when unticked / exit. - if (ImGui::Checkbox("Set up Vulkan layer (for immediate-init Vulkan games)", &vk_layer_enabled_)) - { - if (vk_layer_enabled_) - { + if (ImGui::Checkbox("Set up Vulkan layer (for immediate-init Vulkan games)", &vk_layer_enabled_)) { + if (vk_layer_enabled_) { vk_layer_enabled_ = register_vk_layer(selected_name_); - } - else - { + } else { unregister_vk_layer(); } } - if (ImGui::IsItemHovered()) - { + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Registers a per-user (HKCU, no admin) implicit Vulkan layer scoped to\n" "this game, so a relaunch is captured before Vulkan init. Pair with\n" "Auto re-attach. Removed when you untick it or close the tool."); @@ -642,28 +562,23 @@ void InjectionPanel::draw(bool debug_details) } ImGui::TextUnformatted("Target window"); - if (ImGui::Button("Refresh")) - { + if (ImGui::Button("Refresh")) { refresh_targets(); } ImGui::SameLine(); ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##wfilter", "filter by title or process...", window_filter_, sizeof(window_filter_)); - if (ImGui::BeginListBox("##windows", ImVec2(-1.0f, 180.0f))) - { - for (const WindowEntry& w : windows_) - { - if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_)) - { + if (ImGui::BeginListBox("##windows", ImVec2(-1.0f, 180.0f))) { + for (const WindowEntry& w : windows_) { + if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_)) { continue; } const bool selected = w.pid == selected_pid_; char label[400]; - snprintf(label, sizeof(label), "%-32s [%s %lu]", narrow(w.title).c_str(), - narrow(w.exe_name).c_str(), w.pid); - if (ImGui::Selectable(label, selected)) - { + snprintf(label, sizeof(label), "%-32s [%s %lu]", narrow(w.title).c_str(), narrow(w.exe_name).c_str(), + w.pid); + if (ImGui::Selectable(label, selected)) { selected_pid_ = w.pid; selected_name_ = w.exe_name; } @@ -673,24 +588,19 @@ void InjectionPanel::draw(bool debug_details) // The full process list is the advanced fallback (e.g. a windowless game host), // kept out of the way unless the operator wants it. - if (debug_details) - { + if (debug_details) { ImGui::SeparatorText("All processes (advanced)"); ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##filter", "filter by name...", filter_, sizeof(filter_)); - if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 160.0f))) - { - for (const ProcessEntry& entry : processes_) - { - if (!contains_ci_w(entry.exe_name, filter_)) - { + if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 160.0f))) { + for (const ProcessEntry& entry : processes_) { + if (!contains_ci_w(entry.exe_name, filter_)) { continue; } const bool selected = entry.pid == selected_pid_; char label[300]; snprintf(label, sizeof(label), "%-40s %lu", narrow(entry.exe_name).c_str(), entry.pid); - if (ImGui::Selectable(label, selected)) - { + if (ImGui::Selectable(label, selected)) { selected_pid_ = entry.pid; selected_name_ = entry.exe_name; } @@ -701,20 +611,17 @@ void InjectionPanel::draw(bool debug_details) const bool can_inject = selected_pid_ != 0; ImGui::BeginDisabled(!can_inject); - if (ImGui::Button("Inject & Connect", ImVec2(-1.0f, 0.0f))) - { + if (ImGui::Button("Inject & Connect", ImVec2(-1.0f, 0.0f))) { inject_selected(); } ImGui::EndDisabled(); // Explain the disabled state on hover (AllowWhenDisabled, since the button is greyed out). - if (!can_inject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - { + if (!can_inject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Pick a target window or process above first.\n" "If the game already has the DLL (e.g. after a reconnect), this reuses it."); } - if (!status_.empty()) - { + if (!status_.empty()) { ImGui::TextColored(status_color_, "%s", status_.c_str()); } diff --git a/host/src/injection_panel.hpp b/host/src/injection_panel.hpp index d603dc4..bac67dc 100644 --- a/host/src/injection_panel.hpp +++ b/host/src/injection_panel.hpp @@ -15,21 +15,18 @@ #include "inject/window_list.hpp" #include "ipc/ipc_server.hpp" -namespace coop -{ +namespace coop { // Liveness of the injected target, surfaced in the UI so a dead/hung game is obvious. -enum class TargetState -{ +enum class TargetState { NotInjected, // no hook loaded Alive, // process running and the hook heartbeat is advancing Hung, // process still exists but the heartbeat stalled (not responding) Terminated, // process has exited }; -class InjectionPanel -{ -public: +class InjectionPanel { + public: InjectionPanel(); ~InjectionPanel(); @@ -49,10 +46,7 @@ public: // Test harness (debug builds only): inject into the first running process whose image // name matches. Returns the pid on success, 0 otherwise. Same path as the UI button. unsigned long dev_inject_by_name(const std::wstring& image_name); - void dev_set_auto_reattach(bool on) - { - auto_reattach_ = on; - } + void dev_set_auto_reattach(bool on) { auto_reattach_ = on; } #endif // Forward the latest pad snapshot to the injected hook (if connected). When @@ -61,35 +55,25 @@ public: // Enable/disable synthetic test input. The toggle itself lives in the Controllers // panel (a controller-debug aid); the host feeds its state here each frame. - void set_test_input(bool on) - { - test_input_.store(on, std::memory_order_relaxed); - } + void set_test_input(bool on) { test_input_.store(on, std::memory_order_relaxed); } // The injected game's main window, as reported by the hook (null if none). A // terminated target's HWND is stale/invalid, so report none -- the capture and // audio panels then drop to idle instead of chasing a dead window. [[nodiscard]] HWND game_hwnd() const { - if (target_state_ == TargetState::Terminated) - { + if (target_state_ == TargetState::Terminated) { return nullptr; } return reinterpret_cast(server_.hook_status().game_hwnd); } // Current liveness of the injected target (for other panels / status). - [[nodiscard]] TargetState target_state() const - { - return target_state_; - } + [[nodiscard]] TargetState target_state() const { return target_state_; } // The hook's full diagnostics back-channel (other panels read the audio // render-stream counts from here). - [[nodiscard]] HookStatusView hook_status() const - { - return server_.hook_status(); - } + [[nodiscard]] HookStatusView hook_status() const { return server_.hook_status(); } // Drain log lines the hook streamed (for the Log window). No-op if not active. template @@ -100,23 +84,14 @@ public: // Emit a host-side line into the Log window (color-coded by level), e.g. an // override-overwrite warning. No-op if not connected. - void host_log(std::uint32_t level, const char* text) - { - server_.host_log(level, text); - } + void host_log(std::uint32_t level, const char* text) { server_.host_log(level, text); } // --- Present-hook video path (consumed by the Video mirror panel) ---------- - [[nodiscard]] unsigned long target_pid() const - { - return server_.target_pid(); - } + [[nodiscard]] unsigned long target_pid() const { return server_.target_pid(); } // The hook's Present-hook video channel snapshot (shared-texture descriptor). - [[nodiscard]] VideoShareView video_share() const - { - return server_.video_share(); - } + [[nodiscard]] VideoShareView video_share() const { return server_.video_share(); } // Request the Present-hook video subsystem be installed/removed. Keeps the // Injection panel's own checkbox in sync, so the Video panel can drive it. @@ -126,25 +101,16 @@ public: server_.set_subsystem_enabled(HookSubsys_Video, on); } - [[nodiscard]] bool video_requested() const - { - return want_video_; - } + [[nodiscard]] bool video_requested() const { return want_video_; } // --- Mouse + keyboard forwarding ------------------------------------------- // Whether the operator enabled the MKB-forwarding subsystem (the toggle is the // hook). The host's MKB forwarder only runs while this is on and a hook is alive. - [[nodiscard]] bool mkb_enabled() const - { - return want_mkb_ && injected_; - } + [[nodiscard]] bool mkb_enabled() const { return want_mkb_ && injected_; } // Enqueue an MKB event for the hook to forward into the game. - void push_mkb(const MkbEvent& ev) - { - server_.push_mkb(ev); - } + void push_mkb(const MkbEvent& ev) { server_.push_mkb(ev); } // --- Cursor release (for cursor-clipping games) ---------------------------- @@ -156,31 +122,28 @@ public: server_.set_cursor_clip_allowed(!release_cursor_); } - [[nodiscard]] bool cursor_released() const - { - return release_cursor_; - } + [[nodiscard]] bool cursor_released() const { return release_cursor_; } -private: - void refresh_targets(); // refresh both the window list and the process list + private: + void refresh_targets(); // refresh both the window list and the process list void refresh_processes(); - void inject_selected(); // inject fresh, OR reconnect if a live DLL is already in the target - void reconnect_selected(); // re-attach to an already-injected, live DLL (no re-inject) + void inject_selected(); // inject fresh, OR reconnect if a live DLL is already in the target + void reconnect_selected(); // re-attach to an already-injected, live DLL (no re-inject) void publish_subsystem_state(); // push the desired per-subsystem install state + cursor policy void begin_liveness_tracking(); // mark connected: open the process handle, seed the heartbeat clock // Graceful disconnect: ask the DLL to remove every hook (game returns to vanilla), wait // (bounded) for it to take effect, then drop the channel. The DLL stays injected/dormant for a // later reconnect; we never eject it. Used by the Disconnect button and the destructor. void disconnect_graceful(int timeout_ms); - void reattach(); // re-inject a relaunched same-name target (Terminated state) - void auto_reattach_tick(); // poll for the same game relaunching while auto-reattach is on - void update_liveness(); // recompute target_state_ from process + heartbeat - void close_target_handle(); // close target_process_ and reset liveness state + void reattach(); // re-inject a relaunched same-name target (Terminated state) + void auto_reattach_tick(); // poll for the same game relaunching while auto-reattach is on + void update_liveness(); // recompute target_state_ from process + heartbeat + void close_target_handle(); // close target_process_ and reset liveness state void draw_subsystem_controls(const HookStatusView& status); void draw_hook_list(const HookStatusView& status); void draw_hook_status(bool debug_details); - std::vector windows_; // default picker (visible top-level windows) + std::vector windows_; // default picker (visible top-level windows) std::vector processes_; // advanced picker (all processes) char window_filter_[128] = {}; char filter_[128] = {}; @@ -200,17 +163,17 @@ private: bool want_input_ = true; bool want_focus_ = true; bool want_audio_ = true; - bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default) - bool want_mkb_ = false; // mouse+keyboard forwarding: opt-in + bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default) + bool want_mkb_ = false; // mouse+keyboard forwarding: opt-in bool release_cursor_ = true; // free the operator's mouse from the game's clip (default) - bool injected_ = false; // a hook DLL is loaded in the target + bool injected_ = false; // a hook DLL is loaded in the target // Session-only (never persisted): while on and the target has terminated, auto-inject // the same image name the moment it relaunches, so a quick kill+relaunch re-attaches // early (catching IAudioClient::Initialize) without the operator picking a target. bool auto_reattach_ = false; bool vk_layer_enabled_ = false; // opt-in: registered the implicit Vulkan capture layer for this game - double last_auto_poll_ = 0.0; // throttle the process-list poll + double last_auto_poll_ = 0.0; // throttle the process-list poll // Heartbeat liveness tracking (is the injected DLL responding?). std::uint32_t last_heartbeat_ = 0; diff --git a/host/src/input/input_source.hpp b/host/src/input/input_source.hpp index 145d414..b77f149 100644 --- a/host/src/input/input_source.hpp +++ b/host/src/input/input_source.hpp @@ -12,11 +12,9 @@ #include "coop/protocol.hpp" -namespace coop -{ +namespace coop { -struct PadInfo -{ +struct PadInfo { bool connected = false; CoopPadState state = {}; std::string source; // human-readable label for the debug overlay @@ -25,16 +23,14 @@ struct PadInfo // A copy-safe snapshot of the input backend's state, published by the input worker // thread for the UI to display. This decouples the Controllers panel (UI thread) from // the worker's live polling, so the worker can own its InputSource exclusively. -struct InputSnapshot -{ +struct InputSnapshot { std::array pads{}; const char* backend = "XInput"; // backend name (static string literal; thread-safe to share) bool steam_active = false; }; -class InputSource -{ -public: +class InputSource { + public: virtual ~InputSource() = default; // Name of the backend, shown in the overlay. diff --git a/host/src/input/input_worker.cpp b/host/src/input/input_worker.cpp index 3c29f25..3929b8e 100644 --- a/host/src/input/input_worker.cpp +++ b/host/src/input/input_worker.cpp @@ -11,8 +11,7 @@ #include "input/steam_input_source.hpp" #endif -namespace coop -{ +namespace coop { InputWorker::~InputWorker() { @@ -21,8 +20,7 @@ InputWorker::~InputWorker() void InputWorker::start(InjectionPanel* injection, std::string steam_manifest) { - if (running_.load(std::memory_order_acquire)) - { + if (running_.load(std::memory_order_acquire)) { return; } injection_ = injection; @@ -34,8 +32,7 @@ void InputWorker::start(InjectionPanel* injection, std::string steam_manifest) void InputWorker::stop() { running_.store(false, std::memory_order_release); - if (thread_.joinable()) - { + if (thread_.joinable()) { thread_.join(); } } @@ -68,36 +65,28 @@ void InputWorker::run() std::uint16_t last_rumble_l[kMaxPads] = {}; std::uint16_t last_rumble_r[kMaxPads] = {}; - while (running_.load(std::memory_order_relaxed)) - { + while (running_.load(std::memory_order_relaxed)) { const bool want_steam = want_steam_.load(std::memory_order_relaxed); #ifdef COOP_WITH_STEAM // Reconcile the backend with the UI's request. Initializing Steam Input hijacks // XInput, so it's strictly opt-in; a failed init falls back to plain XInput and // flags steam_failed_ so the UI can reset its toggle (and a later retry is // possible once the request is cleared). - if (want_steam && steam == nullptr && !steam_failed_.load(std::memory_order_relaxed)) - { + if (want_steam && steam == nullptr && !steam_failed_.load(std::memory_order_relaxed)) { steam = std::make_unique(); - if (steam->init(steam_manifest_)) - { + if (steam->init(steam_manifest_)) { active = steam.get(); - } - else - { + } else { steam.reset(); active = &xinput; steam_failed_.store(true, std::memory_order_relaxed); } - } - else if (!want_steam && steam != nullptr) - { + } else if (!want_steam && steam != nullptr) { steam->shutdown(); steam.reset(); active = &xinput; } - if (!want_steam) - { + if (!want_steam) { steam_failed_.store(false, std::memory_order_relaxed); // allow a future retry } const bool steam_active = steam != nullptr; @@ -108,17 +97,14 @@ void InputWorker::run() active->poll(); - if (injection_ != nullptr) - { + if (injection_ != nullptr) { // Push the latest pads to the game (publish() substitutes synthetic test input // itself when that mode is on), then forward any newly requested rumble. injection_->publish(active->pads()); const HookStatusView hs = injection_->hook_status(); - for (int i = 0; i < static_cast(kMaxPads); ++i) - { - if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i]) - { + for (int i = 0; i < static_cast(kMaxPads); ++i) { + if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i]) { active->set_rumble(i, hs.rumble_left[i], hs.rumble_right[i]); last_rumble_l[i] = hs.rumble_left[i]; last_rumble_r[i] = hs.rumble_right[i]; @@ -134,8 +120,7 @@ void InputWorker::run() } #ifdef COOP_WITH_STEAM - if (steam != nullptr) - { + if (steam != nullptr) { steam->shutdown(); } #endif diff --git a/host/src/input/input_worker.hpp b/host/src/input/input_worker.hpp index 2c365b8..f343d66 100644 --- a/host/src/input/input_worker.hpp +++ b/host/src/input/input_worker.hpp @@ -13,14 +13,12 @@ #include "input/input_source.hpp" -namespace coop -{ +namespace coop { class InjectionPanel; -class InputWorker -{ -public: +class InputWorker { + public: InputWorker() = default; ~InputWorker(); @@ -35,22 +33,16 @@ public: void stop(); // UI -> worker: request the Steam Input backend (true) or plain XInput (false). - void set_want_steam(bool on) - { - want_steam_.store(on, std::memory_order_relaxed); - } + void set_want_steam(bool on) { want_steam_.store(on, std::memory_order_relaxed); } // worker -> UI: latest snapshot for the Controllers panel (thread-safe copy). [[nodiscard]] InputSnapshot snapshot() const; // worker -> UI: Steam Input was requested but failed to start (so the panel can // reset its toggle and fall back to XInput). Cleared once Steam is not requested. - [[nodiscard]] bool steam_failed() const - { - return steam_failed_.load(std::memory_order_relaxed); - } + [[nodiscard]] bool steam_failed() const { return steam_failed_.load(std::memory_order_relaxed); } -private: + private: void run(); void publish_snapshot(const InputSource& src, bool steam_active); diff --git a/host/src/input/steam_input_source.cpp b/host/src/input/steam_input_source.cpp index 508d797..f9cdda4 100644 --- a/host/src/input/steam_input_source.cpp +++ b/host/src/input/steam_input_source.cpp @@ -7,16 +7,13 @@ #include -namespace coop -{ +namespace coop { -namespace -{ +namespace { // Digital actions in the manifest, paired with the XInput button bit they map to. // Names must match host/assets/steam_input_actions.vdf. -struct ButtonAction -{ +struct ButtonAction { const char* action; std::uint16_t xinput_bit; }; @@ -40,12 +37,10 @@ const ButtonAction kButtons[kSteamButtonActions] = { std::int16_t to_axis(float v) { - if (v > 1.0f) - { + if (v > 1.0f) { v = 1.0f; } - if (v < -1.0f) - { + if (v < -1.0f) { v = -1.0f; } return static_cast(v * 32767.0f); @@ -53,12 +48,10 @@ std::int16_t to_axis(float v) std::uint8_t to_trigger(float v) { - if (v > 1.0f) - { + if (v > 1.0f) { v = 1.0f; } - if (v < 0.0f) - { + if (v < 0.0f) { v = 0.0f; } return static_cast(v * 255.0f); @@ -75,25 +68,21 @@ bool SteamInputSource::init(const std::string& manifest_absolute_path) { // Running standalone (not launched by Steam) without a steam_appid.txt makes // SteamAPI_Init fail; that's fine -- we degrade to XInput. - if (!SteamAPI_Init()) - { + if (!SteamAPI_Init()) { std::printf("SteamInput: SteamAPI_Init failed (not under Steam?); using XInput.\n"); return false; } - if (SteamInput() == nullptr) - { + if (SteamInput() == nullptr) { std::printf("SteamInput: ISteamInput unavailable; using XInput.\n"); SteamAPI_Shutdown(); return false; } // Point Steam Input at our bundled action manifest so we don't depend on a // partner-backend-registered config. Must be called before Init(). - if (!manifest_absolute_path.empty()) - { + if (!manifest_absolute_path.empty()) { SteamInput()->SetInputActionManifestFilePath(manifest_absolute_path.c_str()); } - if (!SteamInput()->Init(/*bExplicitlyCallRunFrame=*/false)) - { + if (!SteamInput()->Init(/*bExplicitlyCallRunFrame=*/false)) { std::printf("SteamInput: ISteamInput::Init failed; using XInput.\n"); SteamAPI_Shutdown(); return false; @@ -108,8 +97,7 @@ bool SteamInputSource::init(const std::string& manifest_absolute_path) void SteamInputSource::shutdown() { - if (steam_ready_) - { + if (steam_ready_) { SteamInput()->Shutdown(); SteamAPI_Shutdown(); steam_ready_ = false; @@ -120,8 +108,7 @@ void SteamInputSource::shutdown() void SteamInputSource::resolve_handles() { action_set_ = SteamInput()->GetActionSetHandle("GameControls"); - for (int i = 0; i < kSteamButtonActions; ++i) - { + for (int i = 0; i < kSteamButtonActions; ++i) { button_handles_[i] = SteamInput()->GetDigitalActionHandle(kButtons[i].action); } left_stick_ = SteamInput()->GetAnalogActionHandle("LeftStick"); @@ -138,12 +125,10 @@ bool SteamInputSource::read_steam_pad(std::uint64_t controller, PadInfo& out) co st.connected = 1; bool any_active = false; - for (int i = 0; i < kSteamButtonActions; ++i) - { + for (int i = 0; i < kSteamButtonActions; ++i) { const InputDigitalActionData_t d = SteamInput()->GetDigitalActionData(controller, button_handles_[i]); any_active = any_active || d.bActive; - if (d.bState) - { + if (d.bState) { st.buttons |= kButtons[i].xinput_bit; } } @@ -163,8 +148,7 @@ bool SteamInputSource::read_steam_pad(std::uint64_t controller, PadInfo& out) co // No action is bound/active (e.g. the controller isn't using our manifest) -> // let the XInput fallback handle this slot instead of reporting an empty pad. - if (!any_active) - { + if (!any_active) { return false; } @@ -182,8 +166,7 @@ void SteamInputSource::poll() xinput_.poll(); pads_ = xinput_.pads(); - if (!steam_ready_) - { + if (!steam_ready_) { steam_count_ = 0; return; } @@ -192,17 +175,14 @@ void SteamInputSource::poll() InputHandle_t handles[STEAM_INPUT_MAX_COUNT] = {}; steam_count_ = SteamInput()->GetConnectedControllers(handles); - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { controllers_[i] = 0; steam_slot_[i] = false; } - for (int i = 0; i < steam_count_ && i < static_cast(kMaxPads); ++i) - { + for (int i = 0; i < steam_count_ && i < static_cast(kMaxPads); ++i) { controllers_[i] = handles[i]; PadInfo steam_pad; - if (read_steam_pad(handles[i], steam_pad)) - { + if (read_steam_pad(handles[i], steam_pad)) { pads_[i] = steam_pad; // Steam controller active on this slot -> use it steam_slot_[i] = true; } @@ -211,12 +191,10 @@ void SteamInputSource::poll() void SteamInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right) { - if (slot < 0 || slot >= static_cast(kMaxPads)) - { + if (slot < 0 || slot >= static_cast(kMaxPads)) { return; } - if (steam_ready_ && steam_slot_[slot] && controllers_[slot] != 0) - { + if (steam_ready_ && steam_slot_[slot] && controllers_[slot] != 0) { SteamInput()->TriggerVibration(controllers_[slot], left, right); return; } diff --git a/host/src/input/steam_input_source.hpp b/host/src/input/steam_input_source.hpp index bc7f474..df8a9c5 100644 --- a/host/src/input/steam_input_source.hpp +++ b/host/src/input/steam_input_source.hpp @@ -14,15 +14,13 @@ #include "input/input_source.hpp" #include "input/xinput_source.hpp" -namespace coop -{ +namespace coop { // Number of digital (button) actions in the bundled action manifest. inline constexpr int kSteamButtonActions = 15; -class SteamInputSource final : public InputSource -{ -public: +class SteamInputSource final : public InputSource { + public: ~SteamInputSource() override; // Initializes SteamAPI + Steam Input and loads the action manifest at the given @@ -31,30 +29,18 @@ public: bool init(const std::string& manifest_absolute_path); void shutdown(); - [[nodiscard]] const char* name() const override - { - return name_; - } + [[nodiscard]] const char* name() const override { return name_; } void poll() override; - [[nodiscard]] const std::array& pads() const override - { - return pads_; - } + [[nodiscard]] const std::array& pads() const override { return pads_; } // Forward rumble to the guest: SteamInput TriggerVibration on the slot's // controller when it's Steam-active, else the XInput fallback. void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override; - [[nodiscard]] bool steam_active() const - { - return steam_ready_; - } - [[nodiscard]] int steam_controllers() const - { - return steam_count_; - } + [[nodiscard]] bool steam_active() const { return steam_ready_; } + [[nodiscard]] int steam_controllers() const { return steam_count_; } -private: + private: void resolve_handles(); bool read_steam_pad(std::uint64_t controller, PadInfo& out) const; diff --git a/host/src/input/xinput_source.cpp b/host/src/input/xinput_source.cpp index d1eaf65..b267b26 100644 --- a/host/src/input/xinput_source.cpp +++ b/host/src/input/xinput_source.cpp @@ -3,19 +3,16 @@ #include #include -namespace coop -{ +namespace coop { void XInputSource::poll() { - for (DWORD i = 0; i < kMaxPads; ++i) - { + for (DWORD i = 0; i < kMaxPads; ++i) { XINPUT_STATE state = {}; const DWORD result = XInputGetState(i, &state); PadInfo& info = pads_[i]; - if (result == ERROR_SUCCESS) - { + if (result == ERROR_SUCCESS) { info.connected = true; info.source = "XInput #" + std::to_string(i); @@ -31,9 +28,7 @@ void XInputSource::poll() info.state.thumb_ly = g.sThumbLY; info.state.thumb_rx = g.sThumbRX; info.state.thumb_ry = g.sThumbRY; - } - else - { + } else { info = PadInfo{}; } } @@ -41,8 +36,7 @@ void XInputSource::poll() void XInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right) { - if (slot < 0 || slot >= static_cast(kMaxPads)) - { + if (slot < 0 || slot >= static_cast(kMaxPads)) { return; } XINPUT_VIBRATION v{left, right}; diff --git a/host/src/input/xinput_source.hpp b/host/src/input/xinput_source.hpp index 412bdcd..49768f4 100644 --- a/host/src/input/xinput_source.hpp +++ b/host/src/input/xinput_source.hpp @@ -2,30 +2,22 @@ #include "input/input_source.hpp" -namespace coop -{ +namespace coop { // Reads the four XInput slots. Remote Play Together exposes guest controllers // here, alongside any controllers physically attached to the host. -class XInputSource final : public InputSource -{ -public: - [[nodiscard]] const char* name() const override - { - return "XInput"; - } +class XInputSource final : public InputSource { + public: + [[nodiscard]] const char* name() const override { return "XInput"; } void poll() override; - [[nodiscard]] const std::array& pads() const override - { - return pads_; - } + [[nodiscard]] const std::array& pads() const override { return pads_; } // Forward rumble to the XInput device at `slot` (the guest's RPT virtual pad). void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override; -private: + private: std::array pads_; }; diff --git a/host/src/ipc/ipc_server.cpp b/host/src/ipc/ipc_server.cpp index 9796fe0..46927a1 100644 --- a/host/src/ipc/ipc_server.cpp +++ b/host/src/ipc/ipc_server.cpp @@ -3,11 +3,9 @@ #include #include -namespace coop -{ +namespace coop { -namespace -{ +namespace { // The hook writes these cumulative diagnostic counters cross-process (an x86 DLL can do a 64-bit // store in two halves), so read them atomically to avoid a torn value. The shared mapping is // genuinely mutable -- the const here is just our read-only view -- so const_cast for atomic_ref. @@ -22,8 +20,7 @@ bool IpcServer::start(unsigned long target_pid) std::scoped_lock lock(mutex_); stop_locked(); - if (!shm_.create(shared_memory_name(target_pid), sizeof(SharedBlock))) - { + if (!shm_.create(shared_memory_name(target_pid), sizeof(SharedBlock))) { return false; } @@ -40,8 +37,7 @@ bool IpcServer::start(unsigned long target_pid) // Log ring: the injected hook opens this and streams its log lines back for the // Log window. Best-effort -- the rest of the tool works without it. - if (log_shm_.create(log_ring_name(target_pid), log_ring_total_size(kLogCapacity))) - { + if (log_shm_.create(log_ring_name(target_pid), log_ring_total_size(kLogCapacity))) { log_ring_ = log_shm_.as(); log_ring_init(*log_ring_, kLogCapacity); log_cursor_ = 0; @@ -52,13 +48,11 @@ bool IpcServer::start(unsigned long target_pid) void IpcServer::publish(const std::array& pads) { std::scoped_lock lock(mutex_); - if (block_ == nullptr) - { + if (block_ == nullptr) { return; } CoopPadState states[kMaxPads]; - for (std::size_t i = 0; i < pads.size(); ++i) - { + for (std::size_t i = 0; i < pads.size(); ++i) { states[i] = pads[i].state; states[i].connected = pads[i].connected ? 1 : 0; } @@ -69,21 +63,18 @@ HookStatusView IpcServer::hook_status() const { std::scoped_lock lock(mutex_); HookStatusView view; - if (block_ == nullptr) - { + if (block_ == nullptr) { return view; } const HookStatus& s = block_->status; view.attached = s.attached != 0; view.focus_spoof = s.focus_spoof != 0; view.heartbeat = s.heartbeat.load(std::memory_order_relaxed); - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { view.get_state[i] = s.get_state_calls[i].load(std::memory_order_relaxed); view.get_caps[i] = s.get_caps_calls[i].load(std::memory_order_relaxed); } - for (std::uint32_t i = 0; i < FocusApi_Count; ++i) - { + for (std::uint32_t i = 0; i < FocusApi_Count; ++i) { view.focus_calls[i] = s.focus_query_calls[i].load(std::memory_order_relaxed); } view.game_pid = s.game_pid; @@ -94,18 +85,15 @@ HookStatusView IpcServer::hook_status() const view.dinput_loaded = s.dinput_loaded != 0; view.vk_too_late = s.vk_too_late != 0; view.audio_streams_seen = s.audio_streams_seen; - for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) - { + for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) { view.audio_streams[i] = s.audio_streams[i]; view.audio_streams[i].frames_rendered = atomic_load_u64(s.audio_streams[i].frames_rendered); } view.hook_entry_count = s.hook_entry_count; - for (std::uint32_t i = 0; i < kMaxHookEntries; ++i) - { + for (std::uint32_t i = 0; i < kMaxHookEntries; ++i) { view.hook_entries[i] = s.hook_entries[i]; } - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { view.rumble_left[i] = s.rumble_left[i]; view.rumble_right[i] = s.rumble_right[i]; view.read_state[i] = s.read_state[i]; @@ -117,8 +105,7 @@ VideoShareView IpcServer::video_share() const { std::scoped_lock lock(mutex_); VideoShareView v; - if (block_ == nullptr) - { + if (block_ == nullptr) { return v; } const VideoShare& s = block_->video; @@ -135,8 +122,7 @@ VideoShareView IpcServer::video_share() const void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled) { std::scoped_lock lock(mutex_); - if (block_ != nullptr && subsystem < HookSubsys_Count) - { + if (block_ != nullptr && subsystem < HookSubsys_Count) { // 0 = install, 1 = remove. block_->control.subsystem_disabled[subsystem].store(enabled ? 0u : 1u, std::memory_order_release); } @@ -145,12 +131,10 @@ void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled) void IpcServer::request_unhook_all() { std::scoped_lock lock(mutex_); - if (block_ == nullptr) - { + if (block_ == nullptr) { return; } - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block_->control.subsystem_disabled[s].store(1u, std::memory_order_release); // 1 = remove } } @@ -158,20 +142,16 @@ void IpcServer::request_unhook_all() bool IpcServer::all_hooks_removed() const { std::scoped_lock lock(mutex_); - if (block_ == nullptr) - { + if (block_ == nullptr) { return true; // not connected -> nothing of ours is hooked } const HookStatus& s = block_->status; std::uint32_t count = s.hook_entry_count; - if (count > kMaxHookEntries) - { + if (count > kMaxHookEntries) { count = kMaxHookEntries; } - for (std::uint32_t i = 0; i < count; ++i) - { - if (s.hook_entries[i].installed != 0) - { + for (std::uint32_t i = 0; i < count; ++i) { + if (s.hook_entries[i].installed != 0) { return false; } } @@ -181,8 +161,7 @@ bool IpcServer::all_hooks_removed() const void IpcServer::host_log(std::uint32_t level, const char* text) { std::scoped_lock lock(mutex_); - if (log_ring_ != nullptr) - { + if (log_ring_ != nullptr) { log_ring_push(*log_ring_, GetCurrentProcessId(), level, GetTickCount64(), text); } } @@ -195,8 +174,7 @@ void IpcServer::stop() void IpcServer::stop_locked() { - if (block_ != nullptr) - { + if (block_ != nullptr) { block_->magic = 0; // invalidate so a late hook read won't trust stale data block_ = nullptr; } diff --git a/host/src/ipc/ipc_server.hpp b/host/src/ipc/ipc_server.hpp index 9c169d5..ec9bc99 100644 --- a/host/src/ipc/ipc_server.hpp +++ b/host/src/ipc/ipc_server.hpp @@ -11,12 +11,10 @@ #include "coop/shared_memory.hpp" #include "input/input_source.hpp" -namespace coop -{ +namespace coop { // Plain (non-atomic) snapshot of the hook's back-channel for the overlay. -struct HookStatusView -{ +struct HookStatusView { bool attached = false; // XInput hooks installed in the game bool focus_spoof = false; // focus spoofing active std::uint32_t heartbeat = 0; // DLL liveness counter @@ -48,20 +46,18 @@ struct HookStatusView }; // Plain snapshot of the Present-hook video channel for the Video mirror panel. -struct VideoShareView -{ - std::uint32_t generation = 0; // bumps per shared frame; 0 = nothing shared yet - std::uint32_t width = 0; // shared texture dimensions / DXGI format +struct VideoShareView { + std::uint32_t generation = 0; // bumps per shared frame; 0 = nothing shared yet + std::uint32_t width = 0; // shared texture dimensions / DXGI format std::uint32_t height = 0; std::uint32_t format = 0; - std::uint64_t present_calls = 0; // cumulative Present() detours (diagnostic) - std::int64_t present_qpc = 0; // QPC stamp of the last published frame - std::uint64_t frames_dropped = 0; // cumulative captures skipped (mutex busy at present) + std::uint64_t present_calls = 0; // cumulative Present() detours (diagnostic) + std::int64_t present_qpc = 0; // QPC stamp of the last published frame + std::uint64_t frames_dropped = 0; // cumulative captures skipped (mutex busy at present) }; -class IpcServer -{ -public: +class IpcServer { + public: // Creates and initializes the section for `target_pid`. The injected hook // derives the same name from its own pid and opens it. bool start(unsigned long target_pid); @@ -98,8 +94,7 @@ public: void push_mkb(const MkbEvent& ev) { std::scoped_lock lock(mutex_); - if (block_ != nullptr) - { + if (block_ != nullptr) { push_mkb_event(block_->mkb, ev); } } @@ -109,8 +104,7 @@ public: void set_cursor_clip_allowed(bool allowed) { std::scoped_lock lock(mutex_); - if (block_ != nullptr) - { + if (block_ != nullptr) { block_->control.allow_cursor_clip.store(allowed ? 1u : 0u, std::memory_order_release); } } @@ -120,8 +114,7 @@ public: template void drain_logs(F&& emit) { - if (log_ring_ != nullptr) - { + if (log_ring_ != nullptr) { log_ring_drain(*log_ring_, log_cursor_, emit); } } @@ -130,16 +123,10 @@ public: // shows color-coded in the Log window next to the hook's lines. No-op if not started. void host_log(std::uint32_t level, const char* text); - [[nodiscard]] bool running() const - { - return block_ != nullptr; - } - [[nodiscard]] unsigned long target_pid() const - { - return target_pid_; - } + [[nodiscard]] bool running() const { return block_ != nullptr; } + [[nodiscard]] unsigned long target_pid() const { return target_pid_; } -private: + private: void stop_locked(); // tear-down body shared by start()/stop(); caller holds mutex_ // Guards the mapping pointer (block_) and its accesses. The input worker thread @@ -152,9 +139,9 @@ private: SharedBlock* block_ = nullptr; unsigned long target_pid_ = 0; - SharedMemory log_shm_; // shared log ring (named coop_log_) + SharedMemory log_shm_; // shared log ring (named coop_log_) LogRing* log_ring_ = nullptr; - std::uint64_t log_cursor_ = 0; // consumer position into the log ring + std::uint64_t log_cursor_ = 0; // consumer position into the log ring }; } // namespace coop diff --git a/host/src/log_panel.cpp b/host/src/log_panel.cpp index 3667ae7..b10a9fb 100644 --- a/host/src/log_panel.cpp +++ b/host/src/log_panel.cpp @@ -8,13 +8,11 @@ #include "ui/app_chrome.hpp" #include "ui/text_match.hpp" -namespace coop -{ +namespace coop { void LogPanel::add_line(const LogRecord& rec) { - if (first_millis_ == 0) - { + if (first_millis_ == 0) { first_millis_ = rec.millis; } const double secs = static_cast(rec.millis - first_millis_) / 1000.0; @@ -22,8 +20,7 @@ void LogPanel::add_line(const LogRecord& rec) char buf[256]; std::snprintf(buf, sizeof(buf), "[%8.3f] %s", secs, rec.text); lines_.push_back({buf, rec.level}); - while (lines_.size() > kMaxLines) - { + while (lines_.size() > kMaxLines) { lines_.pop_front(); } } @@ -38,8 +35,7 @@ void LogPanel::draw() apply_panel_layout(Panel::Log); ImGui::Begin("Log"); - if (ImGui::Button("Clear")) - { + if (ImGui::Button("Clear")) { lines_.clear(); first_millis_ = 0; } @@ -50,17 +46,13 @@ void LogPanel::draw() ImGui::InputTextWithHint("##logfilter", "filter...", filter_, sizeof(filter_)); ImGui::Separator(); - if (ImGui::BeginChild("loglines", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) - { + if (ImGui::BeginChild("loglines", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) { const bool has_filter = filter_[0] != '\0'; - for (const Line& line : lines_) - { - if (has_filter && !contains_ci(line.text, filter_)) - { + for (const Line& line : lines_) { + if (has_filter && !contains_ci(line.text, filter_)) { continue; } - switch (line.level) - { + switch (line.level) { case LogLevel_Warn: ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", line.text.c_str()); // amber break; @@ -73,8 +65,7 @@ void LogPanel::draw() } } // Stick to the bottom while new lines arrive (unless the user scrolled up). - if (autoscroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) - { + if (autoscroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) { ImGui::SetScrollHereY(1.0f); } } diff --git a/host/src/log_panel.hpp b/host/src/log_panel.hpp index 7a18e03..876eca5 100644 --- a/host/src/log_panel.hpp +++ b/host/src/log_panel.hpp @@ -9,24 +9,21 @@ #include "coop/log_ring.hpp" -namespace coop -{ +namespace coop { class InjectionPanel; -class LogPanel -{ -public: +class LogPanel { + public: // Pull any new lines the hook emitted (call once per frame before draw()). void pull(InjectionPanel& injection); void draw(); -private: + private: void add_line(const LogRecord& rec); - struct Line - { + struct Line { std::string text; std::uint32_t level; // LogLevel, for colouring }; diff --git a/host/src/main.cpp b/host/src/main.cpp index 4d4cf20..6a015f8 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -38,8 +38,7 @@ #include "util/utf8.hpp" #include "vk_layer_setup.hpp" -namespace -{ +namespace { // Timestamped screenshot path next to the exe (e.g. coop_shot_20260622_143501.png). std::wstring screenshot_path() @@ -47,8 +46,8 @@ std::wstring screenshot_path() SYSTEMTIME st{}; GetLocalTime(&st); wchar_t name[64]; - swprintf(name, static_cast(std::size(name)), L"coop_shot_%04u%02u%02u_%02u%02u%02u.png", st.wYear, - st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + swprintf(name, static_cast(std::size(name)), L"coop_shot_%04u%02u%02u_%02u%02u%02u.png", st.wYear, st.wMonth, + st.wDay, st.wHour, st.wMinute, st.wSecond); return coop::exe_directory() + name; } @@ -57,12 +56,11 @@ std::string screenshot_basename(const std::wstring& path) { const std::size_t slash = path.find_last_of(L"\\/"); const std::wstring file = slash == std::wstring::npos ? path : path.substr(slash + 1); - if (file.empty()) - { + if (file.empty()) { return {}; } - const int n = WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast(file.size()), nullptr, 0, - nullptr, nullptr); + const int n = + WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast(file.size()), nullptr, 0, nullptr, nullptr); std::string out(static_cast(n), '\0'); WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast(file.size()), out.data(), n, nullptr, nullptr); return out; @@ -74,16 +72,15 @@ std::string screenshot_basename(const std::wstring& path) void draw_screenshot_toast(double seconds_since, const std::string& name) { const float fade = 1.0f - static_cast(seconds_since) / 2.5f; - if (fade <= 0.0f || name.empty()) - { + if (fade <= 0.0f || name.empty()) { return; } const ImGuiViewport* vp = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + 12.0f, vp->WorkPos.y + vp->WorkSize.y - 44.0f)); ImGui::SetNextWindowBgAlpha(0.45f * fade); - const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::Begin("##shot_toast", nullptr, flags); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 1.0f, 0.6f, fade)); ImGui::Text("Saved screenshot: %s", name.c_str()); @@ -103,98 +100,84 @@ std::string apply_test_command(const std::string& cmd, coop::UiState& ui, coop:: { std::istringstream is(cmd); std::string t; - while (is >> t) - { + while (is >> t) { tok.push_back(t); } } - if (tok.empty()) - { + if (tok.empty()) { return "empty"; } const std::string& v = tok[0]; auto arg = [&](std::size_t i) -> std::string { return i < tok.size() ? tok[i] : std::string(); }; - auto num = [&](std::size_t i) -> unsigned { return static_cast(std::strtoul(arg(i).c_str(), nullptr, 10)); }; + auto num = [&](std::size_t i) -> unsigned { + return static_cast(std::strtoul(arg(i).c_str(), nullptr, 10)); + }; - if (v == "inject") - { + if (v == "inject") { const unsigned long pid = injection.dev_inject_by_name(widen(arg(1))); return pid != 0 ? ("ok pid " + std::to_string(pid)) : "fail no-process-or-inject-failed"; } - if (v == "audio") - { + if (v == "audio") { const bool on = arg(1) == "on"; - if (on) - { + if (on) { audio.dev_set_pid(injection.target_pid()); } audio.dev_set_enabled(on); return "ok"; } - if (v == "video") - { + if (v == "video") { // Install/remove the hooked video subsystem (Present/GL/D3D9/Vulkan capture hooks). injection.request_video(arg(1) == "on"); return "ok"; } - if (v == "debug") - { + if (v == "debug") { ui.debug_details = (arg(1) == "on"); return "ok"; } - if (v == "autoattach") - { + if (v == "autoattach") { injection.dev_set_auto_reattach(arg(1) == "on"); return "ok"; } - if (v == "remeasure") - { + if (v == "remeasure") { audio.dev_request_op(num(1), coop::AudioRingOp_Remeasure, 0, 0, 0, 0); return "ok"; } - if (v == "override") - { + if (v == "override") { const std::uint32_t tag = arg(5) == "float" ? static_cast(WAVE_FORMAT_IEEE_FLOAT) : static_cast(WAVE_FORMAT_PCM); audio.dev_request_op(num(1), coop::AudioRingOp_Override, num(2), num(3), num(4), tag); return "ok"; } - if (v == "screenshot") - { + if (v == "screenshot") { const std::wstring p = screenshot_path(); window.request_screenshot(p); return "ok"; } - if (v == "uisize") - { + if (v == "uisize") { // Force a reference layout size so the UI-fit check is monitor-independent. coop::set_layout_reference(static_cast(num(1)), static_cast(num(2))); return "ok"; } - if (v == "uifit") - { + if (v == "uifit") { // Report any panel whose content overflowed its assigned size last frame. char buf[256]; coop::panel_fit_report(buf, sizeof(buf)); return buf; } - if (v == "quit") - { + if (v == "quit") { ui.request_quit = true; return "ok"; } - if (v == "status") - { + if (v == "status") { const coop::HookStatusView st = injection.hook_status(); const std::string reason = audio.dev_reason(); char buf[512]; std::snprintf(buf, sizeof(buf), "audio_running=%d source=%s rate=%u ch=%u state=%u streams=%u inj_pid=%lu inj_state=%d " "reason=%s", - audio.dev_running() ? 1 : 0, audio.dev_source().c_str(), audio.dev_rate(), - audio.dev_channels(), st.audio_streams[0].format_state, st.audio_streams_seen, - injection.target_pid(), static_cast(injection.target_state()), - reason.empty() ? "-" : reason.c_str()); + audio.dev_running() ? 1 : 0, audio.dev_source().c_str(), audio.dev_rate(), audio.dev_channels(), + st.audio_streams[0].format_state, st.audio_streams_seen, injection.target_pid(), + static_cast(injection.target_state()), reason.empty() ? "-" : reason.c_str()); return buf; } return "unknown-command"; @@ -209,8 +192,7 @@ std::string steam_manifest_path() const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH); std::string path(buffer, len); const std::size_t slash = path.find_last_of("\\/"); - if (slash != std::string::npos) - { + if (slash != std::string::npos) { path.resize(slash + 1); } return path + "steam_input_actions.vdf"; @@ -226,16 +208,15 @@ void draw_vk_too_late_banner() { const ImGuiViewport* vp = ImGui::GetMainViewport(); float w = vp->WorkSize.x - 40.0f; - if (w > 760.0f) - { + if (w > 760.0f) { w = 760.0f; } - ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + 16.0f), - ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + 16.0f), ImGuiCond_Always, + ImVec2(0.5f, 0.0f)); ImGui::SetNextWindowSize(ImVec2(w, 0.0f)); - const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize; + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing + | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.28f, 0.03f, 0.03f, 0.92f)); ImGui::Begin("##vk_too_late", nullptr, flags); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.45f, 1.0f)); @@ -255,15 +236,14 @@ void draw_vk_too_late_banner() void draw_overlay_hidden_hint(double seconds_hidden) { const float fade = 1.0f - static_cast(seconds_hidden) / 4.0f; - if (fade <= 0.0f) - { + if (fade <= 0.0f) { return; // fully faded -> truly clean window for RPT capture } ImGui::SetNextWindowPos(ImVec2(12.0f, 12.0f)); ImGui::SetNextWindowBgAlpha(0.35f * fade); - const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::Begin("##overlay_hint", nullptr, flags); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, fade)); ImGui::TextUnformatted("F1: show overlay"); @@ -281,11 +261,9 @@ bool wait_for_hooked_frame(coop::D3D11Window& window, coop::InjectionPanel& inje QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&start); constexpr double kTimeoutMs = 200.0; // present anyway if the game stalls / is paused - for (;;) - { + for (;;) { const std::uint32_t gen = injection.video_share().generation; - if (gen != last_gen) - { + if (gen != last_gen) { last_gen = gen; return true; } @@ -293,13 +271,11 @@ bool wait_for_hooked_frame(coop::D3D11Window& window, coop::InjectionPanel& inje QueryPerformanceCounter(&now); const double elapsed = static_cast(now.QuadPart - start.QuadPart) * 1000.0 / static_cast(freq.QuadPart); - if (elapsed >= kTimeoutMs) - { + if (elapsed >= kTimeoutMs) { last_gen = gen; return true; } - if (!window.pump_messages()) - { + if (!window.pump_messages()) { return false; // WM_QUIT } Sleep(1); // yield ~1 ms (timeBeginPeriod(1) keeps this granular) instead of busy-spinning @@ -313,8 +289,7 @@ int run() coop::cleanup_stale_vk_layer(); coop::D3D11Window window; - if (!window.create(L"CoopAllTheThings")) - { + if (!window.create(L"CoopAllTheThings")) { MessageBoxW(nullptr, L"Failed to create the D3D11 window.", L"CoopAllTheThings", MB_ICONERROR); return 1; } @@ -324,8 +299,7 @@ int run() // Declaring ui first means it's destroyed AFTER imgui, so that final save never reads freed state. coop::UiState ui; coop::ImGuiLayer imgui; - if (!imgui.init(window.hwnd(), window.device(), window.context())) - { + if (!imgui.init(window.hwnd(), window.device(), window.context())) { MessageBoxW(nullptr, L"Failed to initialize ImGui.", L"CoopAllTheThings", MB_ICONERROR); return 1; } @@ -335,8 +309,7 @@ int run() coop::AudioPanel audio; coop::CapturePanel capture; coop::LogPanel log; - if (!capture.init(window.device())) - { + if (!capture.init(window.device())) { MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR); return 1; } @@ -376,22 +349,18 @@ int run() // Frame-sync: the hook generation we last presented (so we wait for the next one). std::uint32_t last_synced_gen = 0; - while (window.pump_messages()) - { + while (window.pump_messages()) { // Rescale the overlay if the window changed DPI (moved monitors, or the display scale changed). // pump_messages() latches the new DPI; apply it here, outside any in-progress ImGui frame. - if (unsigned new_dpi = 0; window.take_dpi_change(new_dpi)) - { + if (unsigned new_dpi = 0; window.take_dpi_change(new_dpi)) { imgui.set_dpi(new_dpi); } // When the operator enabled "Sync flip to game frames" (Hooked source), pace the // whole iteration to the game: wait for the next published frame before rendering, // then present without vsync so the flip lands in lockstep with the game. - if (capture.frame_sync_active()) - { - if (!wait_for_hooked_frame(window, injection, last_synced_gen)) - { + if (capture.frame_sync_active()) { + if (!wait_for_hooked_frame(window, injection, last_synced_gen)) { break; } } @@ -401,12 +370,9 @@ int run() const coop::InputSnapshot input_snapshot = input_worker.snapshot(); #ifdef COOP_WITH_STEAM input_worker.set_want_steam(controllers.steam_input_requested()); - if (input_worker.steam_failed()) - { + if (input_worker.steam_failed()) { controllers.on_steam_init_failed(); // resets the toggle; worker falls back to XInput - } - else - { + } else { controllers.set_steam_active(input_snapshot.steam_active); } #endif @@ -422,58 +388,45 @@ int run() log.pull(injection); // drain hook log lines even while the Log window is hidden #ifdef COOP_TEST_HARNESS - if (std::string tcmd = harness.poll_command(); !tcmd.empty()) - { + if (std::string tcmd = harness.poll_command(); !tcmd.empty()) { harness.write_response(apply_test_command(tcmd, ui, injection, audio, window)); } #endif - if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) - { + if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) { show_overlay = !show_overlay; - if (!show_overlay) - { + if (!show_overlay) { overlay_hidden_at = ImGui::GetTime(); } } - if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) - { + if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) { injection.toggle_cursor_release(); // free/clip the operator's mouse for clipping games } - if (ImGui::IsKeyPressed(ImGuiKey_F10, false)) - { + if (ImGui::IsKeyPressed(ImGuiKey_F10, false)) { window.request_screenshot(screenshot_path()); // captured at Present, overlay included } coop::reset_panel_fit(); // panels record their overflow as they draw (UI-fit check) coop::set_layout_debug(ui.debug_details); // center split adapts to the debug verbosity - if (show_overlay) - { + if (show_overlay) { coop::draw_main_menu_bar(ui, stats); - if (ui.show_controllers) - { + if (ui.show_controllers) { controllers.draw(input_snapshot, injection.hook_status(), ui.debug_details); } - if (ui.show_injection) - { + if (ui.show_injection) { injection.draw(ui.debug_details); } - if (ui.show_audio) - { + if (ui.show_audio) { audio.draw_ui(injection.hook_status(), ui.debug_details); } - if (ui.show_video) - { + if (ui.show_video) { capture.draw_ui(stats); } - if (ui.show_log) - { + if (ui.show_log) { log.draw(); } draw_screenshot_toast(ImGui::GetTime() - last_shot_at, last_shot_name); - } - else - { + } else { draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at); } if (injection.hook_status().vk_too_late) // Vulkan game injected too late -> relaunch prompt @@ -510,8 +463,7 @@ int run() // A host-side TDR / driver reset / GPU hang surfaces as a lost device on Present. We don't // attempt to recreate the device (it would have to re-init ImGui + the capture pipeline); // surface it and stop cleanly rather than spin forever rendering nothing. - if (window.device_lost()) - { + if (window.device_lost()) { wchar_t msg[320]; swprintf_s(msg, L"The graphics device was lost (0x%08lX) -- a driver reset, GPU hang, or TDR on " @@ -523,8 +475,7 @@ int run() // render_frame saves a pending F10 screenshot just before Present; pick up the // result here so next frame shows the confirmation toast (kept out of the shot). - if (std::wstring shot = window.take_screenshot_result(); !shot.empty()) - { + if (std::wstring shot = window.take_screenshot_result(); !shot.empty()) { last_shot_at = ImGui::GetTime(); last_shot_name = screenshot_basename(shot); } diff --git a/host/src/test_harness.cpp b/host/src/test_harness.cpp index 32f5ead..8fb6a2c 100644 --- a/host/src/test_harness.cpp +++ b/host/src/test_harness.cpp @@ -6,10 +6,8 @@ #include -namespace coop -{ -namespace -{ +namespace coop { +namespace { std::wstring temp_file(const wchar_t* name) { wchar_t dir[MAX_PATH] = {}; @@ -29,8 +27,7 @@ void TestHarness::init() std::string TestHarness::poll_command() { std::ifstream f(cmd_path_.c_str()); // MSVC accepts a wide path - if (!f) - { + if (!f) { return {}; } std::string line; diff --git a/host/src/test_harness.hpp b/host/src/test_harness.hpp index af5ec52..8db97dc 100644 --- a/host/src/test_harness.hpp +++ b/host/src/test_harness.hpp @@ -12,12 +12,10 @@ #include -namespace coop -{ +namespace coop { -class TestHarness -{ -public: +class TestHarness { + public: #ifdef COOP_TEST_HARNESS void init(); // resolve the %TEMP% file paths and clear any stale command // Main thread: returns the next pending command line (acking by deleting the cmd @@ -26,16 +24,13 @@ public: // Main thread: write the response for the command just handled. void write_response(const std::string& resp); -private: + private: std::wstring cmd_path_; std::wstring resp_path_; #else // No-op shims so call sites don't need their own #ifdef. void init() {} - std::string poll_command() - { - return {}; - } + std::string poll_command() { return {}; } void write_response(const std::string&) {} #endif }; diff --git a/host/src/ui/app_chrome.cpp b/host/src/ui/app_chrome.cpp index 5cb6f18..c64456e 100644 --- a/host/src/ui/app_chrome.cpp +++ b/host/src/ui/app_chrome.cpp @@ -8,11 +8,9 @@ #include "imgui.h" #include "imgui_internal.h" // ImGuiSettingsHandler / AddSettingsHandler (custom .ini section) -namespace coop -{ +namespace coop { -namespace -{ +namespace { // --- Custom .ini persistence for the UI switches --------------------------- // We piggy-back on ImGui's .ini so the "Debug details" verbosity survives restarts // without inventing a separate settings file. The section looks like: @@ -33,8 +31,7 @@ void ui_settings_read_line(ImGuiContext*, ImGuiSettingsHandler*, void* entry, co auto* ui = static_cast(entry); // Manual parse (avoids the sscanf CRT-secure deprecation for a single int key). constexpr char kKey[] = "DebugDetails="; - if (std::strncmp(line, kKey, sizeof(kKey) - 1) == 0) - { + if (std::strncmp(line, kKey, sizeof(kKey) - 1) == 0) { ui->debug_details = std::atoi(line + sizeof(kKey) - 1) != 0; } } @@ -66,8 +63,7 @@ float g_ref_h = 0.0f; bool g_layout_debug = false; // Per-frame panel-overflow registry (UI-fit instrumentation). -struct PanelFit -{ +struct PanelFit { char name[24]; float over_x; float over_y; @@ -79,8 +75,7 @@ int g_fit_count = 0; void register_ui_settings(UiState& ui) { // Idempotent: don't stack a second handler if this is somehow called twice. - if (ImGui::FindSettingsHandler(kUiSettingsType) != nullptr) - { + if (ImGui::FindSettingsHandler(kUiSettingsType) != nullptr) { return; } ImGuiSettingsHandler handler; @@ -106,8 +101,7 @@ void set_layout_persisted(bool had_persisted_layout) void apply_layout_end_frame() { g_layout_reset = false; - if (g_startup_force > 0) - { + if (g_startup_force > 0) { --g_startup_force; } } @@ -135,8 +129,7 @@ void record_panel_fit(const char* name) // so > 0 on either axis means content is cut off at the assigned size. const float ox = ImGui::GetScrollMaxX(); const float oy = ImGui::GetScrollMaxY(); - if (g_fit_count >= static_cast(sizeof(g_fits) / sizeof(g_fits[0]))) - { + if (g_fit_count >= static_cast(sizeof(g_fits) / sizeof(g_fits[0]))) { return; } PanelFit& f = g_fits[g_fit_count++]; @@ -148,17 +141,14 @@ void record_panel_fit(const char* name) bool panel_fit_overflow(float* worst_x, float* worst_y) { float mx = 0.0f, my = 0.0f; - for (int i = 0; i < g_fit_count; ++i) - { + for (int i = 0; i < g_fit_count; ++i) { mx = std::max(mx, g_fits[i].over_x); my = std::max(my, g_fits[i].over_y); } - if (worst_x != nullptr) - { + if (worst_x != nullptr) { *worst_x = mx; } - if (worst_y != nullptr) - { + if (worst_y != nullptr) { *worst_y = my; } return mx > 0.5f || my > 0.5f; @@ -166,24 +156,20 @@ bool panel_fit_overflow(float* worst_x, float* worst_y) void panel_fit_report(char* buf, int cap) { - if (buf == nullptr || cap <= 0) - { + if (buf == nullptr || cap <= 0) { return; } int n = 0; bool any = false; - for (int i = 0; i < g_fit_count && n < cap - 1; ++i) - { - if (g_fits[i].over_x <= 0.5f && g_fits[i].over_y <= 0.5f) - { + for (int i = 0; i < g_fit_count && n < cap - 1; ++i) { + if (g_fits[i].over_x <= 0.5f && g_fits[i].over_y <= 0.5f) { continue; } any = true; - n += std::snprintf(buf + n, static_cast(cap - n), "%s%s:%.0f,%.0f", n > 0 ? " " : "", - g_fits[i].name, g_fits[i].over_x, g_fits[i].over_y); + n += std::snprintf(buf + n, static_cast(cap - n), "%s%s:%.0f,%.0f", n > 0 ? " " : "", g_fits[i].name, + g_fits[i].over_x, g_fits[i].over_y); } - if (!any) - { + if (!any) { std::snprintf(buf, static_cast(cap), "fit"); } } @@ -227,8 +213,7 @@ void apply_panel_layout(Panel panel) const float audio_h = stack_avail * audio_frac; ImVec2 pos, size; - switch (panel) - { + switch (panel) { case Panel::Injection: pos = ImVec2(left_x, top); size = ImVec2(left_w, full_h); @@ -255,8 +240,7 @@ void apply_panel_layout(Panel panel) // install with no saved layout to restore. Otherwise FirstUseEver lets ImGui's // restored .ini positions stand (and still seeds any brand-new panel). A forced // reference size (UI-fit check) also forces, so the assigned sizes are exact. - const bool force = - g_layout_reset || g_ref_w > 0.0f || (!g_had_persisted_layout && g_startup_force > 0); + const bool force = g_layout_reset || g_ref_w > 0.0f || (!g_had_persisted_layout && g_startup_force > 0); const ImGuiCond cond = force ? ImGuiCond_Always : ImGuiCond_FirstUseEver; ImGui::SetNextWindowPos(pos, cond); ImGui::SetNextWindowSize(size, cond); @@ -265,44 +249,37 @@ void apply_panel_layout(Panel panel) float draw_main_menu_bar(UiState& ui, const FrameStats& stats) { float height = 0.0f; - if (!ImGui::BeginMainMenuBar()) - { + if (!ImGui::BeginMainMenuBar()) { return height; } ImGui::TextUnformatted("CoopAllTheThings"); ImGui::Separator(); - if (ImGui::BeginMenu("File")) - { - if (ImGui::MenuItem("Exit", "Alt+F4")) - { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Exit", "Alt+F4")) { ui.request_quit = true; // the main loop sees this and stops } ImGui::EndMenu(); } - if (ImGui::BeginMenu("View")) - { + if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Controllers", nullptr, &ui.show_controllers); ImGui::MenuItem("Injection", nullptr, &ui.show_injection); ImGui::MenuItem("Video mirror", nullptr, &ui.show_video); ImGui::MenuItem("Audio mirror", nullptr, &ui.show_audio); ImGui::MenuItem("Log", nullptr, &ui.show_log); ImGui::Separator(); - if (ImGui::MenuItem("Debug details", nullptr, &ui.debug_details)) - { + if (ImGui::MenuItem("Debug details", nullptr, &ui.debug_details)) { ImGui::MarkIniSettingsDirty(); // persist the new verbosity to coop_layout.ini } - if (ImGui::MenuItem("Reset layout")) - { + if (ImGui::MenuItem("Reset layout")) { request_layout_reset(); } ImGui::EndMenu(); } - if (ImGui::BeginMenu("Help")) - { + if (ImGui::BeginMenu("Help")) { ImGui::TextDisabled("F1 hide/show this overlay"); ImGui::TextDisabled("F2 release/clip the operator cursor"); ImGui::TextDisabled("F10 save a screenshot (PNG, next to the exe)"); @@ -318,16 +295,14 @@ float draw_main_menu_bar(UiState& ui, const FrameStats& stats) char perf[96]; // Fixed field widths so the readout doesn't jitter/blur as values cross digit // thresholds (e.g. 99 -> 100) each frame. - std::snprintf(perf, sizeof(perf), "%4.0f FPS %6.2f ms (%6.2f-%6.2f)", stats.fps(), stats.avg_ms(), - stats.min_ms(), stats.max_ms()); + std::snprintf(perf, sizeof(perf), "%4.0f FPS %6.2f ms (%6.2f-%6.2f)", stats.fps(), stats.avg_ms(), stats.min_ms(), + stats.max_ms()); const float text_w = ImGui::CalcTextSize(perf).x; ImGui::SameLine(ImGui::GetWindowWidth() - text_w - ImGui::GetStyle().FramePadding.x * 2.0f); if (stats.max_ms() > 25.0f) // ~sub-40 FPS spike in the window { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", perf); - } - else - { + } else { ImGui::TextUnformatted(perf); } diff --git a/host/src/ui/app_chrome.hpp b/host/src/ui/app_chrome.hpp index c438b9d..17be878 100644 --- a/host/src/ui/app_chrome.hpp +++ b/host/src/ui/app_chrome.hpp @@ -6,14 +6,12 @@ #include -namespace coop -{ +namespace coop { // Visibility + verbosity shared by all panels. Panels read `debug_details` to // gate verbose diagnostics; the main loop reads the per-panel flags to decide // what to draw. -struct UiState -{ +struct UiState { bool show_controllers = true; bool show_injection = true; bool show_video = true; @@ -31,8 +29,7 @@ struct UiState void register_ui_settings(UiState& ui); // The overlay panels, for the shared default layout below. -enum class Panel -{ +enum class Panel { Injection, // left column, full height (room for hook diagnostics) Controllers, // center column, top Video, // center column, below Controllers @@ -89,26 +86,22 @@ void panel_fit_report(char* buf, int cap); // Rolling frame-timing over a ~1 s window, recomputed each window so the status // bar can show a stable FPS plus the min/max frame time (jitter) underneath it. -class FrameStats -{ -public: +class FrameStats { + public: // Number of frame samples kept for the graphs (~2 s at 120 FPS). static constexpr int kHistory = 240; void tick(float dt_ms) { - if (dt_ms < cur_min_) - { + if (dt_ms < cur_min_) { cur_min_ = dt_ms; } - if (dt_ms > cur_max_) - { + if (dt_ms > cur_max_) { cur_max_ = dt_ms; } accum_ms_ += dt_ms; ++frames_; - if (accum_ms_ >= 1000.0f && frames_ > 0) - { + if (accum_ms_ >= 1000.0f && frames_ > 0) { avg_ms_ = accum_ms_ / static_cast(frames_); min_ms_ = cur_min_; max_ms_ = cur_max_; @@ -120,43 +113,26 @@ public: history_[hist_pos_] = dt_ms; hist_pos_ = (hist_pos_ + 1) % kHistory; - if (hist_count_ < kHistory) - { + if (hist_count_ < kHistory) { ++hist_count_; } } // --- 1 s windowed aggregates (stable readout for the menu bar) --------- - [[nodiscard]] float avg_ms() const - { - return avg_ms_; - } - [[nodiscard]] float min_ms() const - { - return min_ms_; - } - [[nodiscard]] float max_ms() const - { - return max_ms_; - } - [[nodiscard]] float fps() const - { - return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f; - } + [[nodiscard]] float avg_ms() const { return avg_ms_; } + [[nodiscard]] float min_ms() const { return min_ms_; } + [[nodiscard]] float max_ms() const { return max_ms_; } + [[nodiscard]] float fps() const { return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f; } // --- Sample history (for graphs) --------------------------------------- - [[nodiscard]] int history_size() const - { - return hist_count_; - } + [[nodiscard]] int history_size() const { return hist_count_; } // Copy the frame-time samples (ms) into `out` oldest-to-newest; `out` must // hold at least kHistory floats. Returns the number written. int copy_frame_ms(float* out) const { const int start = (hist_pos_ - hist_count_ + kHistory * 2) % kHistory; - for (int i = 0; i < hist_count_; ++i) - { + for (int i = 0; i < hist_count_; ++i) { out[i] = history_[(start + i) % kHistory]; } return hist_count_; @@ -165,14 +141,12 @@ public: // min / max / mean over the whole retained history (order-independent). void history_stats(float& min_ms, float& max_ms, float& avg_ms) const { - if (hist_count_ == 0) - { + if (hist_count_ == 0) { min_ms = max_ms = avg_ms = 0.0f; return; } float mn = 1.0e9f, mx = 0.0f, sum = 0.0f; - for (int i = 0; i < hist_count_; ++i) - { + for (int i = 0; i < hist_count_; ++i) { const float v = history_[i]; mn = std::min(mn, v); mx = std::max(mx, v); @@ -183,7 +157,7 @@ public: avg_ms = sum / static_cast(hist_count_); } -private: + private: float accum_ms_ = 0.0f; int frames_ = 0; float cur_min_ = 1.0e9f; diff --git a/host/src/ui/text_match.hpp b/host/src/ui/text_match.hpp index 2284562..7f43746 100644 --- a/host/src/ui/text_match.hpp +++ b/host/src/ui/text_match.hpp @@ -6,13 +6,11 @@ #include #include -namespace coop -{ +namespace coop { inline std::string ascii_lower(std::string s) { - for (char& c : s) - { + for (char& c : s) { c = static_cast(std::tolower(static_cast(c))); } return s; @@ -21,8 +19,7 @@ inline std::string ascii_lower(std::string s) // True when `needle` is empty or a case-insensitive substring of `haystack`. inline bool contains_ci(const std::string& haystack, const char* needle) { - if (needle == nullptr || needle[0] == '\0') - { + if (needle == nullptr || needle[0] == '\0') { return true; } return ascii_lower(haystack).find(ascii_lower(needle)) != std::string::npos; diff --git a/host/src/util/utf8.hpp b/host/src/util/utf8.hpp index ce310f0..849e6ac 100644 --- a/host/src/util/utf8.hpp +++ b/host/src/util/utf8.hpp @@ -6,14 +6,12 @@ #include -namespace coop -{ +namespace coop { // UTF-16 -> UTF-8. inline std::string narrow(const std::wstring& w) { - if (w.empty()) - { + if (w.empty()) { return {}; } const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); @@ -25,8 +23,7 @@ inline std::string narrow(const std::wstring& w) // UTF-8 -> UTF-16. inline std::wstring widen(const std::string& s) { - if (s.empty()) - { + if (s.empty()) { return {}; } const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); diff --git a/host/src/vk_layer_setup.cpp b/host/src/vk_layer_setup.cpp index bf9491f..6f0d5a3 100644 --- a/host/src/vk_layer_setup.cpp +++ b/host/src/vk_layer_setup.cpp @@ -5,10 +5,8 @@ #include "coop/tool_paths.hpp" #include "util/utf8.hpp" -namespace coop -{ -namespace -{ +namespace coop { +namespace { // The Vulkan loader's per-user implicit-layer registry list. Each value is a manifest path; its // DWORD data 0 = enabled. constexpr const wchar_t* kImplicitLayersKey = L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers"; @@ -30,14 +28,12 @@ bool register_vk_layer(const std::wstring& target_image) { // Write the scoping file (target image basename, UTF-8) the layer checks against its own image. const std::wstring sf = scoping_file(); - if (!sf.empty()) - { + if (!sf.empty()) { const std::size_t slash = target_image.find_last_of(L"\\/"); const std::wstring base = slash == std::wstring::npos ? target_image : target_image.substr(slash + 1); const std::string utf8 = narrow(base); HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (f != INVALID_HANDLE_VALUE) - { + if (f != INVALID_HANDLE_VALUE) { DWORD written = 0; WriteFile(f, utf8.data(), static_cast(utf8.size()), &written, nullptr); CloseHandle(f); @@ -45,15 +41,14 @@ bool register_vk_layer(const std::wstring& target_image) } HKEY key = nullptr; - if (RegCreateKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, - nullptr) != ERROR_SUCCESS) - { + if (RegCreateKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr) + != ERROR_SUCCESS) { return false; } const std::wstring mp = manifest_path(); DWORD enabled = 0; // 0 = enabled, per the loader's convention - const LONG r = RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast(&enabled), - sizeof(enabled)); + const LONG r = + RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast(&enabled), sizeof(enabled)); RegCloseKey(key); return r == ERROR_SUCCESS; } @@ -61,14 +56,12 @@ bool register_vk_layer(const std::wstring& target_image) void unregister_vk_layer() { HKEY key = nullptr; - if (RegOpenKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS) - { + if (RegOpenKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS) { RegDeleteValueW(key, manifest_path().c_str()); RegCloseKey(key); } const std::wstring sf = scoping_file(); - if (!sf.empty()) - { + if (!sf.empty()) { DeleteFileW(sf.c_str()); } } diff --git a/host/src/vk_layer_setup.hpp b/host/src/vk_layer_setup.hpp index 72768e8..c6d629d 100644 --- a/host/src/vk_layer_setup.hpp +++ b/host/src/vk_layer_setup.hpp @@ -10,8 +10,7 @@ #include -namespace coop -{ +namespace coop { // Register the implicit layer (HKCU) and scope it to `target_image` (the game's exe basename, // e.g. "game.exe"). Returns true on success. Idempotent. diff --git a/tests/audio_correlation_test.cpp b/tests/audio_correlation_test.cpp index 479c8d1..290f098 100644 --- a/tests/audio_correlation_test.cpp +++ b/tests/audio_correlation_test.cpp @@ -17,14 +17,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -36,8 +34,7 @@ constexpr double kPi = 3.14159265358979323846; double source(double t) { const double chirp = std::sin(2.0 * kPi * (300.0 * t + 140.0 * t * t)); - return 0.5 * std::sin(2.0 * kPi * 221.0 * t) + 0.28 * std::sin(2.0 * kPi * 437.0 * t + 0.6) + - 0.22 * chirp; + return 0.5 * std::sin(2.0 * kPi * 221.0 * t) + 0.28 * std::sin(2.0 * kPi * 437.0 * t + 0.6) + 0.22 * chirp; } // Sample s(t) at `rate` for `seconds`, starting at t0 (capture-latency skew), optionally adding @@ -48,8 +45,7 @@ std::vector capture(unsigned rate, double seconds, double t0, double nois std::vector out(n); std::mt19937 rng(seed); std::uniform_real_distribution jitter(-1.0f, 1.0f); - for (std::size_t i = 0; i < n; ++i) - { + for (std::size_t i = 0; i < n; ++i) { const double t = t0 + static_cast(i) / static_cast(rate); out[i] = static_cast(source(t)) + static_cast(noise) * jitter(rng); } @@ -64,15 +60,13 @@ double multi(double t, unsigned channel) { const double k = 1.0 + 0.37 * static_cast(channel); // distinct frequency scale per channel const double chirp = std::sin(2.0 * kPi * (300.0 * k * t + 140.0 * t * t)); - return 0.5 * std::sin(2.0 * kPi * 221.0 * k * t) + 0.28 * std::sin(2.0 * kPi * 437.0 * k * t + 0.6) + - 0.22 * chirp; + return 0.5 * std::sin(2.0 * kPi * 221.0 * k * t) + 0.28 * std::sin(2.0 * kPi * 437.0 * k * t + 0.6) + 0.22 * chirp; } double multi_mono(double t, unsigned channels) { double sum = 0.0; - for (unsigned c = 0; c < channels; ++c) - { + for (unsigned c = 0; c < channels; ++c) { sum += multi(t, c); } return sum / channels; @@ -84,20 +78,15 @@ std::vector encode(unsigned rate, unsigned channels, unsigned bits const unsigned bps = bits / 8; const std::size_t frames = static_cast(rate * seconds); std::vector out(frames * channels * bps); - for (std::size_t i = 0; i < frames; ++i) - { + for (std::size_t i = 0; i < frames; ++i) { const double t = static_cast(i) / rate; - for (unsigned c = 0; c < channels; ++c) - { + for (unsigned c = 0; c < channels; ++c) { const double s = multi(t, c); std::uint8_t* p = out.data() + (i * channels + c) * bps; - if (tag == coop::kWaveFormatFloat) - { + if (tag == coop::kWaveFormatFloat) { const float f = static_cast(s); std::memcpy(p, &f, 4); - } - else - { + } else { const std::int16_t v = static_cast(s * 30000.0); std::memcpy(p, &v, 2); } @@ -121,16 +110,14 @@ coop::ChunkedCapture make_chunks(unsigned rate, unsigned ch, unsigned bits, unsi std::mt19937 rng(123); std::uniform_int_distribution garbage(0, 255); std::size_t f = 0; - while (f < frames) - { + while (f < frames) { const unsigned count = static_cast(std::min(480, frames - f)); cap.counts.push_back(count); // The hook reads count*stride contiguous bytes: the count real frames first // (count*real_block bytes), then count*(stride-real_block) bytes of stale over-read. const std::uint8_t* src = clean.data() + f * real_block; cap.bytes.insert(cap.bytes.end(), src, src + static_cast(count) * real_block); - for (std::size_t p = 0; p < static_cast(count) * (stride - real_block); ++p) - { + for (std::size_t p = 0; p < static_cast(count) * (stride - real_block); ++p) { cap.bytes.push_back(static_cast(garbage(rng))); } f += count; @@ -140,11 +127,11 @@ coop::ChunkedCapture make_chunks(unsigned rate, unsigned ch, unsigned bits, unsi // One layout scenario: the hook bytes are at (true_*) and still being measured; the loopback is the // post-mix mono of the same audio at device_rate. Assert correlate_format recovers the full layout. -void test_layout(unsigned true_rate, unsigned true_ch, unsigned true_bits, unsigned true_tag, - unsigned device_rate, const char* label) +void test_layout(unsigned true_rate, unsigned true_ch, unsigned true_bits, unsigned true_tag, unsigned device_rate, + const char* label) { - std::printf("== layout: %s (%u Hz / %u ch / %u-bit %s -> device %u Hz) ==\n", label, true_rate, true_ch, - true_bits, true_tag == coop::kWaveFormatFloat ? "float" : "pcm", device_rate); + std::printf("== layout: %s (%u Hz / %u ch / %u-bit %s -> device %u Hz) ==\n", label, true_rate, true_ch, true_bits, + true_tag == coop::kWaveFormatFloat ? "float" : "pcm", device_rate); // Device block 32 (8ch float) is the largest stride; every test layout's real block is <= 32. const coop::ChunkedCapture hook = make_chunks(true_rate, true_ch, true_bits, true_tag, /*stride=*/32, 0.6); // Loopback: the post-mix mono of the same audio, at the device rate, started ~18 ms later + noise. @@ -152,16 +139,15 @@ void test_layout(unsigned true_rate, unsigned true_ch, unsigned true_bits, unsig std::vector loop(loop_frames); std::mt19937 rng(5); std::uniform_real_distribution j(-1.0f, 1.0f); - for (std::size_t m = 0; m < loop_frames; ++m) - { - loop[m] = static_cast(multi_mono(0.018 + static_cast(m) / device_rate, true_ch)) + - 0.02f * j(rng); + for (std::size_t m = 0; m < loop_frames; ++m) { + loop[m] = + static_cast(multi_mono(0.018 + static_cast(m) / device_rate, true_ch)) + 0.02f * j(rng); } const FormatCorrelation r = correlate_format(hook, loop, device_rate, standard_audio_rates(), standard_audio_layouts()); - std::printf(" picked %u Hz / %u ch / %u-bit %s score=%.3f runner_up=%.3f ok=%d\n", r.rate, r.channels, - r.bits, r.tag == coop::kWaveFormatFloat ? "float" : "pcm", r.score, r.runner_up, r.ok ? 1 : 0); + std::printf(" picked %u Hz / %u ch / %u-bit %s score=%.3f runner_up=%.3f ok=%d\n", r.rate, r.channels, r.bits, + r.tag == coop::kWaveFormatFloat ? "float" : "pcm", r.score, r.runner_up, r.ok ? 1 : 0); check(r.ok, "layout pick is confident"); check(r.rate == true_rate, "recovered the true rate"); check(r.channels == true_ch, "recovered the true channel count"); @@ -179,8 +165,7 @@ void test_case(unsigned true_rate, unsigned device_rate, const char* label) const std::vector loop = capture(device_rate, 0.55, 0.022, 0.02, 7); const RateCorrelation r = correlate_rate(hook, loop, device_rate, standard_audio_rates()); - std::printf(" picked %u Hz score=%.3f runner_up=%.3f ok=%d\n", r.rate, r.score, r.runner_up, - r.ok ? 1 : 0); + std::printf(" picked %u Hz score=%.3f runner_up=%.3f ok=%d\n", r.rate, r.score, r.runner_up, r.ok ? 1 : 0); check(r.rate == true_rate, "correlator picked the true rate"); check(r.ok, "pick is confident (clears threshold + beats runner-up)"); check(r.score > r.runner_up, "winner scores above the runner-up"); @@ -210,8 +195,8 @@ int main() std::vector stereo = {1.0f, 3.0f, 2.0f, 4.0f, -1.0f, 1.0f}; std::vector mono; correlate_detail::downmix(stereo.data(), 3, 2, mono); - check(mono.size() == 3 && std::fabs(mono[0] - 2.0f) < 1e-6 && std::fabs(mono[1] - 3.0f) < 1e-6 && - std::fabs(mono[2] - 0.0f) < 1e-6, + check(mono.size() == 3 && std::fabs(mono[0] - 2.0f) < 1e-6 && std::fabs(mono[1] - 3.0f) < 1e-6 + && std::fabs(mono[2] - 0.0f) < 1e-6, "stereo frames average to mono"); } @@ -222,8 +207,7 @@ int main() std::vector noise(static_cast(48000 * 0.5)); std::mt19937 rng(99); std::uniform_real_distribution d(-1.0f, 1.0f); - for (float& x : noise) - { + for (float& x : noise) { x = d(rng); } const RateCorrelation r = correlate_rate(hook, noise, 48000, standard_audio_rates()); @@ -231,8 +215,7 @@ int main() check(!r.ok, "unrelated loopback is not a confident match"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS audio_correlation_test\n"); return 0; } diff --git a/tests/audio_hook_test.cpp b/tests/audio_hook_test.cpp index ade65e3..269556a 100644 --- a/tests/audio_hook_test.cpp +++ b/tests/audio_hook_test.cpp @@ -34,16 +34,14 @@ using namespace coop; using coop::tone::ToneFormat; using coop::tone::ToneSource; -namespace -{ +namespace { int g_failures = 0; // Returns 1 (and logs) on failure, 0 on success -- so callers can sum a tally. int expect(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; return 1; @@ -63,10 +61,8 @@ bool ring_has_nonsilent(AudioRingHeader* ring) { std::vector buf(128 * 1024, 0); const std::uint32_t got = audio_ring_pop(*ring, buf.data(), static_cast(buf.size())); - for (std::uint32_t i = 0; i < got; ++i) - { - if (buf[i] != 0) - { + for (std::uint32_t i = 0; i < got; ++i) { + if (buf[i] != 0) { return true; } } @@ -85,15 +81,13 @@ void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* blo { char d[64]; reset_ring(ring, block); - if (!hook::install_audio_hooks(ipc, ring)) - { + if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP see-init (audio hooks unavailable)\n"); return; } ToneSource tone; - if (!tone.open(want)) - { + if (!tone.open(want)) { std::printf(" SKIP see-init %s (format unavailable here)\n", fmt_desc(want, d, sizeof(d))); hook::remove_audio_hooks(); return; @@ -103,8 +97,7 @@ void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* blo // Exact format publishes at registration; render briefly so capture fills the ring. const std::uint64_t silenced_before = hook::audio_frames_silenced(); const DWORD end = GetTickCount() + 300; - while (GetTickCount() < end) - { + while (GetTickCount() < end) { tone.render_step(50); } @@ -117,8 +110,7 @@ void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* blo fail += expect(s.sample_rate == f.rate, "see-init: HookStatus rate == exact rate"); fail += expect(s.format_state == AudioFormat_Exact, "see-init: provenance == Exact"); fail += expect(ring_has_nonsilent(ring), "see-init: non-silent audio captured"); - fail += expect(hook::audio_frames_silenced() > silenced_before, - "see-init: local playback muted (no echo)"); + fail += expect(hook::audio_frames_silenced() > silenced_before, "see-init: local playback muted (no echo)"); std::printf(" %s see-init %s -> ring %uHz/%uch/%ubit state=%u\n", fail == 0 ? "PASS" : "FAIL", fmt_desc(f, d, sizeof(d)), ring->sample_rate, ring->channels, ring->bits, s.format_state); @@ -137,8 +129,7 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, ToneSource tone; ToneFormat want; want.rate = rate; // channels/bits resolve to the device's - if (!tone.open(want)) - { + if (!tone.open(want)) { std::printf(" SKIP guess %u Hz (format unavailable here)\n", rate); return; } @@ -147,14 +138,12 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, // Let the stream reach steady state before attaching, like a game already running when // we inject (the real case) -- not a stream we caught at its first buffer. const DWORD warm = GetTickCount() + 300; - while (GetTickCount() < warm) - { + while (GetTickCount() < warm) { tone.render_step(30); } // Hooks install *after* the client exists -> the lazy-discovery (guess) path. - if (!hook::install_audio_hooks(ipc, ring)) - { + if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP guess (audio hooks unavailable)\n"); tone.close(); return; @@ -163,16 +152,14 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, // Render while driving republish (the DLL's worker does this each tick) until the // measured rate is published, then render a bit more so capture fills the ring. const DWORD measure_deadline = GetTickCount() + 2000; - while (GetTickCount() < measure_deadline && !audio_ring_format_ready(*ring)) - { + while (GetTickCount() < measure_deadline && !audio_ring_format_ready(*ring)) { tone.render_step(30); hook::republish_audio_format(); } // Format is published; from here the hook must capture AND mute (the no-echo path). const std::uint64_t silenced_before = hook::audio_frames_silenced(); const DWORD cap_end = GetTickCount() + 200; - while (GetTickCount() < cap_end) - { + while (GetTickCount() < cap_end) { tone.render_step(30); } @@ -188,8 +175,8 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, // game plays locally AND the mirror re-renders it, slightly delayed = a metallic double. fail += expect(hook::audio_frames_silenced() > silenced_before, "guess: local playback muted (no echo) -- the Brotato double-audio bug"); - std::printf(" %s guess %u Hz (device %uch/%ubit) -> measured %uHz state=%u\n", fail == 0 ? "PASS" : "FAIL", - rate, f.channels, f.bits, ring->sample_rate, s.format_state); + std::printf(" %s guess %u Hz (device %uch/%ubit) -> measured %uHz state=%u\n", fail == 0 ? "PASS" : "FAIL", rate, + f.channels, f.bits, ring->sample_rate, s.format_state); tone.close(); hook::remove_audio_hooks(); @@ -200,26 +187,23 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, // VirtualQuery clamp must stop the copy reading past the source buffer. We can't assert a // "correct" format here (it's fundamentally undetectable); we assert the hook survives and // doesn't read absurd amounts, i.e. the unit test completes without an access violation. -void test_guess_mismatch_safe(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, - const ToneFormat& want, const char* label) +void test_guess_mismatch_safe(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block, const ToneFormat& want, + const char* label) { char d[64]; reset_ring(ring, block); ToneSource tone; - if (!tone.open(want)) - { + if (!tone.open(want)) { std::printf(" SKIP guess-mismatch %s (%s unavailable here)\n", label, fmt_desc(want, d, sizeof(d))); return; } const ToneFormat& f = tone.format(); const DWORD warm = GetTickCount() + 300; // steady state before attaching - while (GetTickCount() < warm) - { + while (GetTickCount() < warm) { tone.render_step(30); } - if (!hook::install_audio_hooks(ipc, ring)) - { + if (!hook::install_audio_hooks(ipc, ring)) { std::printf(" SKIP guess-mismatch (audio hooks unavailable)\n"); tone.close(); return; @@ -228,8 +212,7 @@ void test_guess_mismatch_safe(hook::IpcClient& ipc, AudioRingHeader* ring, Share // Render and capture through the guessed (too-large) block. The clamp must keep this // from over-reading; reaching the end of the loop is the pass (no AV). const DWORD end = GetTickCount() + 600; - while (GetTickCount() < end) - { + while (GetTickCount() < end) { tone.render_step(30); hook::republish_audio_format(); } @@ -244,8 +227,7 @@ void test_guess_mismatch_safe(hook::IpcClient& ipc, AudioRingHeader* ring, Share int main() { - if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) - { + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::printf("FAIL: CoInitializeEx\n"); return 1; } @@ -253,8 +235,7 @@ int main() // Host side: the IPC SharedBlock (named by our pid) the hook's IpcClient connects to, // plus one producer ring with capture enabled. SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -276,8 +257,7 @@ int main() ToneFormat dev; { ToneSource probe; - if (!probe.open(ToneFormat{})) - { + if (!probe.open(ToneFormat{})) { std::printf("SKIP: no default render endpoint (no audio device?)\n"); CoUninitialize(); return 0; @@ -297,8 +277,7 @@ int main() {44100, 1, 16, false}, // mono PCM {48000, 6, 32, true}, // 5.1 float }; - for (const ToneFormat& f : see_init) - { + for (const ToneFormat& f : see_init) { test_see_init(ipc, ring, block, f); } @@ -306,8 +285,7 @@ int main() // rate differs -- the hook measures + corrects it to the true rate (the Brotato/Godot // case). Rendered at the device's channel/bit layout, so the bytes/frame match. std::printf("== GUESS, byte-compatible (pre-existing client -> measure the true rate) ==\n"); - for (unsigned rate : {44100u, 48000u, 96000u}) - { + for (unsigned rate : {44100u, 48000u, 96000u}) { test_guess(ipc, ring, block, rate); } @@ -316,15 +294,13 @@ int main() // a documented limitation), but the VirtualQuery clamp must keep the capture safe rather // than over-reading the source buffer. std::printf("== GUESS, byte-incompatible (channels/bits differ -> capture must stay safe) ==\n"); - if (dev.channels >= 2) - { + if (dev.channels >= 2) { test_guess_mismatch_safe(ipc, ring, block, {dev.rate, 1, dev.bits, dev.is_float}, "mono"); } { const unsigned alt_bits = (dev.bits == 32) ? 16u : 32u; const bool alt_float = (alt_bits == 32); - test_guess_mismatch_safe(ipc, ring, block, {dev.rate, dev.channels, alt_bits, alt_float}, - "alt bit depth"); + test_guess_mismatch_safe(ipc, ring, block, {dev.rate, dev.channels, alt_bits, alt_float}, "alt bit depth"); } CoUninitialize(); diff --git a/tests/audio_loopback_test.cpp b/tests/audio_loopback_test.cpp index ad20c9f..3c18b0c 100644 --- a/tests/audio_loopback_test.cpp +++ b/tests/audio_loopback_test.cpp @@ -22,8 +22,7 @@ using namespace coop; -namespace -{ +namespace { // Directory of this test executable (coop_tone.exe is built alongside it). std::wstring exe_dir() @@ -40,19 +39,15 @@ bool wait_for_token(HANDLE pipe, const char* token, DWORD timeout_ms) { std::string acc; const DWORD end = GetTickCount() + timeout_ms; - while (GetTickCount() < end) - { + while (GetTickCount() < end) { DWORD avail = 0; - if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) - { + if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) { char buf[256]; DWORD read = 0; - if (ReadFile(pipe, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) - { + if (ReadFile(pipe, buf, sizeof(buf) - 1, &read, nullptr) && read > 0) { acc.append(buf, read); std::fwrite(buf, 1, read, stdout); - if (acc.find(token) != std::string::npos) - { + if (acc.find(token) != std::string::npos) { return true; } continue; @@ -72,8 +67,7 @@ bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args HANDLE read_pipe = nullptr; HANDLE write_pipe = nullptr; SECURITY_ATTRIBUTES sa = {sizeof(sa), nullptr, TRUE}; - if (!CreatePipe(&read_pipe, &write_pipe, &sa, 0)) - { + if (!CreatePipe(&read_pipe, &write_pipe, &sa, 0)) { std::printf("FAIL: CreatePipe\n"); return false; } @@ -88,8 +82,7 @@ bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args PROCESS_INFORMATION pi = {}; std::vector cmd_buf(cmd.begin(), cmd.end()); cmd_buf.push_back(L'\0'); - if (!CreateProcessW(nullptr, cmd_buf.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) - { + if (!CreateProcessW(nullptr, cmd_buf.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) { std::printf("FAIL: CreateProcess(coop_tone) err=%lu\n", GetLastError()); CloseHandle(read_pipe); CloseHandle(write_pipe); @@ -98,12 +91,9 @@ bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args CloseHandle(write_pipe); // keep only the read end bool ok = false; - if (!wait_for_token(read_pipe, "TONE_RENDERING", 5000)) - { + if (!wait_for_token(read_pipe, "TONE_RENDERING", 5000)) { std::printf("FAIL: tone generator never started rendering\n"); - } - else - { + } else { ProcessLoopbackCapture capture; const bool started = capture.start(pi.dwProcessId, endpoint_fmt, nullptr); std::printf("Capture start: %s, targeting pid %lu\n", started ? "ok" : "FAILED", pi.dwProcessId); @@ -114,8 +104,7 @@ bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args // Expect at least ~0.2 s of non-silent audio for a 1.5 s capture. const unsigned long long need = endpoint_fmt->nSamplesPerSec / 5; - std::printf("Non-silent frames: %llu (need >= %llu)\n", static_cast(nonsilent), - need); + std::printf("Non-silent frames: %llu (need >= %llu)\n", static_cast(nonsilent), need); ok = nonsilent >= need; std::printf("%s\n", ok ? "PASS" : "FAIL: too few non-silent frames"); } @@ -132,27 +121,23 @@ bool capture_one(const WAVEFORMATEX* endpoint_fmt, const std::wstring& tone_args int main() { - if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) - { + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::printf("FAIL: CoInitializeEx\n"); return 1; } WAVEFORMATEX* fmt = default_render_format(); - if (!fmt) - { + if (!fmt) { std::printf("SKIP: no default render endpoint (no audio device?)\n"); CoUninitialize(); return 0; } - std::printf("Endpoint format: %u Hz, %u ch, %u-bit\n", fmt->nSamplesPerSec, fmt->nChannels, - fmt->wBitsPerSample); + std::printf("Endpoint format: %u Hz, %u ch, %u-bit\n", fmt->nSamplesPerSec, fmt->nChannels, fmt->wBitsPerSample); // Each case spawns coop_tone at a different source format; loopback should capture all // of them correctly because it captures post-mix at the device endpoint format. // Args: . ~8 s outlives capture. - struct Case - { + struct Case { std::wstring args; const char* label; }; @@ -164,10 +149,8 @@ int main() }; int failures = 0; - for (const Case& c : cases) - { - if (!capture_one(fmt, c.args, c.label)) - { + for (const Case& c : cases) { + if (!capture_one(fmt, c.args, c.label)) { ++failures; } } diff --git a/tests/audio_mix_test.cpp b/tests/audio_mix_test.cpp index 04744d5..2f7e550 100644 --- a/tests/audio_mix_test.cpp +++ b/tests/audio_mix_test.cpp @@ -9,13 +9,11 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf("FAIL: %s\n", what); ++g_failures; } @@ -47,8 +45,7 @@ int main() float out[4] = {}; mix_store(reinterpret_cast(out), acc, 4, kWaveFormatFloat, 32); // Sum then tanh; small sums are ~unchanged. - for (int i = 0; i < 4; ++i) - { + for (int i = 0; i < 4; ++i) { check(near_f(out[i], std::tanh(a[i] + b[i])), "float32 mix == tanh(sum)"); } } @@ -57,8 +54,7 @@ int main() { float acc[2] = {}; const float loud[2] = {0.9f, -0.9f}; - for (int s = 0; s < 5; ++s) - { + for (int s = 0; s < 5; ++s) { mix_add(acc, reinterpret_cast(loud), 2, kWaveFormatFloat, 32); } float out[2] = {}; @@ -79,8 +75,7 @@ int main() check(std::abs(out[0] - 1000) <= 3 && std::abs(out[1] - (-2000)) <= 3, "int16 round-trip"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS: audio_mix_test\n"); return 0; } diff --git a/tests/audio_overrides_test.cpp b/tests/audio_overrides_test.cpp index f35d9b9..8488895 100644 --- a/tests/audio_overrides_test.cpp +++ b/tests/audio_overrides_test.cpp @@ -12,18 +12,14 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf("FAIL: %s\n", what); ++g_failures; - } - else - { + } else { std::printf(" ok: %s\n", what); } } @@ -93,8 +89,7 @@ int main() } DeleteFileW(path.c_str()); - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS audio_overrides_test\n"); return 0; } diff --git a/tests/audio_ring_test.cpp b/tests/audio_ring_test.cpp index fd7eb75..61c4622 100644 --- a/tests/audio_ring_test.cpp +++ b/tests/audio_ring_test.cpp @@ -13,15 +13,13 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -49,8 +47,7 @@ int main() check(!audio_ring_format_ready(*h), "format not ready before set"); audio_ring_set_format(*h, 48000, 2, 32, 3 /*IEEE_FLOAT*/, 8); check(audio_ring_format_ready(*h), "format ready after set"); - check(h->sample_rate == 48000 && h->channels == 2 && h->bits == 32 && h->format_tag == 3 && - h->block_align == 8, + check(h->sample_rate == 48000 && h->channels == 2 && h->bits == 32 && h->format_tag == 3 && h->block_align == 8, "format fields round-trip"); } @@ -60,8 +57,7 @@ int main() AudioRingHeader* h = make_ring(storage, 4096); std::uint8_t src[256]; - for (int i = 0; i < 256; ++i) - { + for (int i = 0; i < 256; ++i) { src[i] = static_cast(i); } check(audio_ring_push(*h, src, sizeof(src), 32), "push 256 bytes"); @@ -83,11 +79,9 @@ int main() std::uint8_t counter = 0; std::uint8_t expect = 0; const std::uint32_t chunk = 300; // not a divisor of cap, so offsets drift across the seam - for (int iter = 0; iter < 50; ++iter) - { + for (int iter = 0; iter < 50; ++iter) { std::uint8_t buf[300]; - for (std::uint32_t i = 0; i < chunk; ++i) - { + for (std::uint32_t i = 0; i < chunk; ++i) { buf[i] = counter++; } check(audio_ring_push(*h, buf, chunk, chunk), "wrap push fits"); @@ -95,8 +89,7 @@ int main() std::uint8_t out[300] = {}; check(audio_ring_pop(*h, out, chunk) == chunk, "wrap pop full chunk"); bool ok = true; - for (std::uint32_t i = 0; i < chunk; ++i) - { + for (std::uint32_t i = 0; i < chunk; ++i) { ok = ok && out[i] == expect++; } check(ok, "wrap data integrity across the ring seam"); @@ -142,8 +135,7 @@ int main() // A 200-byte push now splits across the seam; it fits, so the data must survive the split. std::vector wrap(200); - for (std::uint32_t i = 0; i < 200; ++i) - { + for (std::uint32_t i = 0; i < 200; ++i) { wrap[i] = static_cast(i); } check(audio_ring_push(*h, wrap.data(), 200, 1), "seam: a wrapping push that fits is accepted"); @@ -160,8 +152,7 @@ int main() std::vector out(200, 0); check(audio_ring_pop(*h, out.data(), 200) == 200, "seam: pop the wrapped payload"); bool ok = true; - for (std::uint32_t i = 0; i < 200; ++i) - { + for (std::uint32_t i = 0; i < 200; ++i) { ok = ok && out[i] == static_cast(i); } check(ok, "seam: wrapping push/pop preserved data across the buffer seam"); diff --git a/tests/audio_verify_test.cpp b/tests/audio_verify_test.cpp index 5cf1113..0cab5c2 100644 --- a/tests/audio_verify_test.cpp +++ b/tests/audio_verify_test.cpp @@ -27,14 +27,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -42,18 +40,14 @@ void check(bool ok, const char* what) void kill_stray_mock_games() { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snap == INVALID_HANDLE_VALUE) - { + if (snap == INVALID_HANDLE_VALUE) { return; } PROCESSENTRY32W pe{}; pe.dwSize = sizeof(pe); - for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) - { - if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0) - { - if (HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID)) - { + for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) { + if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0) { + if (HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID)) { TerminateProcess(h, 0); CloseHandle(h); } @@ -65,26 +59,22 @@ void kill_stray_mock_games() bool inject(unsigned long pid) { const std::wstring dll = deployed_artifact_path(L"coop_hook.dll"); - if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES) { return false; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { return false; } const SIZE_T bytes = (dll.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) - { - auto load = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); - if (HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr)) - { + if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) { + auto load = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr)) { WaitForSingleObject(th, INFINITE); DWORD code = 0; GetExitCodeThread(th, &code); @@ -92,8 +82,7 @@ bool inject(unsigned long pid) ok = code != 0; } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); @@ -102,10 +91,8 @@ bool inject(unsigned long pid) bool inject_retry(unsigned long pid) { - for (int i = 0; i < 4; ++i) - { - if (inject(pid)) - { + for (int i = 0; i < 4; ++i) { + if (inject(pid)) { return true; } Sleep(300); @@ -113,8 +100,7 @@ bool inject_retry(unsigned long pid) return false; } -struct Scenario -{ +struct Scenario { unsigned rate, channels, bits; bool distinct; // distinct per-channel content (so the channel count is recoverable) bool recover_layout; // false = rate only (step a); true = full layout (step b) @@ -128,25 +114,21 @@ FormatVerification run(const Scenario& sc, bool& ran) FormatVerification fv; kill_stray_mock_games(); - if (sc.distinct) - { + if (sc.distinct) { SetEnvironmentVariableW(L"COOP_TONE_DISTINCT_CH", L"1"); } const std::wstring exe = exe_directory() + L"coop_mock_game.exe"; - std::wstring cmd = L"\"" + exe + L"\" dx11 30 " + std::to_wstring(sc.rate) + L" " + - std::to_wstring(sc.channels) + L" " + std::to_wstring(sc.bits) + L" " + - (sc.bits == 16 ? L"pcm" : L"float"); + std::wstring cmd = L"\"" + exe + L"\" dx11 30 " + std::to_wstring(sc.rate) + L" " + std::to_wstring(sc.channels) + + L" " + std::to_wstring(sc.bits) + L" " + (sc.bits == 16 ? L"pcm" : L"float"); STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; - const BOOL launched = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, - &si, &pi); - if (sc.distinct) - { + const BOOL launched = + CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); + if (sc.distinct) { SetEnvironmentVariableW(L"COOP_TONE_DISTINCT_CH", nullptr); } - if (!launched) - { + if (!launched) { return fv; } auto cleanup = [&] { @@ -160,9 +142,8 @@ FormatVerification run(const Scenario& sc, bool& ran) SharedMemory shm; SharedMemory ring_shm; - if (!shm.create(shared_memory_name(pi.dwProcessId), sizeof(SharedBlock)) || - !ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) - { + if (!shm.create(shared_memory_name(pi.dwProcessId), sizeof(SharedBlock)) + || !ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) { cleanup(); return fv; } @@ -178,8 +159,7 @@ FormatVerification run(const Scenario& sc, bool& ran) auto* ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); // capture_enabled stays 0: audible + still measuring - if (!inject_retry(pi.dwProcessId)) - { + if (!inject_retry(pi.dwProcessId)) { cleanup(); return fv; } @@ -197,8 +177,7 @@ int main() const bool com = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)); unsigned dev_rate = 48000, dev_channels = 2; - if (WAVEFORMATEX* dev = default_render_format()) - { + if (WAVEFORMATEX* dev = default_render_format()) { dev_rate = dev->nSamplesPerSec; dev_channels = dev->nChannels; CoTaskMemFree(dev); @@ -210,21 +189,17 @@ int main() std::printf("== (a) rate recovery: %u Hz / %u ch ==\n", mismatched, dev_channels); bool ran = false; FormatVerification a = run({mismatched, dev_channels, 32, /*distinct=*/false, /*recover_layout=*/false}, ran); - if (!ran) - { + if (!ran) { std::printf(" environment can't run the mock+inject -- skipping audio_verify_test.\n"); - if (com) - { + if (com) { CoUninitialize(); } return 0; } std::printf(" ok=%d rate=%u score=%.3f\n", a.ok ? 1 : 0, a.rate, a.score); - if (!a.ok && a.rate == 0 && a.score == 0.0) - { + if (!a.ok && a.rate == 0 && a.score == 0.0) { std::printf(" no usable co-capture (no endpoint / silent) -- skipping.\n"); - if (com) - { + if (com) { CoUninitialize(); } return 0; @@ -238,25 +213,20 @@ int main() FormatVerification b = run({44100, 2, 32, /*distinct=*/true, /*recover_layout=*/true}, ran); std::printf(" ok=%d rate=%u ch=%u bits=%u tag=%u score=%.3f\n", b.ok ? 1 : 0, b.rate, b.channels, b.bits, b.format_tag, b.score); - if (b.ok || b.score > 0.0) - { + if (b.ok || b.score > 0.0) { check(b.layout_ok, "(b) verifier confidently recovered the layout"); check(b.rate == 44100, "(b) recovered the true rate"); check(b.channels == 2, "(b) recovered the true channel count (2, not the device's)"); check(b.bits == 32 && b.format_tag == 3, "(b) recovered 32-bit float"); - } - else - { + } else { std::printf(" no usable co-capture for (b) -- skipping that scenario.\n"); } - if (com) - { + if (com) { CoUninitialize(); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS audio_verify_test\n"); return 0; } diff --git a/tests/d3d9_hook_test.cpp b/tests/d3d9_hook_test.cpp index c1bdfbd..123da41 100644 --- a/tests/d3d9_hook_test.cpp +++ b/tests/d3d9_hook_test.cpp @@ -23,13 +23,11 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -37,8 +35,7 @@ void check(bool ok, const char* what) template void release(T*& p) { - if (p) - { + if (p) { p->Release(); p = nullptr; } @@ -55,8 +52,7 @@ constexpr UINT kH = 720; int main() { SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -68,15 +64,13 @@ int main() hook::IpcClient ipc; check(ipc.connect(10, 5), "IPC client connect"); - if (!hook::install_d3d9_hooks(ipc)) - { + if (!hook::install_d3d9_hooks(ipc)) { std::printf("SKIP: could not install the D3D9 Present hook (no d3d9.dll / device?)\n"); return 0; } IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION); - if (d3d == nullptr) - { + if (d3d == nullptr) { std::printf("SKIP: Direct3DCreate9 failed\n"); hook::remove_d3d9_hooks(); return 0; @@ -101,8 +95,7 @@ int main() IDirect3DDevice9* dev = nullptr; HRESULT hr = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &dev); - if (FAILED(hr) || dev == nullptr) - { + if (FAILED(hr) || dev == nullptr) { std::printf("SKIP: CreateDevice failed (hr=0x%08lX)\n", static_cast(hr)); release(d3d); hook::remove_d3d9_hooks(); @@ -111,8 +104,7 @@ int main() // Clear to a known color (R=51,G=102,B=153) and Present -> fires the detour. const D3DCOLOR color = D3DCOLOR_XRGB(51, 102, 153); - for (int frame = 0; frame < 3; ++frame) - { + for (int frame = 0; frame < 3; ++frame) { dev->Clear(0, nullptr, D3DCLEAR_TARGET, color, 1.0f, 0); dev->Present(nullptr, nullptr, nullptr, nullptr); } @@ -132,18 +124,15 @@ int main() ID3D11Device* devB = nullptr; ID3D11DeviceContext* ctxB = nullptr; if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, - &devB, nullptr, &ctxB))) - { + &devB, nullptr, &ctxB))) { ID3D11Device1* dev1 = nullptr; devB->QueryInterface(IID_PPV_ARGS(&dev1)); ID3D11Texture2D* sharedB = nullptr; IDXGIKeyedMutex* km = nullptr; const std::wstring name = video_share_name(GetCurrentProcessId()); - if (dev1 != nullptr && - SUCCEEDED(dev1->OpenSharedResourceByName(name.c_str(), - DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, - IID_PPV_ARGS(&sharedB)))) - { + if (dev1 != nullptr + && SUCCEEDED(dev1->OpenSharedResourceByName( + name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, IID_PPV_ARGS(&sharedB)))) { sharedB->QueryInterface(IID_PPV_ARGS(&km)); D3D11_TEXTURE2D_DESC sd{}; sharedB->GetDesc(&sd); @@ -153,32 +142,24 @@ int main() sd.MiscFlags = 0; ID3D11Texture2D* staging = nullptr; check(SUCCEEDED(devB->CreateTexture2D(&sd, nullptr, &staging)), "create staging texture"); - if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) - { + if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) { ctxB->CopyResource(staging, sharedB); km->ReleaseSync(kVideoMutexKey); D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) - { + if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) { const auto* px = static_cast(mapped.pData); std::printf("readback pixel0 = {%u,%u,%u,%u}\n", px[0], px[1], px[2], px[3]); check(near_byte(px[0], 51) && near_byte(px[1], 102) && near_byte(px[2], 153), "shared texture carries the rendered color (BGRA->RGBA swizzle)"); ctxB->Unmap(staging, 0); - } - else - { + } else { check(false, "map staging texture"); } - } - else - { + } else { check(false, "acquire keyed mutex + copy shared texture"); } release(staging); - } - else - { + } else { check(false, "open shared texture by name"); } release(km); diff --git a/tests/detour_gate_test.cpp b/tests/detour_gate_test.cpp index 2e5ecc7..551fdcb 100644 --- a/tests/detour_gate_test.cpp +++ b/tests/detour_gate_test.cpp @@ -13,14 +13,12 @@ using coop::hook::DetourGate; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -48,8 +46,7 @@ int main() // Release the Guard; drain() must then return promptly. delete guard; const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); - while (!drained.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) - { + while (!drained.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } check(drained.load(std::memory_order_acquire), "drain() returns once the in-flight detour finishes"); @@ -62,8 +59,8 @@ int main() DetourGate gate; const auto t0 = std::chrono::steady_clock::now(); gate.drain(); - const auto ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - t0) - .count(); + const auto ms = + std::chrono::duration_cast(std::chrono::steady_clock::now() - t0).count(); check(ms < 100, "drain() with no in-flight detours returns quickly"); } @@ -79,15 +76,12 @@ int main() std::atomic violations{0}; auto worker = [&] { - while (!stop.load(std::memory_order_relaxed)) - { - if (disabled.load(std::memory_order_acquire)) - { + while (!stop.load(std::memory_order_relaxed)) { + if (disabled.load(std::memory_order_acquire)) { continue; // "hook removed" -> no new detour starts } DetourGate::Guard g(gate); - if (freed.load(std::memory_order_acquire)) - { + if (freed.load(std::memory_order_acquire)) { violations.fetch_add(1, std::memory_order_relaxed); // ran the body on freed state } guarded.fetch_add(1, std::memory_order_relaxed); @@ -95,12 +89,10 @@ int main() }; std::thread workers[6]; - for (auto& w : workers) - { + for (auto& w : workers) { w = std::thread(worker); } - for (int c = 0; c < 3000; ++c) - { + for (int c = 0; c < 3000; ++c) { disabled.store(true, std::memory_order_release); // disable: no new detours gate.drain(); // wait for in-flight detours freed.store(true, std::memory_order_release); // "free" the shared state @@ -108,8 +100,7 @@ int main() disabled.store(false, std::memory_order_release); } stop.store(true, std::memory_order_relaxed); - for (auto& w : workers) - { + for (auto& w : workers) { w.join(); } std::printf(" stress: guarded=%lld violations=%lld\n", guarded.load(), violations.load()); diff --git a/tests/dinput_hook_test.cpp b/tests/dinput_hook_test.cpp index 2ef5b69..7fe2ff0 100644 --- a/tests/dinput_hook_test.cpp +++ b/tests/dinput_hook_test.cpp @@ -23,14 +23,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -44,8 +42,8 @@ HWND make_window() wc.hInstance = GetModuleHandleW(nullptr); wc.lpszClassName = L"coop_dinput_test"; RegisterClassExW(&wc); - return CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 16, 16, nullptr, nullptr, - wc.hInstance, nullptr); + return CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 16, 16, nullptr, nullptr, wc.hInstance, + nullptr); } // Create + acquire a DI device of `kind` (GUID_SysKeyboard / GUID_SysMouse) with `fmt`. Returns null @@ -53,13 +51,11 @@ HWND make_window() IDirectInputDevice8W* make_device(IDirectInput8W* di, const GUID& kind, const DIDATAFORMAT* fmt, HWND hwnd) { IDirectInputDevice8W* dev = nullptr; - if (FAILED(di->CreateDevice(kind, &dev, nullptr)) || dev == nullptr) - { + if (FAILED(di->CreateDevice(kind, &dev, nullptr)) || dev == nullptr) { return nullptr; } - if (FAILED(dev->SetDataFormat(fmt)) || - FAILED(dev->SetCooperativeLevel(hwnd, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)) || FAILED(dev->Acquire())) - { + if (FAILED(dev->SetDataFormat(fmt)) || FAILED(dev->SetCooperativeLevel(hwnd, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)) + || FAILED(dev->Acquire())) { dev->Release(); return nullptr; } @@ -73,8 +69,7 @@ int main() // Host side: the IPC block + a couple of forwarded events. SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("Could not create shared memory -- skipping.\n"); return 0; } @@ -99,35 +94,30 @@ int main() IDirectInput8W* di = nullptr; if (FAILED(DirectInput8Create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W, - reinterpret_cast(&di), nullptr)) || - di == nullptr) - { + reinterpret_cast(&di), nullptr)) + || di == nullptr) { std::printf("DirectInput8Create failed -- skipping.\n"); hook::remove_mkb_hooks(); return 0; } // Keyboard: GetDeviceState must show our forwarded key (at its DIK = scan code). - if (IDirectInputDevice8W* kbd = make_device(di, GUID_SysKeyboard, &c_dfDIKeyboard, hwnd)) - { + if (IDirectInputDevice8W* kbd = make_device(di, GUID_SysKeyboard, &c_dfDIKeyboard, hwnd)) { BYTE keys[256] = {}; const HRESULT hr = kbd->GetDeviceState(sizeof(keys), keys); const BYTE dik = static_cast(MapVirtualKeyW(vk, MAPVK_VK_TO_VSC) & 0xFF); - std::printf(" keyboard GetDeviceState hr=0x%08lX dik=0x%02X state=0x%02X\n", - static_cast(hr), dik, keys[dik]); + std::printf(" keyboard GetDeviceState hr=0x%08lX dik=0x%02X state=0x%02X\n", static_cast(hr), + dik, keys[dik]); check(SUCCEEDED(hr), "keyboard GetDeviceState succeeded"); check((keys[dik] & 0x80) != 0, "forwarded 'A' appears in the DirectInput keyboard state"); kbd->Unacquire(); kbd->Release(); - } - else - { + } else { std::printf(" keyboard device unavailable -- skipping keyboard assertion.\n"); } // Mouse: GetDeviceState must show our forwarded left button. - if (IDirectInputDevice8W* ms = make_device(di, GUID_SysMouse, &c_dfDIMouse, hwnd)) - { + if (IDirectInputDevice8W* ms = make_device(di, GUID_SysMouse, &c_dfDIMouse, hwnd)) { DIMOUSESTATE m{}; const HRESULT hr = ms->GetDeviceState(sizeof(m), &m); std::printf(" mouse GetDeviceState hr=0x%08lX btn0=0x%02X\n", static_cast(hr), @@ -136,20 +126,16 @@ int main() check((m.rgbButtons[0] & 0x80) != 0, "forwarded left button appears in the DirectInput mouse state"); ms->Unacquire(); ms->Release(); - } - else - { + } else { std::printf(" mouse device unavailable -- skipping mouse assertion.\n"); } di->Release(); hook::remove_mkb_hooks(); - if (hwnd != nullptr) - { + if (hwnd != nullptr) { DestroyWindow(hwnd); } - if (com) - { + if (com) { CoUninitialize(); } diff --git a/tests/dpi_test.cpp b/tests/dpi_test.cpp index 191cbe6..d36f9d8 100644 --- a/tests/dpi_test.cpp +++ b/tests/dpi_test.cpp @@ -9,14 +9,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } diff --git a/tests/dx12_present_hook_test.cpp b/tests/dx12_present_hook_test.cpp index 88f611e..590d7a0 100644 --- a/tests/dx12_present_hook_test.cpp +++ b/tests/dx12_present_hook_test.cpp @@ -26,14 +26,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -42,8 +40,7 @@ void check(bool ok, const char* what) template void release(T*& p) { - if (p) - { + if (p) { p->Release(); p = nullptr; } @@ -66,8 +63,7 @@ int main() { // --- Host side: shared block named by our pid (the hook opens the same name). --- SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -78,8 +74,7 @@ int main() // --- D3D12 device + direct queue. SKIP if the machine has no D3D12. --- ID3D12Device* device = nullptr; - if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device))) || device == nullptr) - { + if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device))) || device == nullptr) { std::printf("SKIP: no D3D12 device on this machine\n"); return 0; } @@ -114,8 +109,7 @@ int main() "create D3D12 swapchain"); IDXGISwapChain* swapchain = nullptr; IDXGISwapChain3* sc3 = nullptr; // for GetCurrentBackBufferIndex - if (sc1 != nullptr) - { + if (sc1 != nullptr) { sc1->QueryInterface(IID_PPV_ARGS(&swapchain)); sc1->QueryInterface(IID_PPV_ARGS(&sc3)); } @@ -128,11 +122,9 @@ int main() device->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&rtv_heap)); const UINT rtv_size = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); ID3D12Resource* render_targets[kBuffers] = {}; - if (swapchain != nullptr && rtv_heap != nullptr) - { + if (swapchain != nullptr && rtv_heap != nullptr) { D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_heap->GetCPUDescriptorHandleForHeapStart(); - for (UINT i = 0; i < kBuffers; ++i) - { + for (UINT i = 0; i < kBuffers; ++i) { swapchain->GetBuffer(i, IID_PPV_ARGS(&render_targets[i])); device->CreateRenderTargetView(render_targets[i], nullptr, rtv); rtv.ptr += rtv_size; @@ -143,8 +135,7 @@ int main() device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)); ID3D12GraphicsCommandList* cmdlist = nullptr; device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator, nullptr, IID_PPV_ARGS(&cmdlist)); - if (cmdlist != nullptr) - { + if (cmdlist != nullptr) { cmdlist->Close(); } ID3D12Fence* fence = nullptr; @@ -159,8 +150,7 @@ int main() const bool can_render = swapchain != nullptr && sc3 != nullptr && allocator != nullptr && cmdlist != nullptr && fence != nullptr; - for (int frame = 0; frame < 4 && can_render; ++frame) - { + for (int frame = 0; frame < 4 && can_render; ++frame) { const UINT idx = sc3->GetCurrentBackBufferIndex(); allocator->Reset(); cmdlist->Reset(allocator, nullptr); @@ -188,8 +178,7 @@ int main() // Block until the GPU finished this frame (keeps the test simple + correct). queue->Signal(fence, ++fence_value); - if (fence->GetCompletedValue() < fence_value) - { + if (fence->GetCompletedValue() < fence_value) { fence->SetEventOnCompletion(fence_value, fence_event); WaitForSingleObject(fence_event, 1000); } @@ -200,8 +189,7 @@ int main() static_cast(hook::present_frames_shared()), block->video.generation.load(), block->video.width, block->video.height, block->video.format); - if (can_render) - { + if (can_render) { check(hook::present_calls() >= 3, "Present detour fired for the D3D12 swapchain"); check(hook::present_frames_shared() > 0, "D3D12 backbuffer bridged into the shared texture"); check(block->video.generation.load() > 0, "video generation published to IPC"); @@ -211,18 +199,15 @@ int main() ID3D11Device* devB = nullptr; ID3D11DeviceContext* ctxB = nullptr; if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, - &devB, nullptr, &ctxB))) - { + &devB, nullptr, &ctxB))) { ID3D11Device1* dev1 = nullptr; devB->QueryInterface(IID_PPV_ARGS(&dev1)); const std::wstring name = video_share_name(GetCurrentProcessId()); ID3D11Texture2D* sharedB = nullptr; IDXGIKeyedMutex* km = nullptr; - if (dev1 != nullptr && - SUCCEEDED(dev1->OpenSharedResourceByName(name.c_str(), - DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, - IID_PPV_ARGS(&sharedB)))) - { + if (dev1 != nullptr + && SUCCEEDED(dev1->OpenSharedResourceByName( + name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, IID_PPV_ARGS(&sharedB)))) { sharedB->QueryInterface(IID_PPV_ARGS(&km)); D3D11_TEXTURE2D_DESC sd{}; sharedB->GetDesc(&sd); @@ -232,32 +217,24 @@ int main() sd.MiscFlags = 0; ID3D11Texture2D* staging = nullptr; check(SUCCEEDED(devB->CreateTexture2D(&sd, nullptr, &staging)), "create staging texture"); - if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) - { + if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) { ctxB->CopyResource(staging, sharedB); km->ReleaseSync(kVideoMutexKey); D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) - { + if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) { const auto* px = static_cast(mapped.pData); std::printf("readback pixel0 = {%u,%u,%u,%u}\n", px[0], px[1], px[2], px[3]); check(near_byte(px[0], 51) && near_byte(px[1], 102) && near_byte(px[2], 153), "shared texture carries the D3D12-rendered color"); ctxB->Unmap(staging, 0); - } - else - { + } else { check(false, "map staging texture"); } - } - else - { + } else { check(false, "acquire keyed mutex + copy shared texture"); } release(staging); - } - else - { + } else { check(false, "open shared texture by name"); } release(km); @@ -292,8 +269,7 @@ int main() auto present = [&] { swapchain->Present(0, 0); queue->Signal(fence, ++fence_value); - if (fence->GetCompletedValue() < fence_value) - { + if (fence->GetCompletedValue() < fence_value) { fence->SetEventOnCompletion(fence_value, fence_event); WaitForSingleObject(fence_event, 1000); } @@ -309,15 +285,13 @@ int main() hook::remove_present_hooks(); - if (fence_event != nullptr) - { + if (fence_event != nullptr) { CloseHandle(fence_event); } release(fence); release(cmdlist); release(allocator); - for (UINT i = 0; i < kBuffers; ++i) - { + for (UINT i = 0; i < kBuffers; ++i) { release(render_targets[i]); } release(rtv_heap); @@ -330,7 +304,6 @@ int main() DestroyWindow(hwnd); UnregisterClassW(wc.lpszClassName, wc.hInstance); - std::printf(g_failures == 0 ? "DX12 PRESENT HOOK TEST PASS\n" : "DX12 PRESENT HOOK TEST FAILED (%d)\n", - g_failures); + std::printf(g_failures == 0 ? "DX12 PRESENT HOOK TEST PASS\n" : "DX12 PRESENT HOOK TEST FAILED (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; } diff --git a/tests/focus_spoof_test.cpp b/tests/focus_spoof_test.cpp index 3dd8728..3e612c3 100644 --- a/tests/focus_spoof_test.cpp +++ b/tests/focus_spoof_test.cpp @@ -14,22 +14,19 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } void pump() { MSG msg; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) - { + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } @@ -39,8 +36,7 @@ void pump() int main() { SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -66,17 +62,14 @@ int main() // Install, retrying a few times in case the window isn't enumerable yet. bool installed = false; - for (int i = 0; i < 20 && !installed; ++i) - { + for (int i = 0; i < 20 && !installed; ++i) { installed = hook::install_focus_spoof(ipc); - if (!installed) - { + if (!installed) { pump(); Sleep(20); } } - if (!installed) - { + if (!installed) { std::printf("SKIP: focus spoof could not find the test window\n"); DestroyWindow(win); UnregisterClassW(wc.lpszClassName, wc.hInstance); diff --git a/tests/hook_install_test.cpp b/tests/hook_install_test.cpp index 14d0c08..7d2d77a 100644 --- a/tests/hook_install_test.cpp +++ b/tests/hook_install_test.cpp @@ -14,14 +14,12 @@ using coop::hook::DetourGate; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -53,8 +51,7 @@ void install() } void remove() { - if (g_hook && !g_hook.disable()) - { + if (g_hook && !g_hook.disable()) { // surface, don't discard } g_gate.drain(); // persistent model: disable + drain, but do NOT destroy @@ -87,8 +84,7 @@ int main() // Many cycles: the trampoline must never change and enable/disable must toggle cleanly. bool reuse = true, toggles = true; - for (int i = 0; i < 50; ++i) - { + for (int i = 0; i < 50; ++i) { remove(); toggles = toggles && !g_hook.enabled(); install(); diff --git a/tests/hook_selftest.cpp b/tests/hook_selftest.cpp index 841076a..ade1e2d 100644 --- a/tests/hook_selftest.cpp +++ b/tests/hook_selftest.cpp @@ -15,8 +15,7 @@ using namespace coop; -namespace -{ +namespace { constexpr std::uint16_t kButtonA = 0x1000; constexpr std::uint16_t kButtonB = 0x2000; @@ -25,8 +24,7 @@ int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -41,8 +39,7 @@ void check(bool ok, const char* what) void exercise_dll(const wchar_t* dll_name) { HMODULE m = GetModuleHandleW(dll_name); - if (m == nullptr) - { + if (m == nullptr) { return; // not loaded on this machine; nothing to exercise } char tag[96]; @@ -56,26 +53,22 @@ void exercise_dll(const wchar_t* dll_name) auto get_caps = reinterpret_cast(GetProcAddress(m, "XInputGetCapabilities")); auto set_state = reinterpret_cast(GetProcAddress(m, "XInputSetState")); - if (get_state != nullptr) - { + if (get_state != nullptr) { XINPUT_STATE s = {}; std::snprintf(tag, sizeof(tag), "%ls XInputGetState forwards state", dll_name); check(get_state(0, &s) == ERROR_SUCCESS && s.dwPacketNumber == 7, tag); } - if (get_state_ex != nullptr) - { + if (get_state_ex != nullptr) { XINPUT_STATE s = {}; std::snprintf(tag, sizeof(tag), "%ls XInputGetStateEx (ord 100) forwards state", dll_name); check(get_state_ex(0, &s) == ERROR_SUCCESS && s.dwPacketNumber == 7, tag); } - if (get_caps != nullptr) - { + if (get_caps != nullptr) { XINPUT_CAPABILITIES c = {}; std::snprintf(tag, sizeof(tag), "%ls XInputGetCapabilities reports gamepad", dll_name); check(get_caps(0, 0, &c) == ERROR_SUCCESS && c.Type == XINPUT_DEVTYPE_GAMEPAD, tag); } - if (set_state != nullptr) - { + if (set_state != nullptr) { // A game commonly rumbles in response to a button press; this is the call // path "crashes as soon as a button is pressed" pointed at. XINPUT_VIBRATION v = {}; @@ -89,9 +82,9 @@ void exercise_dll(const wchar_t* dll_name) void dump_layout() { std::printf("LAYOUT sizeof(SharedBlock)=%zu CoopPadState=%zu\n", sizeof(SharedBlock), sizeof(CoopPadState)); - std::printf("LAYOUT off pads=%zu sequence=%zu status=%zu control=%zu video=%zu\n", - offsetof(SharedBlock, pads), offsetof(SharedBlock, sequence), offsetof(SharedBlock, status), - offsetof(SharedBlock, control), offsetof(SharedBlock, video)); + std::printf("LAYOUT off pads=%zu sequence=%zu status=%zu control=%zu video=%zu\n", offsetof(SharedBlock, pads), + offsetof(SharedBlock, sequence), offsetof(SharedBlock, status), offsetof(SharedBlock, control), + offsetof(SharedBlock, video)); std::printf("LAYOUT HookStatus sizeof=%zu get_state_calls=%zu attached=%zu audio_streams=%zu hook_entries=%zu\n", sizeof(HookStatus), offsetof(HookStatus, get_state_calls), offsetof(HookStatus, attached), offsetof(HookStatus, audio_streams), offsetof(HookStatus, hook_entries)); @@ -118,8 +111,7 @@ int main() // --- Host side: create the section (named by our pid) and publish a pad. --- SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: could not create shared memory\n"); return 1; } @@ -143,8 +135,7 @@ int main() // loads exactly one, but which one varies by game age -- and the 32-bit crash // only reproduces over the specific DLL the game uses. const wchar_t* xinput_modules[] = {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinputuap.dll"}; - for (const wchar_t* name : xinput_modules) - { + for (const wchar_t* name : xinput_modules) { LoadLibraryW(name); // best-effort; absent variants stay unloaded } @@ -172,8 +163,7 @@ int main() // Now drive every loaded variant's full export set (GetState, ordinal-100 // GetStateEx, GetCapabilities, and the rumble SetState a game calls on a button // press) so each DLL's hooked prologue/trampoline is actually run. - for (const wchar_t* name : xinput_modules) - { + for (const wchar_t* name : xinput_modules) { exercise_dll(name); } diff --git a/tests/ipc_server_test.cpp b/tests/ipc_server_test.cpp index c41bb39..dd2a177 100644 --- a/tests/ipc_server_test.cpp +++ b/tests/ipc_server_test.cpp @@ -15,24 +15,20 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } bool all_disabled(const SharedBlock* b) { - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { - if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 1u) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { + if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 1u) { return false; } } @@ -41,10 +37,8 @@ bool all_disabled(const SharedBlock* b) bool none_disabled(const SharedBlock* b) { - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { - if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 0u) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { + if (b->control.subsystem_disabled[s].load(std::memory_order_acquire) != 0u) { return false; } } @@ -57,8 +51,7 @@ int main() const unsigned long pid = GetCurrentProcessId(); // section name is per-pid; no real game needed IpcServer server; - if (!server.start(pid)) - { + if (!server.start(pid)) { std::printf("FAIL: IpcServer::start\n"); return 1; } @@ -66,8 +59,7 @@ int main() // A second view of the same section, standing in for the injected hook: it reads the control // flags the host writes and publishes the hook registry the host reads back. SharedMemory hook_view; - if (!hook_view.open(shared_memory_name(pid), sizeof(SharedBlock))) - { + if (!hook_view.open(shared_memory_name(pid), sizeof(SharedBlock))) { std::printf("FAIL: open hook view\n"); return 1; } diff --git a/tests/keyed_mutex_test.cpp b/tests/keyed_mutex_test.cpp index a32c31e..84890c6 100644 --- a/tests/keyed_mutex_test.cpp +++ b/tests/keyed_mutex_test.cpp @@ -14,14 +14,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } diff --git a/tests/log_ring_test.cpp b/tests/log_ring_test.cpp index 022cc0d..3b6a631 100644 --- a/tests/log_ring_test.cpp +++ b/tests/log_ring_test.cpp @@ -18,14 +18,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -37,8 +35,7 @@ std::string make_line(unsigned thread, unsigned long long seq) char token[32]; std::snprintf(token, sizeof(token), "T%02uS%010llu", thread, seq); std::string s; - while (s.size() + std::strlen(token) + 1 < kLogMsgLen - 1) - { + while (s.size() + std::strlen(token) + 1 < kLogMsgLen - 1) { s += token; s += ' '; } @@ -50,29 +47,20 @@ bool line_consistent(const char* text) { std::string first; std::string cur; - for (const char* p = text;; ++p) - { - if (*p == ' ' || *p == '\0') - { - if (!cur.empty()) - { - if (first.empty()) - { + for (const char* p = text;; ++p) { + if (*p == ' ' || *p == '\0') { + if (!cur.empty()) { + if (first.empty()) { first = cur; - } - else if (cur != first) - { + } else if (cur != first) { return false; } cur.clear(); } - if (*p == '\0') - { + if (*p == '\0') { break; } - } - else - { + } else { cur.push_back(*p); } } @@ -93,8 +81,7 @@ int main() { auto buf = make_ring(8); auto& ring = *reinterpret_cast(buf.data()); - for (unsigned long long n = 0; n < 11; ++n) - { + for (unsigned long long n = 0; n < 11; ++n) { const std::string line = make_line(0, n); log_ring_push(ring, 1234, LogLevel_Info, n, line.c_str()); } @@ -103,8 +90,7 @@ int main() log_ring_drain(ring, cursor, [&](const LogRecord& rec) { got.push_back(rec.text); }); check(got.size() == 8, "wrap: drains exactly capacity lines after overflow"); bool ordered = true; - for (std::size_t k = 0; k < got.size(); ++k) - { + for (std::size_t k = 0; k < got.size(); ++k) { ordered = ordered && got[k] == make_line(0, 3 + k); // oldest 3 (0,1,2) dropped } check(ordered, "wrap: keeps the newest `capacity` lines, in order, oldest dropped"); @@ -119,12 +105,10 @@ int main() std::atomic stop{false}; std::atomic produced{0}; std::vector producers; - for (unsigned t = 0; t < 4; ++t) - { + for (unsigned t = 0; t < 4; ++t) { producers.emplace_back([&, t] { unsigned long long n = 0; - while (!stop.load(std::memory_order_relaxed)) - { + while (!stop.load(std::memory_order_relaxed)) { const std::string line = make_line(t, n++); log_ring_push(ring, t, LogLevel_Info, n, line.c_str()); produced.fetch_add(1, std::memory_order_relaxed); @@ -139,14 +123,12 @@ int main() auto drain = [&] { log_ring_drain(ring, cursor, [&](const LogRecord& rec) { consumed.fetch_add(1, std::memory_order_relaxed); - if (!line_consistent(rec.text)) - { + if (!line_consistent(rec.text)) { torn.fetch_add(1, std::memory_order_relaxed); } }); }; - while (!stop.load(std::memory_order_relaxed)) - { + while (!stop.load(std::memory_order_relaxed)) { drain(); std::this_thread::sleep_for(std::chrono::microseconds(50)); // fall behind so slots wrap } @@ -155,8 +137,7 @@ int main() std::this_thread::sleep_for(std::chrono::milliseconds(1500)); stop.store(true, std::memory_order_relaxed); - for (auto& p : producers) - { + for (auto& p : producers) { p.join(); } consumer.join(); diff --git a/tests/mkb_map_test.cpp b/tests/mkb_map_test.cpp index b25a154..733daeb 100644 --- a/tests/mkb_map_test.cpp +++ b/tests/mkb_map_test.cpp @@ -6,13 +6,11 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool cond, const char* what) { - if (!cond) - { + if (!cond) { std::printf("FAIL: %s\n", what); ++g_failures; } @@ -83,8 +81,7 @@ int main() check(map_host_to_game_client(in, gx, gy) && gx == 100 && gy == 100, "decorated: client interior"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS: mkb_map_test\n"); return 0; } diff --git a/tests/mkb_ring_test.cpp b/tests/mkb_ring_test.cpp index 23437f2..77f3ce4 100644 --- a/tests/mkb_ring_test.cpp +++ b/tests/mkb_ring_test.cpp @@ -5,13 +5,11 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool cond, const char* what) { - if (!cond) - { + if (!cond) { std::printf("FAIL: %s\n", what); ++g_failures; } @@ -27,13 +25,11 @@ int main() check(!pop_mkb_event(ring, out), "pop on empty ring returns false"); // Push then pop returns the same event, FIFO. - for (std::uint32_t i = 0; i < 10; ++i) - { + for (std::uint32_t i = 0; i < 10; ++i) { MkbEvent ev{Mkb_KeyDown, i, static_cast(i) * 2, static_cast(i) * 3}; check(push_mkb_event(ring, ev), "push succeeds with room"); } - for (std::uint32_t i = 0; i < 10; ++i) - { + for (std::uint32_t i = 0; i < 10; ++i) { check(pop_mkb_event(ring, out), "pop succeeds with data"); check(out.code == i && out.x == static_cast(i) * 2 && out.y == static_cast(i) * 3, "popped event matches pushed (FIFO)"); @@ -41,39 +37,32 @@ int main() check(!pop_mkb_event(ring, out), "ring empty again after draining"); // Fill to capacity, then one more push is dropped. - for (std::uint32_t i = 0; i < kMkbQueueSize; ++i) - { + for (std::uint32_t i = 0; i < kMkbQueueSize; ++i) { check(push_mkb_event(ring, MkbEvent{Mkb_Char, i, 0, 0}), "push fills to capacity"); } check(!push_mkb_event(ring, MkbEvent{Mkb_Char, 999, 0, 0}), "push on full ring is dropped"); // Drain and verify order survived a full buffer. - for (std::uint32_t i = 0; i < kMkbQueueSize; ++i) - { + for (std::uint32_t i = 0; i < kMkbQueueSize; ++i) { check(pop_mkb_event(ring, out) && out.code == i, "full-buffer drain is in order"); } // Wrap-around: indices are free-running, so many cycles must keep working. std::uint32_t produced = 0, consumed = 0; - for (int cycle = 0; cycle < 1000; ++cycle) - { - for (int k = 0; k < 50; ++k) - { - if (push_mkb_event(ring, MkbEvent{Mkb_MouseDown, produced, 0, 0})) - { + for (int cycle = 0; cycle < 1000; ++cycle) { + for (int k = 0; k < 50; ++k) { + if (push_mkb_event(ring, MkbEvent{Mkb_MouseDown, produced, 0, 0})) { ++produced; } } - while (pop_mkb_event(ring, out)) - { + while (pop_mkb_event(ring, out)) { check(out.code == consumed, "wrap-around preserves FIFO order"); ++consumed; } } check(produced == consumed, "all wrap-around events consumed"); - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS: mkb_ring_test\n"); return 0; } diff --git a/tests/mock_game_test.cpp b/tests/mock_game_test.cpp index 568bc29..4592b5e 100644 --- a/tests/mock_game_test.cpp +++ b/tests/mock_game_test.cpp @@ -34,14 +34,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -56,19 +54,15 @@ std::wstring tool_path(const wchar_t* name) void kill_stray_mock_games() { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snap == INVALID_HANDLE_VALUE) - { + if (snap == INVALID_HANDLE_VALUE) { return; } PROCESSENTRY32W pe{}; pe.dwSize = sizeof(pe); - for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) - { - if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0) - { + for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) { + if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0) { HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID); - if (h != nullptr) - { + if (h != nullptr) { TerminateProcess(h, 0); CloseHandle(h); } @@ -81,27 +75,23 @@ void kill_stray_mock_games() bool inject(unsigned long pid) { const std::wstring dll = deployed_artifact_path(L"coop_hook.dll"); // root is one dir up from tests/ - if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES) { return false; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { return false; } const SIZE_T bytes = (dll.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) - { - auto load = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) { + auto load = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr); - if (th != nullptr) - { + if (th != nullptr) { WaitForSingleObject(th, INFINITE); DWORD code = 0; GetExitCodeThread(th, &code); @@ -109,8 +99,7 @@ bool inject(unsigned long pid) ok = code != 0; } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); @@ -120,10 +109,8 @@ bool inject(unsigned long pid) // Inject with a few retries: a freshly-launched process can briefly refuse a remote thread. bool inject_retry(unsigned long pid) { - for (int i = 0; i < 4; ++i) - { - if (inject(pid)) - { + for (int i = 0; i < 4; ++i) { + if (inject(pid)) { return true; } Sleep(300); @@ -131,8 +118,7 @@ bool inject_retry(unsigned long pid) return false; } -struct MockGame -{ +struct MockGame { PROCESS_INFORMATION pi{}; bool ok = false; @@ -144,31 +130,22 @@ struct MockGame std::wstring cmd = L"\"" + exe + L"\" " + args; STARTUPINFOW si{}; si.cb = sizeof(si); - g.ok = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, - &g.pi) != 0; + g.ok = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &g.pi) != 0; return g; } - unsigned long pid() const - { - return pi.dwProcessId; - } - bool alive() const - { - return pi.hProcess != nullptr && WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT; - } + unsigned long pid() const { return pi.dwProcessId; } + bool alive() const { return pi.hProcess != nullptr && WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT; } unsigned long exit_code() const { DWORD code = 0; - if (pi.hProcess != nullptr) - { + if (pi.hProcess != nullptr) { GetExitCodeProcess(pi.hProcess, &code); } return code; } void kill() { - if (pi.hProcess != nullptr) - { + if (pi.hProcess != nullptr) { TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); @@ -182,16 +159,14 @@ struct MockGame // HookSubsystem). Keeps the mapping alive in `shm`. SharedBlock* make_ipc(SharedMemory& shm, unsigned long pid, std::uint32_t disabled_mask) { - if (!shm.create(shared_memory_name(pid), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(pid), sizeof(SharedBlock))) { return nullptr; } auto* block = shm.as(); block->version = kProtocolVersion; block->pad_count = 0; block->sequence.store(0, std::memory_order_relaxed); - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store((disabled_mask >> s) & 1u, std::memory_order_release); } block->magic = kProtocolMagic; @@ -202,9 +177,8 @@ ID3D11Device* make_device() { ID3D11Device* dev = nullptr; const D3D_FEATURE_LEVEL fl[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; - if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, - static_cast(std::size(fl)), D3D11_SDK_VERSION, &dev, nullptr, nullptr))) - { + if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, static_cast(std::size(fl)), + D3D11_SDK_VERSION, &dev, nullptr, nullptr))) { return nullptr; } return dev; @@ -254,8 +228,7 @@ void test_video_capture(const char* backend, ID3D11Device* device) } std::wstring args = wbackend + L" 30"; MockGame game = MockGame::launch(args); - if (!game.ok) - { + if (!game.ok) { check(false, "launch coop_mock_game"); return; } @@ -263,11 +236,10 @@ void test_video_capture(const char* backend, ID3D11Device* device) // Only the video subsystem (disable input/focus/audio/mkb to keep the test focused). SharedMemory shm; - const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | - (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); + const std::uint32_t disabled = + (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); - if (block == nullptr || !inject_retry(game.pid())) - { + if (block == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game"); game.kill(); return; @@ -284,18 +256,15 @@ void test_video_capture(const char* backend, ID3D11Device* device) { Sleep(50); const VideoShareView share = read_video_share(block); - if (!src.update(share, game.pid())) - { + if (!src.update(share, game.pid())) { continue; } std::uint8_t px[4] = {}; - if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) - { + if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); - if (have_last && f < last) - { + if (have_last && f < last) { ++backward; // a stale / wrong (rotated) buffer -> frame number went backwards } last = f; @@ -312,8 +281,7 @@ void test_video_capture(const char* backend, ID3D11Device* device) check(src.frames_copied() >= 5, "host copied multiple shared frames"); check(seq.size() >= 5, "decoded multiple frame numbers from the captured pixels"); check(backward == 0, "captured frame numbers never go backwards (no stale/rotated buffer)"); - if (!seq.empty()) - { + if (!seq.empty()) { const std::uint32_t span = seq.back() - seq.front(); std::printf(" frame# %u..%u (span %u)\n", seq.front(), seq.back(), span); check(span >= 20, "captured frame numbers advance (mirror gets fresh frames)"); @@ -321,8 +289,7 @@ void test_video_capture(const char* backend, ID3D11Device* device) check(!all_same, "captured frames are not stuck on one number"); } - if (game.alive()) - { + if (game.alive()) { check_capture_present_rate(block, backend); } game.kill(); @@ -345,15 +312,14 @@ void test_vk_capture(ID3D11Device* device) const BOOL ok = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr); - if (!ok) - { + if (!ok) { check(false, "launch suspended vk mock"); return; } SharedMemory shm; - const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | - (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); + const std::uint32_t disabled = + (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, pi.dwProcessId, disabled); const bool injected = block != nullptr && inject_retry(pi.dwProcessId); ResumeThread(pi.hThread); // the mock loads vulkan + waits, then renders @@ -363,8 +329,7 @@ void test_vk_capture(ID3D11Device* device) CloseHandle(pi.hThread); CloseHandle(pi.hProcess); }; - if (!injected) - { + if (!injected) { check(false, "inject suspended vk mock"); cleanup(); return; @@ -387,31 +352,26 @@ void test_vk_capture(ID3D11Device* device) { Sleep(50); const VideoShareView share = read_video_share(block); - if (!src.update(share, pi.dwProcessId)) - { + if (!src.update(share, pi.dwProcessId)) { continue; } std::uint8_t px[4] = {}; - if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) - { + if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); - if (have_last && f < last) - { + if (have_last && f < last) { ++backward; } last = f; have_last = true; seq.push_back(f); - if (seq.size() >= 40) - { + if (seq.size() >= 40) { break; } } - if (!alive() && exit_code() == 2 && seq.empty()) - { + if (!alive() && exit_code() == 2 && seq.empty()) { std::printf(" Vulkan unavailable on this machine -- skipping vk capture\n"); cleanup(); return; @@ -424,12 +384,10 @@ void test_vk_capture(ID3D11Device* device) check(src.frames_copied() >= 5, "host copied multiple shared frames (vk)"); check(seq.size() >= 5, "decoded multiple frame numbers from the captured pixels (vk)"); check(backward == 0, "captured vk frame numbers never go backwards"); - if (!seq.empty()) - { + if (!seq.empty()) { check(seq.back() - seq.front() >= 10, "captured vk frame numbers advance"); } - if (alive()) - { + if (alive()) { check_capture_present_rate(block, "vk (inject)"); } cleanup(); @@ -443,8 +401,7 @@ void test_vk_layer_capture(ID3D11Device* device) { std::printf("== video capture: vk implicit layer ==\n"); const std::wstring manifest = deployed_artifact_path(L"coop_vk_layer.json"); - if (GetFileAttributesW(manifest.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(manifest.c_str()) == INVALID_FILE_ATTRIBUTES) { check(false, "coop_vk_layer.json staged"); return; } @@ -460,18 +417,16 @@ void test_vk_layer_capture(ID3D11Device* device) SetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", nullptr); }; unset_env(); - if (!game.ok) - { + if (!game.ok) { check(false, "launch vk mock (layer)"); return; } SharedMemory shm; - const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | - (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); + const std::uint32_t disabled = + (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); // the layer connects to this + publishes - if (block == nullptr) - { + if (block == nullptr) { check(false, "ipc block (layer)"); game.kill(); return; @@ -487,30 +442,25 @@ void test_vk_layer_capture(ID3D11Device* device) { Sleep(50); const VideoShareView share = read_video_share(block); - if (!src.update(share, game.pid())) - { + if (!src.update(share, game.pid())) { continue; } std::uint8_t px[4] = {}; - if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) - { + if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); - if (have_last && f < last) - { + if (have_last && f < last) { ++backward; } last = f; have_last = true; seq.push_back(f); - if (seq.size() >= 40) - { + if (seq.size() >= 40) { break; } } - if (!game.alive() && game.exit_code() == 2 && seq.empty()) - { + if (!game.alive() && game.exit_code() == 2 && seq.empty()) { std::printf(" Vulkan unavailable on this machine -- skipping layer capture\n"); game.kill(); return; @@ -520,12 +470,10 @@ void test_vk_layer_capture(ID3D11Device* device) check(src.frames_copied() >= 5, "layer copied multiple shared frames"); check(seq.size() >= 5, "decoded multiple frame numbers via the layer"); check(backward == 0, "layer-captured frame numbers never go backwards"); - if (!seq.empty()) - { + if (!seq.empty()) { check(seq.back() - seq.front() >= 10, "layer-captured frame numbers advance"); } - if (game.alive()) - { + if (game.alive()) { check_capture_present_rate(block, "vk (layer)"); } game.kill(); @@ -539,30 +487,24 @@ void test_vk_too_late() { std::printf("== vk too-late detection (late inject) ==\n"); MockGame game = MockGame::launch(L"vk 30"); - if (!game.ok) - { + if (!game.ok) { check(false, "launch vk mock (too-late)"); return; } Sleep(1200); // let it create its instance/device and start presenting - if (!game.alive() && game.exit_code() == 2) - { + if (!game.alive() && game.exit_code() == 2) { std::printf(" Vulkan unavailable on this machine -- skipping\n"); game.kill(); return; } SharedMemory shm; - const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | - (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); + const std::uint32_t disabled = + (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); - if (block == nullptr || !inject_retry(game.pid())) - { - if (!game.alive() && game.exit_code() == 2) - { + if (block == nullptr || !inject_retry(game.pid())) { + if (!game.alive() && game.exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping\n"); - } - else - { + } else { check(false, "inject vk mock (too-late)"); } game.kill(); @@ -572,8 +514,7 @@ void test_vk_too_late() for (int i = 0; i < 160 && game.alive(); ++i) // ~8 s (past the hook's 4 s grace) { Sleep(50); - if (block->status.vk_too_late != 0) - { + if (block->status.vk_too_late != 0) { too_late = true; break; } @@ -592,8 +533,7 @@ void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const w swprintf(args, static_cast(std::size(args)), L"dx11 30 %u %u %u %ls", rate, channels, bits, fmt); std::printf("== audio variant: %u Hz %u ch %u-bit %ls ==\n", rate, channels, bits, fmt); MockGame game = MockGame::launch(args); - if (!game.ok) - { + if (!game.ok) { check(false, "launch coop_mock_game (audio variant)"); return; } @@ -601,19 +541,17 @@ void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const w // Audio subsystem only. SharedMemory shm; - const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | - (1u << HookSubsys_Video) | (1u << HookSubsys_Mkb); + const std::uint32_t disabled = + (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Video) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); SharedMemory ring_shm; AudioRingHeader* ring = nullptr; - if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) - { + if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } - if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) - { + if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game (audio variant)"); game.kill(); return; @@ -629,35 +567,26 @@ void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const w { Sleep(50); std::uint32_t got = 0; - while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) - { - if (ring->bits == 32 && ring->format_tag == 3) - { + while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { + if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); - for (std::uint32_t k = 0; k < got / 4; ++k) - { + for (std::uint32_t k = 0; k < got / 4; ++k) { peak = std::max(peak, static_cast(std::fabs(f[k]))); } - } - else if (ring->bits == 16) - { + } else if (ring->bits == 16) { const auto* s = reinterpret_cast(drain.data()); - for (std::uint32_t k = 0; k < got / 2; ++k) - { + for (std::uint32_t k = 0; k < got / 2; ++k) { peak = std::max(peak, std::abs(s[k]) / 32768.0); } } - if (got < drain.size()) - { + if (got < drain.size()) { break; } } state = block->status.audio_streams[0].format_state; - if (state == AudioFormat_Measured || state == AudioFormat_LowConfidence || state == AudioFormat_Exact) - { + if (state == AudioFormat_Measured || state == AudioFormat_LowConfidence || state == AudioFormat_Exact) { measured = block->status.audio_streams[0].sample_rate; - if (measured != 0 && peak > 0.01) - { + if (measured != 0 && peak > 0.01) { break; } } @@ -675,8 +604,7 @@ void test_av_and_hook_cycles(ID3D11Device* device) { std::printf("== A/V + hook/unhook stress (dx11 + audio) ==\n"); MockGame game = MockGame::launch(L"dx11 30 48000 2 32 float"); - if (!game.ok) - { + if (!game.ok) { check(false, "launch coop_mock_game (A/V)"); return; } @@ -686,14 +614,12 @@ void test_av_and_hook_cycles(ID3D11Device* device) SharedBlock* block = make_ipc(shm, game.pid(), /*disabled=*/0); // all subsystems on SharedMemory ring_shm; AudioRingHeader* ring = nullptr; - if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) - { + if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } - if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) - { + if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game (A/V)"); game.kill(); return; @@ -710,32 +636,25 @@ void test_av_and_hook_cycles(ID3D11Device* device) { Sleep(50); const VideoShareView share = read_video_share(block); - if (src.update(share, game.pid())) - { + if (src.update(share, game.pid())) { std::uint8_t px[4] = {}; - if (src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) - { + if (src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); - if (first_frame == 0) - { + if (first_frame == 0) { first_frame = f; } last_frame = f; } } std::uint32_t got = 0; - while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) - { - if (ring->bits == 32 && ring->format_tag == 3) - { + while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { + if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); - for (std::uint32_t k = 0; k < got / 4; ++k) - { + for (std::uint32_t k = 0; k < got / 4; ++k) { peak = std::max(peak, static_cast(std::fabs(f[k]))); } } - if (got < drain.size()) - { + if (got < drain.size()) { break; } } @@ -747,16 +666,14 @@ void test_av_and_hook_cycles(ID3D11Device* device) // the worker's ~250 ms reconcile tick, so the install/remove fully completes each time // (this is the realistic cadence -- an operator toggling a checkbox, not thrashing it). const std::uint32_t hb_start = block->status.heartbeat.load(std::memory_order_relaxed); - for (int c = 0; c < 3 && game.alive(); ++c) - { + for (int c = 0; c < 3 && game.alive(); ++c) { block->control.subsystem_disabled[HookSubsys_Audio].store(1, std::memory_order_release); Sleep(400); block->control.subsystem_disabled[HookSubsys_Audio].store(0, std::memory_order_release); Sleep(400); } const std::uint32_t hb_end = block->status.heartbeat.load(std::memory_order_relaxed); - if (!game.alive()) - { + if (!game.alive()) { std::printf(" game exit code = 0x%08lX\n", game.exit_code()); } check(game.alive(), "game survived hook/unhook cycles (no crash)"); @@ -770,18 +687,14 @@ void test_av_and_hook_cycles(ID3D11Device* device) { Sleep(50); std::uint32_t got = 0; - while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) - { - if (ring->bits == 32 && ring->format_tag == 3) - { + while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { + if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); - for (std::uint32_t k = 0; k < got / 4; ++k) - { + for (std::uint32_t k = 0; k < got / 4; ++k) { peak2 = std::max(peak2, static_cast(std::fabs(f[k]))); } } - if (got < drain.size()) - { + if (got < drain.size()) { break; } } @@ -817,19 +730,16 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) si.cb = sizeof(si); const std::wstring exe = tool_path(L"coop_mock_game.exe"); std::wstring cmd = L"\"" + exe + L"\" " + wbackend + L" 60"; - if (vk_early) - { + if (vk_early) { SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", L"1"); } const DWORD launch_flags = vk_early ? CREATE_SUSPENDED : 0; const BOOL launched = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, launch_flags, nullptr, nullptr, &si, &pi); - if (vk_early) - { + if (vk_early) { SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr); } - if (!launched) - { + if (!launched) { check(false, "launch mock game (storm)"); return; } @@ -850,25 +760,19 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) SharedBlock* block = make_ipc(shm, pi.dwProcessId, /*disabled=*/0); // all subsystems on SharedMemory ring_shm; AudioRingHeader* ring = nullptr; - if (ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) - { + if (ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } const bool injected = block != nullptr && inject_retry(pi.dwProcessId); - if (vk_early) - { + if (vk_early) { ResumeThread(pi.hThread); // the mock loads Vulkan + waits, then renders } - if (!injected) - { - if (vk_early && !alive() && exit_code() == 2) - { + if (!injected) { + if (vk_early && !alive() && exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping storm\n"); - } - else - { + } else { check(false, "inject mock game (storm)"); } cleanup(); @@ -876,8 +780,7 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) } Sleep(vk_early ? 2500 : 1000); // let the hook attach + the game start presenting - if (vk_early && !alive() && exit_code() == 2) - { + if (vk_early && !alive() && exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping storm\n"); cleanup(); return; @@ -890,11 +793,9 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) std::atomic stop{false}; std::thread storm([&] { bool off = false; - while (!stop.load(std::memory_order_relaxed)) - { + while (!stop.load(std::memory_order_relaxed)) { off = !off; - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store(off ? 1u : 0u, std::memory_order_release); } Sleep(60); @@ -905,28 +806,24 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) for (int i = 0; i < 170 && !crashed; ++i) // ~10 s of storming { Sleep(60); - if (!alive()) - { + if (!alive()) { crashed = true; } } stop.store(true, std::memory_order_relaxed); storm.join(); - if (crashed) - { + if (crashed) { std::printf(" game CRASHED during the storm (exit 0x%08lX)\n", exit_code()); } check(!crashed, "game survived the hook/unhook storm (no crash)"); - if (crashed) - { + if (crashed) { cleanup(); return; } // Re-enable everything and confirm the game is still alive + the hook still beating. - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store(0, std::memory_order_release); } Sleep(600); @@ -936,8 +833,7 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) // Capture must resume (the rehook works end-to-end). Vulkan can't re-arm after a toggle (its // present pointer was cached at init), so only assert resume for the other backends. - if (!vk_early) - { + if (!vk_early) { SharedTextureSource src; src.init(device); const std::uint64_t frames0 = src.frames_copied(); @@ -946,8 +842,7 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) { Sleep(50); const VideoShareView share = read_video_share(block); - if (src.update(share, pi.dwProcessId) && src.frames_copied() > frames0 + 3) - { + if (src.update(share, pi.dwProcessId) && src.frames_copied() > frames0 + 3) { advanced = true; break; } @@ -963,13 +858,11 @@ void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) std::uint32_t installed_hook_count(const SharedBlock* block) { std::uint32_t count = block->status.hook_entry_count; - if (count > kMaxHookEntries) - { + if (count > kMaxHookEntries) { count = kMaxHookEntries; } std::uint32_t installed = 0; - for (std::uint32_t i = 0; i < count; ++i) - { + for (std::uint32_t i = 0; i < count; ++i) { installed += block->status.hook_entries[i].installed != 0 ? 1u : 0u; } return installed; @@ -983,13 +876,11 @@ void test_graceful_disconnect(const char* backend) { std::printf("== graceful disconnect: %s ==\n", backend); std::wstring wbackend; - for (const char* p = backend; *p != '\0'; ++p) - { + for (const char* p = backend; *p != '\0'; ++p) { wbackend.push_back(static_cast(*p)); } MockGame game = MockGame::launch(wbackend + L" 30"); - if (!game.ok) - { + if (!game.ok) { check(false, "launch mock game (graceful disconnect)"); return; } @@ -999,8 +890,7 @@ void test_graceful_disconnect(const char* backend) // test doesn't depend on an audio endpoint. Then inject. SharedMemory shm; SharedBlock* block = make_ipc(shm, game.pid(), 1u << HookSubsys_Audio); - if (block == nullptr || !inject_retry(game.pid())) - { + if (block == nullptr || !inject_retry(game.pid())) { check(false, "inject mock game (graceful disconnect)"); game.kill(); return; @@ -1017,8 +907,7 @@ void test_graceful_disconnect(const char* backend) const std::uint32_t hb0 = block->status.heartbeat.load(std::memory_order_relaxed); // Graceful disconnect: request every subsystem removed (what request_unhook_all writes). - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store(1u, std::memory_order_release); } @@ -1049,13 +938,11 @@ void test_reconnect(const char* backend) { std::printf("== reconnect to an already-injected DLL: %s ==\n", backend); std::wstring wbackend; - for (const char* p = backend; *p != '\0'; ++p) - { + for (const char* p = backend; *p != '\0'; ++p) { wbackend.push_back(static_cast(*p)); } MockGame game = MockGame::launch(wbackend + L" 30"); - if (!game.ok) - { + if (!game.ok) { check(false, "launch mock game (reconnect)"); return; } @@ -1064,16 +951,14 @@ void test_reconnect(const char* backend) const std::uint32_t disabled = 1u << HookSubsys_Audio; // input+focus+video+mkb on; audio off SharedMemory shm_a; SharedBlock* block_a = make_ipc(shm_a, game.pid(), disabled); - if (block_a == nullptr || !inject_retry(game.pid())) - { + if (block_a == nullptr || !inject_retry(game.pid())) { check(false, "inject mock game (reconnect)"); game.kill(); return; } bool installed = false; - for (int i = 0; i < 100 && game.alive() && !installed; ++i) - { + for (int i = 0; i < 100 && game.alive() && !installed; ++i) { Sleep(50); installed = installed_hook_count(block_a) > 0; } @@ -1082,12 +967,10 @@ void test_reconnect(const char* backend) // Graceful disconnect: unhook everything, then simulate the host going away (drop our handle; // the DLL keeps the section alive). This stands in for both an explicit disconnect and a restart. - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block_a->control.subsystem_disabled[s].store(1u, std::memory_order_release); } - for (int i = 0; i < 100 && installed_hook_count(block_a) != 0; ++i) - { + for (int i = 0; i < 100 && installed_hook_count(block_a) != 0; ++i) { Sleep(50); } check(installed_hook_count(block_a) == 0, "graceful disconnect unhooked the game"); @@ -1103,15 +986,13 @@ void test_reconnect(const char* backend) // ...and reconnect by re-attaching to the SAME section (no re-inject) and re-enabling subsystems. SharedMemory shm_b; SharedBlock* block_b = make_ipc(shm_b, game.pid(), 0u); // re-attach, all subsystems on - if (block_b == nullptr) - { + if (block_b == nullptr) { check(false, "reconnect: re-attach to the section"); game.kill(); return; } bool reinstalled = false; - for (int i = 0; i < 100 && game.alive() && !reinstalled; ++i) - { + for (int i = 0; i < 100 && game.alive() && !reinstalled; ++i) { Sleep(50); reinstalled = installed_hook_count(block_b) > 0; } @@ -1128,8 +1009,7 @@ int main() kill_stray_mock_games(); // clean slate: no leftover game holding coop_hook.dll ID3D11Device* device = make_device(); - if (device == nullptr) - { + if (device == nullptr) { std::printf("No D3D11 device -- skipping mock_game_test.\n"); return 0; } @@ -1174,8 +1054,7 @@ int main() device->Release(); kill_stray_mock_games(); // belt-and-suspenders: ensure nothing is left running - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS mock_game_test\n"); return 0; } diff --git a/tests/opengl_hook_test.cpp b/tests/opengl_hook_test.cpp index c63da96..fd1db4c 100644 --- a/tests/opengl_hook_test.cpp +++ b/tests/opengl_hook_test.cpp @@ -28,13 +28,11 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -42,8 +40,7 @@ void check(bool ok, const char* what) template void release(T*& p) { - if (p) - { + if (p) { p->Release(); p = nullptr; } @@ -64,8 +61,7 @@ int main() { // --- Host side: SharedBlock so the hook's IpcClient connects. --- SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -77,8 +73,7 @@ int main() hook::IpcClient ipc; check(ipc.connect(10, 5), "IPC client connect"); - if (!hook::install_opengl_hooks(ipc)) - { + if (!hook::install_opengl_hooks(ipc)) { std::printf("SKIP: could not install the OpenGL swap hooks\n"); return 0; } @@ -92,8 +87,8 @@ int main() RegisterClassExW(&wc); // WS_POPUP so the client area is exactly kW x kH (an overlapped window can't // shrink below its minimum caption size, which would skew the captured dims). - HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_POPUP, 0, 0, kW, kH, nullptr, nullptr, - wc.hInstance, nullptr); + HWND hwnd = + CreateWindowExW(0, wc.lpszClassName, L"", WS_POPUP, 0, 0, kW, kH, nullptr, nullptr, wc.hInstance, nullptr); ShowWindow(hwnd, SW_SHOWNOACTIVATE); // a mapped window makes the backbuffer reliable HDC hdc = GetDC(hwnd); @@ -105,17 +100,15 @@ int main() pfd.cColorBits = 32; const int pf = ChoosePixelFormat(hdc, &pfd); HGLRC glrc = nullptr; - if (pf == 0 || !SetPixelFormat(hdc, pf, &pfd) || (glrc = wglCreateContext(hdc)) == nullptr || - !wglMakeCurrent(hdc, glrc)) - { + if (pf == 0 || !SetPixelFormat(hdc, pf, &pfd) || (glrc = wglCreateContext(hdc)) == nullptr + || !wglMakeCurrent(hdc, glrc)) { std::printf("SKIP: could not create an OpenGL context on this machine\n"); hook::remove_opengl_hooks(); return 0; } // Clear the backbuffer to a known color, then SwapBuffers (fires the detour). - for (int frame = 0; frame < 3; ++frame) - { + for (int frame = 0; frame < 3; ++frame) { glViewport(0, 0, kW, kH); glClearColor(0.20f, 0.40f, 0.60f, 1.0f); // -> ~{51,102,153,255} glClear(GL_COLOR_BUFFER_BIT); @@ -134,23 +127,19 @@ int main() check(block->video.width == kW && block->video.height == kH, "shared dimensions published"); // --- Consumer: open the shared texture by name, copy to staging, verify color. - if (hook::opengl_frames_shared() > 0) - { + if (hook::opengl_frames_shared() > 0) { ID3D11Device* devB = nullptr; ID3D11DeviceContext* ctxB = nullptr; - if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, - D3D11_SDK_VERSION, &devB, nullptr, &ctxB))) - { + if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, + &devB, nullptr, &ctxB))) { ID3D11Device1* dev1 = nullptr; devB->QueryInterface(IID_PPV_ARGS(&dev1)); const std::wstring name = video_share_name(GetCurrentProcessId()); ID3D11Texture2D* sharedB = nullptr; IDXGIKeyedMutex* km = nullptr; - if (dev1 != nullptr && - SUCCEEDED(dev1->OpenSharedResourceByName(name.c_str(), - DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, - IID_PPV_ARGS(&sharedB)))) - { + if (dev1 != nullptr + && SUCCEEDED(dev1->OpenSharedResourceByName( + name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, IID_PPV_ARGS(&sharedB)))) { sharedB->QueryInterface(IID_PPV_ARGS(&km)); D3D11_TEXTURE2D_DESC sd{}; sharedB->GetDesc(&sd); @@ -160,32 +149,24 @@ int main() sd.MiscFlags = 0; ID3D11Texture2D* staging = nullptr; devB->CreateTexture2D(&sd, nullptr, &staging); - if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) - { + if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) { ctxB->CopyResource(staging, sharedB); km->ReleaseSync(kVideoMutexKey); D3D11_MAPPED_SUBRESOURCE m{}; - if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &m))) - { + if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &m))) { const auto* px = static_cast(m.pData); std::printf("readback pixel0 = {%u,%u,%u,%u}\n", px[0], px[1], px[2], px[3]); check(near_byte(px[0], 51) && near_byte(px[1], 102) && near_byte(px[2], 153), "shared texture carries the rendered color"); ctxB->Unmap(staging, 0); - } - else - { + } else { check(false, "map staging texture"); } - } - else - { + } else { check(false, "acquire keyed mutex + copy shared texture"); } release(staging); - } - else - { + } else { check(false, "open shared texture by name"); } release(km); diff --git a/tests/present_hook_test.cpp b/tests/present_hook_test.cpp index afb122e..78e66fb 100644 --- a/tests/present_hook_test.cpp +++ b/tests/present_hook_test.cpp @@ -28,15 +28,13 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf(" FAIL: %s\n", what); ++g_failures; } @@ -45,8 +43,7 @@ void check(bool ok, const char* what) template void release(T*& p) { - if (p) - { + if (p) { p->Release(); p = nullptr; } @@ -68,8 +65,7 @@ int main() { // --- Host side: SharedBlock (named by our pid) so the hook's IpcClient connects. SharedMemory shm; - if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } @@ -82,8 +78,7 @@ int main() check(ipc.connect(10, 5), "IPC client connect"); // --- Install the Present hook (inline-hooks IDXGISwapChain::Present). --- - if (!hook::install_present_hooks(ipc)) - { + if (!hook::install_present_hooks(ipc)) { std::printf("SKIP: could not install the Present hook (no D3D11 device?)\n"); return 0; } @@ -114,22 +109,18 @@ int main() ID3D11DeviceContext* ctx = nullptr; HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &scd, &swapchain, &device, nullptr, &ctx); - if (FAILED(hr) || swapchain == nullptr) - { + if (FAILED(hr) || swapchain == nullptr) { std::printf("SKIP: could not create a D3D11 swapchain (hr=0x%08lX)\n", static_cast(hr)); hook::remove_present_hooks(); return 0; } // Clear the backbuffer to the known color, then Present (fires the detour). - for (int frame = 0; frame < 3; ++frame) - { + for (int frame = 0; frame < 3; ++frame) { ID3D11Texture2D* back = nullptr; - if (SUCCEEDED(swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&back)))) - { + if (SUCCEEDED(swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&back)))) { ID3D11RenderTargetView* rtv = nullptr; - if (SUCCEEDED(device->CreateRenderTargetView(back, nullptr, &rtv))) - { + if (SUCCEEDED(device->CreateRenderTargetView(back, nullptr, &rtv))) { ctx->ClearRenderTargetView(rtv, kClear); ctx->Flush(); rtv->Release(); @@ -141,8 +132,8 @@ int main() std::printf("present_calls=%llu frames_shared=%llu video{gen=%u %ux%u fmt=%u}\n", static_cast(hook::present_calls()), - static_cast(hook::present_frames_shared()), - block->video.generation.load(), block->video.width, block->video.height, block->video.format); + static_cast(hook::present_frames_shared()), block->video.generation.load(), + block->video.width, block->video.height, block->video.format); check(hook::present_calls() >= 3, "Present detour fired"); check(hook::present_frames_shared() > 0, "backbuffer copied into the shared texture"); @@ -156,19 +147,16 @@ int main() { ID3D11Device* devB = nullptr; ID3D11DeviceContext* ctxB = nullptr; - if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, - D3D11_SDK_VERSION, &devB, nullptr, &ctxB))) - { + if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, + &devB, nullptr, &ctxB))) { ID3D11Device1* dev1 = nullptr; devB->QueryInterface(IID_PPV_ARGS(&dev1)); const std::wstring name = video_share_name(GetCurrentProcessId()); ID3D11Texture2D* sharedB = nullptr; IDXGIKeyedMutex* km = nullptr; - if (dev1 != nullptr && - SUCCEEDED(dev1->OpenSharedResourceByName(name.c_str(), - DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, - IID_PPV_ARGS(&sharedB)))) - { + if (dev1 != nullptr + && SUCCEEDED(dev1->OpenSharedResourceByName( + name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, IID_PPV_ARGS(&sharedB)))) { sharedB->QueryInterface(IID_PPV_ARGS(&km)); D3D11_TEXTURE2D_DESC sd{}; @@ -180,33 +168,25 @@ int main() ID3D11Texture2D* staging = nullptr; check(SUCCEEDED(devB->CreateTexture2D(&sd, nullptr, &staging)), "create staging texture"); - if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) - { + if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) { ctxB->CopyResource(staging, sharedB); km->ReleaseSync(kVideoMutexKey); D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) - { + if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped))) { const auto* px = static_cast(mapped.pData); std::printf("readback pixel0 = {%u,%u,%u,%u}\n", px[0], px[1], px[2], px[3]); check(near_byte(px[0], 51) && near_byte(px[1], 102) && near_byte(px[2], 153), "shared texture carries the rendered color"); ctxB->Unmap(staging, 0); - } - else - { + } else { check(false, "map staging texture"); } - } - else - { + } else { check(false, "acquire keyed mutex + copy shared texture"); } release(staging); - } - else - { + } else { check(false, "open shared texture by name"); } release(km); @@ -222,11 +202,9 @@ int main() { auto render = [&] { ID3D11Texture2D* back = nullptr; - if (SUCCEEDED(swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&back)))) - { + if (SUCCEEDED(swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&back)))) { ID3D11RenderTargetView* rtv = nullptr; - if (SUCCEEDED(device->CreateRenderTargetView(back, nullptr, &rtv))) - { + if (SUCCEEDED(device->CreateRenderTargetView(back, nullptr, &rtv))) { ctx->ClearRenderTargetView(rtv, kClear); ctx->Flush(); rtv->Release(); diff --git a/tests/present_overhead.hpp b/tests/present_overhead.hpp index e106b25..6d64d2f 100644 --- a/tests/present_overhead.hpp +++ b/tests/present_overhead.hpp @@ -10,8 +10,7 @@ #include -namespace cooptest -{ +namespace cooptest { inline double now_ms() { LARGE_INTEGER f, c; @@ -28,8 +27,7 @@ double avg_present_ms(int n, RenderFn render, PresentFn present) render(); present(); // warm (first present/resource setup) double total = 0; - for (int i = 0; i < n; ++i) - { + for (int i = 0; i < n; ++i) { render(); const double a = now_ms(); present(); diff --git a/tests/protocol_test.cpp b/tests/protocol_test.cpp index 9f11fde..fab0620 100644 --- a/tests/protocol_test.cpp +++ b/tests/protocol_test.cpp @@ -16,14 +16,12 @@ using namespace coop; using coop::hook::IpcClient; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -43,11 +41,9 @@ int main() // generations, so the slots' packet values would disagree. std::thread writer([&] { std::uint32_t gen = 1; - while (!stop.load(std::memory_order_relaxed)) - { + while (!stop.load(std::memory_order_relaxed)) { CoopPadState pads[kMaxPads]; - for (auto& p : pads) - { + for (auto& p : pads) { p = CoopPadState{}; p.connected = 1; p.packet = gen; @@ -57,17 +53,13 @@ int main() } }); std::thread reader([&] { - while (!stop.load(std::memory_order_relaxed)) - { + while (!stop.load(std::memory_order_relaxed)) { CoopPadState out[kMaxPads]; std::uint32_t count = 0; - if (read_pads(block, out, count)) - { + if (read_pads(block, out, count)) { reads.fetch_add(1, std::memory_order_relaxed); - for (std::uint32_t i = 1; i < kMaxPads; ++i) - { - if (out[i].packet != out[0].packet) - { + for (std::uint32_t i = 1; i < kMaxPads; ++i) { + if (out[i].packet != out[0].packet) { torn.fetch_add(1, std::memory_order_relaxed); break; } @@ -90,7 +82,8 @@ int main() block.sequence.store(1, std::memory_order_relaxed); // odd = write in progress, never completed CoopPadState out[kMaxPads]; std::uint32_t count = 0; - check(!read_pads(block, out, count), "seqlock: read_pads returns false when stuck mid-write (bounded, no hang)"); + check(!read_pads(block, out, count), + "seqlock: read_pads returns false when stuck mid-write (bounded, no hang)"); } // --- Handshake: IpcClient::connect rejects a wrong magic / version ---------------------------- diff --git a/tests/rate_estimator_test.cpp b/tests/rate_estimator_test.cpp index fe9c180..87a2b48 100644 --- a/tests/rate_estimator_test.cpp +++ b/tests/rate_estimator_test.cpp @@ -13,18 +13,14 @@ using namespace coop::hook; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (!ok) - { + if (!ok) { std::printf("FAIL: %s\n", what); ++g_failures; - } - else - { + } else { std::printf(" ok: %s\n", what); } } @@ -32,8 +28,7 @@ void check(bool ok, const char* what) // Drives an estimator with a controllable QPC clock. One feed per "window" (we use a // 0.5 s step that matches the estimator's window, so each feed after the first completes // exactly one window -- making each window's frame count individually controllable). -struct Sim -{ +struct Sim { RateEstimator est; static constexpr std::int64_t kFreq = 1'000'000; // 1 MHz (microseconds) std::int64_t qpc = 0; @@ -53,11 +48,9 @@ RateEstimate run_steady(double rate, int max = 40) { Sim s; RateEstimate r; - for (int i = 0; i < max; ++i) - { + for (int i = 0; i < max; ++i) { r = s.window(rate); - if (r.done) - { + if (r.done) { return r; } } @@ -78,13 +71,10 @@ int main() check(snap_standard_rate(45000.0) == 0, "45000 (non-standard) snaps to nothing"); // --- steady standard rates converge, confidently ----------------------------- - for (double rate : {44100.0, 48000.0, 96000.0, 22050.0}) - { + for (double rate : {44100.0, 48000.0, 96000.0, 22050.0}) { const RateEstimate r = run_steady(rate); - check(r.done && r.confident && r.rate == static_cast(rate), - "steady rate converges confidently"); - if (!(r.done && r.rate == static_cast(rate))) - { + check(r.done && r.confident && r.rate == static_cast(rate), "steady rate converges confidently"); + if (!(r.done && r.rate == static_cast(rate))) { std::printf(" (rate=%.0f -> done=%d confident=%d got=%u)\n", rate, r.done, r.confident, r.rate); } } @@ -94,8 +84,7 @@ int main() Sim s; RateEstimate r; const double wobble[] = {+150.0, -120.0, +90.0, -150.0, +60.0, -90.0, +130.0, -40.0}; - for (int i = 0; i < 30 && !r.done; ++i) - { + for (int i = 0; i < 30 && !r.done; ++i) { r = s.window(44100.0, wobble[i % 8]); // ~0.3% jitter, within the snap band } check(r.done && r.confident && r.rate == 44100, "44100 with small jitter -> 44100 confident"); @@ -112,8 +101,7 @@ int main() check(!burst.done, "single burst window does not commit"); // windows 3..N: clean 44100 -> consensus on 44100. RateEstimate r = burst; - for (int i = 0; i < 10 && !r.done; ++i) - { + for (int i = 0; i < 10 && !r.done; ++i) { r = s.window(44100.0); } check(r.done && r.confident && r.rate == 44100, "burst rejected; converges to 44100, not 48000"); @@ -124,8 +112,7 @@ int main() const RateEstimate r = run_steady(45000.0, 40); check(r.done && !r.confident, "non-standard 45000 -> done but LOW-confidence"); check(r.rate >= 44600 && r.rate <= 45400, "low-confidence estimate is ~45000"); - if (r.done) - { + if (r.done) { std::printf(" (45000 -> confident=%d rate=%u)\n", r.confident, r.rate); } } @@ -134,20 +121,17 @@ int main() { Sim s; RateEstimate r; - for (int i = 0; i < 6; ++i) - { + for (int i = 0; i < 6; ++i) { r = s.window(0.0); // silent: no frames advance check(!r.done, "idle window never commits"); } - for (int i = 0; i < 12 && !r.done; ++i) - { + for (int i = 0; i < 12 && !r.done; ++i) { r = s.window(48000.0); // audio resumes } check(r.done && r.confident && r.rate == 48000, "after idle, real audio converges to 48000"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS rate_estimator_test\n"); return 0; } diff --git a/tests/render_pacer_test.cpp b/tests/render_pacer_test.cpp index cde0272..30a6ade 100644 --- a/tests/render_pacer_test.cpp +++ b/tests/render_pacer_test.cpp @@ -20,17 +20,13 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (ok) - { + if (ok) { std::printf(" ok: %s\n", what); - } - else - { + } else { std::printf("FAIL: %s\n", what); ++g_failures; } @@ -38,34 +34,29 @@ void check(bool ok, const char* what) // The original policy: re-prime on any partial fill (`to_write < avail`). Kept here only to // contrast against the shipping RenderPacer. -struct LegacyPacer -{ +struct LegacyPacer { std::uint32_t prime_frames = 0; bool primed = false; std::uint32_t pump(std::uint32_t avail, std::uint32_t have, std::uint32_t /*padding*/) { - if (!primed && have >= prime_frames) - { + if (!primed && have >= prime_frames) { primed = true; } - if (!primed) - { + if (!primed) { return 0; } const std::uint32_t to_write = std::min(avail, have); - if (to_write < avail) - { + if (to_write < avail) { primed = false; // the bug: a partial fill forces a full re-prime } return to_write; } }; -struct SimResult -{ - int underruns = 0; // device couldn't supply a full period (audible silence) +struct SimResult { + int underruns = 0; // device couldn't supply a full period (audible silence) std::uint32_t min_headroom = 0; // smallest device-buffer level seen after warm-up (cushion left) - int withheld = 0; // ticks the policy refused to feed though the ring had >=1 period + int withheld = 0; // ticks the policy refused to feed though the ring had >=1 period }; // Run one policy over a producer schedule, modelling an event-driven WASAPI render client. @@ -83,19 +74,14 @@ SimResult simulate(Pacer pacer, const std::vector& producer, std: SimResult res; res.min_headroom = render_frames; bool warming = true; // ignore the initial fill-up before playback starts - for (std::size_t t = 0; t < producer.size(); ++t) - { + for (std::size_t t = 0; t < producer.size(); ++t) { ring += producer[t]; // the game pushed this tick's frames into the ring // The device plays a period; its event then fires asking for more. If the buffer // can't supply a full period, the renderer plays silence -- an audible under-run. - if (!warming) - { - if (device >= period) - { + if (!warming) { + if (device >= period) { device -= period; - } - else - { + } else { ++res.underruns; device = 0; } @@ -109,15 +95,12 @@ SimResult simulate(Pacer pacer, const std::vector& producer, std: w = static_cast(std::min(w, ring)); device += w; ring -= w; - if (w > 0) - { + if (w > 0) { warming = false; // playback has begun } - if (!warming) - { + if (!warming) { res.min_headroom = std::min(res.min_headroom, device); - if (w == 0 && have >= period && avail >= period) - { + if (w == 0 && have >= period && avail >= period) { ++res.withheld; // had at least a period to give and room to put it -- but didn't } } @@ -137,8 +120,7 @@ std::vector jittery_schedule(std::uint32_t period, int ticks) p.reserve(ticks); std::uint64_t owed = 0; const std::uint32_t catch_up = period * 7 / 5; // 1.4x: clears the 2/7 stall backlog, but no faster - for (int t = 0; t < ticks; ++t) - { + for (int t = 0; t < ticks; ++t) { owed += period; const std::uint32_t cap = (t % 7 < 2) ? 0u : catch_up; // stall 2/7 ticks, else catch up gently const std::uint32_t deliver = static_cast(std::min(owed, cap)); @@ -178,10 +160,10 @@ int main() lp.prime_frames = kPrime; const SimResult n = simulate(np, sched, kRender, kPeriod); const SimResult l = simulate(lp, sched, kRender, kPeriod); - std::printf(" (jittery: NEW under-runs=%d min_headroom=%u withheld=%d)\n", n.underruns, - n.min_headroom, n.withheld); - std::printf(" (jittery: OLD under-runs=%d min_headroom=%u withheld=%d)\n", l.underruns, - l.min_headroom, l.withheld); + std::printf(" (jittery: NEW under-runs=%d min_headroom=%u withheld=%d)\n", n.underruns, n.min_headroom, + n.withheld); + std::printf(" (jittery: OLD under-runs=%d min_headroom=%u withheld=%d)\n", l.underruns, l.min_headroom, + l.withheld); // The defining pathology, measured directly (not timing-marginal): the old policy refuses // to feed data it has, repeatedly; the new policy never withholds once playing. check(n.withheld == 0, "jittery: new policy never withholds available data"); @@ -212,8 +194,7 @@ int main() check(!p.primed, "true starvation (padding==0 && have==0) re-primes"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS render_pacer_test\n"); return 0; } diff --git a/tests/shared_memory_test.cpp b/tests/shared_memory_test.cpp index 68b956a..32a0624 100644 --- a/tests/shared_memory_test.cpp +++ b/tests/shared_memory_test.cpp @@ -10,14 +10,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } diff --git a/tests/srgb_format_test.cpp b/tests/srgb_format_test.cpp index 43ceba8..32c8c59 100644 --- a/tests/srgb_format_test.cpp +++ b/tests/srgb_format_test.cpp @@ -9,14 +9,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void expect(DXGI_FORMAT in, DXGI_FORMAT want, const char* what) { const DXGI_FORMAT got = srgb_to_unorm(in); - if (got != want) - { + if (got != want) { std::printf(" FAIL: %s (got %d, want %d)\n", what, static_cast(got), static_cast(want)); ++g_failures; } diff --git a/tests/text_util_test.cpp b/tests/text_util_test.cpp index 2fde92d..2856186 100644 --- a/tests/text_util_test.cpp +++ b/tests/text_util_test.cpp @@ -9,14 +9,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -57,8 +55,7 @@ int main() // Round-trip preserves the string (ASCII and a non-ASCII code point that must survive // persist/reload of an override key -- the reason narrow/widen exist instead of a byte mask). - for (const wchar_t* s : {L"CoopAllTheThings", L"path\\to\\Game (2).exe", L"café.exe", L"游戏.exe"}) - { + for (const wchar_t* s : {L"CoopAllTheThings", L"path\\to\\Game (2).exe", L"café.exe", L"游戏.exe"}) { check(widen(narrow(std::wstring(s))) == std::wstring(s), "narrow->widen round-trips"); } diff --git a/tests/tone_analysis_test.cpp b/tests/tone_analysis_test.cpp index a664883..d7ac8f4 100644 --- a/tests/tone_analysis_test.cpp +++ b/tests/tone_analysis_test.cpp @@ -15,17 +15,13 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { - if (ok) - { + if (ok) { std::printf(" ok: %s\n", what); - } - else - { + } else { std::printf("FAIL: %s\n", what); ++g_failures; } @@ -39,8 +35,7 @@ std::vector make_sine(double freq, unsigned rate, double seconds, double const std::size_t n = static_cast(rate * seconds); std::vector v(n); const double step = kTwoPi * freq / rate; - for (std::size_t i = 0; i < n; ++i) - { + for (std::size_t i = 0; i < n; ++i) { v[i] = static_cast(std::sin(step * i) * amp); } return v; @@ -61,8 +56,8 @@ int main() check(r.glitch_count == 0, "clean: no clicks"); check(r.dropout_count == 0, "clean: no dropouts"); check(std::fabs(r.peak - 0.25) < 0.01, "clean: peak ~0.25"); - std::printf(" (clean: %.3f Hz, %.2f cents, SNR %.1f dB, THD %.3f%%)\n", r.dominant_hz, - r.pitch_error_cents, r.snr_db, r.thd_percent); + std::printf(" (clean: %.3f Hz, %.2f cents, SNR %.1f dB, THD %.3f%%)\n", r.dominant_hz, r.pitch_error_cents, + r.snr_db, r.thd_percent); } // --- Pitch-shift bug: real 44100 samples played as if 48000 ----------------------- @@ -70,28 +65,26 @@ int main() // them at 48 kHz, shifting a 1000 Hz tone up to 1000*48000/44100 ~= 1088.4 Hz. Expected // cents = 1200*log2(48000/44100) ~= +146.7. The analyzer must recover that. { - auto sine = make_sine(1000.0, 44100, 2.0); // generated at the *true* rate + auto sine = make_sine(1000.0, 44100, 2.0); // generated at the *true* rate const ToneReport r = analyze_tone(sine.data(), sine.size(), 48000, 1000.0); // analyzed at the wrong rate const double expect_cents = 1200.0 * std::log2(48000.0 / 44100.0); check(std::fabs(r.pitch_error_cents - expect_cents) < 5.0, "pitch-shift: ~+147 cents detected"); check(r.pitch_error_ratio > 1.05, "pitch-shift: ratio > 1.05 (audibly sharp)"); check(std::fabs(r.dominant_hz - 1088.4) < 3.0, "pitch-shift: dominant ~1088 Hz"); - std::printf(" (pitch-shift: %.2f cents vs expected %.2f, dominant %.2f Hz)\n", - r.pitch_error_cents, expect_cents, r.dominant_hz); + std::printf(" (pitch-shift: %.2f cents vs expected %.2f, dominant %.2f Hz)\n", r.pitch_error_cents, + expect_cents, r.dominant_hz); } // --- Click injection: discontinuities the analyzer must count --------------------- { auto sine = make_sine(1000.0, 48000, 2.0); const unsigned injected = 9; - for (unsigned k = 0; k < injected; ++k) - { + for (unsigned k = 0; k < injected; ++k) { const std::size_t at = sine.size() * (k + 1) / (injected + 2); sine[at] += 0.7f; // a sharp isolated jump (a click) } const ToneReport r = analyze_tone(sine.data(), sine.size(), 48000, 1000.0); - check(r.glitch_count >= injected - 1 && r.glitch_count <= injected + 1, - "clicks: counted ~9 discontinuities"); + check(r.glitch_count >= injected - 1 && r.glitch_count <= injected + 1, "clicks: counted ~9 discontinuities"); check(r.dropout_count == 0, "clicks: no false dropouts"); std::printf(" (clicks: injected %u, detected %u, rate %.2f/s)\n", injected, r.glitch_count, r.glitch_rate_per_sec); @@ -101,11 +94,9 @@ int main() { auto sine = make_sine(1000.0, 48000, 2.0); // Two ~20 ms gaps of silence. - for (int g = 0; g < 2; ++g) - { + for (int g = 0; g < 2; ++g) { const std::size_t at = sine.size() * (g + 1) / 3; - for (std::size_t i = 0; i < 48000u * 20 / 1000; ++i) - { + for (std::size_t i = 0; i < 48000u * 20 / 1000; ++i) { sine[at + i] = 0.0f; } } @@ -129,13 +120,12 @@ int main() const unsigned rate = 48000, ch = 2; auto mono = make_sine(1000.0, rate, 0.5, 0.5); std::vector inter(mono.size() * ch, 0); - for (std::size_t i = 0; i < mono.size(); ++i) - { + for (std::size_t i = 0; i < mono.size(); ++i) { inter[i * ch + 0] = static_cast(mono[i] * 32767.0f); } const std::wstring path = L"tone_analysis_test_roundtrip.wav"; - const bool wrote = wav_write(path, inter.data(), inter.size() * sizeof(std::int16_t), rate, ch, 16, - kToneFormatPcm); + const bool wrote = + wav_write(path, inter.data(), inter.size() * sizeof(std::int16_t), rate, ch, 16, kToneFormatPcm); check(wrote, "wav: write ok"); WavData wd; const bool readback = wav_read(path, wd); @@ -149,8 +139,7 @@ int main() std::remove("tone_analysis_test_roundtrip.wav"); } - if (g_failures == 0) - { + if (g_failures == 0) { std::printf("PASS tone_analysis_test\n"); return 0; } diff --git a/tests/tool_paths_test.cpp b/tests/tool_paths_test.cpp index 915c273..68d9267 100644 --- a/tests/tool_paths_test.cpp +++ b/tests/tool_paths_test.cpp @@ -10,14 +10,12 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -25,8 +23,7 @@ void check(bool ok, const char* what) bool touch(const std::wstring& path) { HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (h == INVALID_HANDLE_VALUE) - { + if (h == INVALID_HANDLE_VALUE) { return false; } CloseHandle(h); @@ -35,8 +32,7 @@ bool touch(const std::wstring& path) std::wstring parent_of(std::wstring dir) // dir has a trailing separator { - if (!dir.empty()) - { + if (!dir.empty()) { dir.pop_back(); } const std::size_t slash = dir.find_last_of(L"\\/"); @@ -62,8 +58,7 @@ int main() check(deployed_artifact_path(near_name.c_str()) == here + near_name, "resolves an artifact next to the exe"); // one directory up (the deployable root) when it isn't next to the exe. - if (have_up) - { + if (have_up) { check(touch(up_dir + up_name), "created a marker one directory up"); check(deployed_artifact_path(up_name.c_str()) == up_dir + up_name, "falls back to the artifact one directory up"); @@ -74,8 +69,7 @@ int main() "missing artifact -> next-to-exe fallback path"); DeleteFileW((here + near_name).c_str()); - if (have_up) - { + if (have_up) { DeleteFileW((up_dir + up_name).c_str()); } diff --git a/tests/ui_fit_test.cpp b/tests/ui_fit_test.cpp index 07d2c57..d16c18d 100644 --- a/tests/ui_fit_test.cpp +++ b/tests/ui_fit_test.cpp @@ -27,8 +27,7 @@ using namespace coop; -namespace -{ +namespace { // Populate the hook back-channel with the worst case for the panels: every pad busy, and // the maximum number of render streams, each low-confidence (the longest provenance label) @@ -38,8 +37,7 @@ void fill_max_status(HookStatusView& st) st.attached = true; st.focus_spoof = true; st.heartbeat = 123456; - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { st.get_state[i] = 9876543; st.get_caps[i] = 4242; st.rumble_left[i] = 65535; @@ -52,8 +50,7 @@ void fill_max_status(HookStatusView& st) st.game_pid = 4242; st.game_hwnd = 0x12345678; st.audio_streams_seen = kMaxAudioStreams + 5; // exercises the "(showing first N)" note - for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) - { + for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) { AudioStreamInfo& s = st.audio_streams[i]; s.is_primary = (i == 0) ? 1u : 0u; s.sample_rate = 192000; @@ -67,8 +64,7 @@ void fill_max_status(HookStatusView& st) void fill_max_input(InputSnapshot& in) { - for (std::uint32_t i = 0; i < kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < kMaxPads; ++i) { in.pads[i].connected = true; in.pads[i].source = "XInput (RPT guest)"; in.pads[i].state.connected = 1; @@ -90,8 +86,7 @@ bool run_size(float w, float h, AudioPanel& audio, ControllersPanel& controllers ImGui::GetIO().DisplaySize = ImVec2(w, h); set_layout_reference(w, h); set_layout_debug(true); // we drive the panels with Debug details on - for (int frame = 0; frame < 4; ++frame) - { + for (int frame = 0; frame < 4; ++frame) { ImGui::GetIO().DeltaTime = 1.0f / 60.0f; ImGui::NewFrame(); reset_panel_fit(); @@ -103,8 +98,8 @@ bool run_size(float w, float h, AudioPanel& audio, ControllersPanel& controllers const bool over = panel_fit_overflow(&ox, &oy); char report[256]; panel_fit_report(report, sizeof(report)); - std::printf(" %4.0fx%-4.0f %-9s %-28s (worst %.0f x, %.0f y)%s\n", w, h, over ? "OVERFLOW" : "fit", - report, ox, oy, (over && !required) ? " [best-effort]" : ""); + std::printf(" %4.0fx%-4.0f %-9s %-28s (worst %.0f x, %.0f y)%s\n", w, h, over ? "OVERFLOW" : "fit", report, ox, + oy, (over && !required) ? " [best-effort]" : ""); return required ? !over : true; } @@ -139,8 +134,7 @@ int main() run_size(1366, 768, audio, controllers, st, in, /*required=*/false); ImGui::DestroyContext(); - if (!ok) - { + if (!ok) { std::printf("FAIL: a panel overflows its assigned size at 1920x1080 with Debug details on.\n"); return 1; } diff --git a/tests/vk_capture_perf_test.cpp b/tests/vk_capture_perf_test.cpp index 95f51b7..e8f5005 100644 --- a/tests/vk_capture_perf_test.cpp +++ b/tests/vk_capture_perf_test.cpp @@ -30,8 +30,7 @@ #include "vk_capture.hpp" -namespace -{ +namespace { double now_ms() { LARGE_INTEGER f, c; @@ -44,15 +43,13 @@ int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } // Everything the test resolves from the device (superset of VkCapture::Fns + image-build helpers). -struct DevFns -{ +struct DevFns { PFN_vkGetDeviceProcAddr GetDeviceProcAddr; PFN_vkGetDeviceQueue GetDeviceQueue; PFN_vkCreateCommandPool CreateCommandPool; @@ -94,10 +91,8 @@ VkPhysicalDeviceMemoryProperties g_memprops{}; bool find_mem(std::uint32_t type_bits, VkMemoryPropertyFlags want, std::uint32_t& out) { - for (std::uint32_t i = 0; i < g_memprops.memoryTypeCount; ++i) - { - if ((type_bits & (1u << i)) && (g_memprops.memoryTypes[i].propertyFlags & want) == want) - { + for (std::uint32_t i = 0; i < g_memprops.memoryTypeCount; ++i) { + if ((type_bits & (1u << i)) && (g_memprops.memoryTypes[i].propertyFlags & want) == want) { out = i; return true; } @@ -146,14 +141,12 @@ int main(int argc, char** argv) const std::uint32_t W = 1920, H = 1080; // the resolution where the stall was measured HMODULE vk = LoadLibraryW(L"vulkan-1.dll"); - if (vk == nullptr) - { + if (vk == nullptr) { std::printf("SKIP vk_capture_perf_test (no vulkan-1.dll)\n"); return 0; } auto gipa = reinterpret_cast(GetProcAddress(vk, "vkGetInstanceProcAddr")); - if (gipa == nullptr) - { + if (gipa == nullptr) { std::printf("SKIP vk_capture_perf_test (no vkGetInstanceProcAddr)\n"); return 0; } @@ -166,8 +159,7 @@ int main(int argc, char** argv) app.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo ci{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ci.pApplicationInfo = &app; - if (create == nullptr || create(&ci, nullptr, &instance) != VK_SUCCESS) - { + if (create == nullptr || create(&ci, nullptr, &instance) != VK_SUCCESS) { std::printf("SKIP vk_capture_perf_test (vkCreateInstance failed)\n"); return 0; } @@ -182,8 +174,7 @@ int main(int argc, char** argv) std::uint32_t n = 0; EnumeratePhysicalDevices(instance, &n, nullptr); - if (n == 0) - { + if (n == 0) { std::printf("SKIP vk_capture_perf_test (no physical devices)\n"); DestroyInstance(instance, nullptr); return 0; @@ -197,16 +188,13 @@ int main(int argc, char** argv) std::vector qf(qn); GetPhysicalDeviceQueueFamilyProperties(gpu, &qn, qf.data()); std::uint32_t qfam = UINT32_MAX; - for (std::uint32_t i = 0; i < qn; ++i) - { - if (qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) - { + for (std::uint32_t i = 0; i < qn; ++i) { + if (qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { qfam = i; break; } } - if (qfam == UINT32_MAX) - { + if (qfam == UINT32_MAX) { std::printf("SKIP vk_capture_perf_test (no graphics queue)\n"); DestroyInstance(instance, nullptr); return 0; @@ -221,8 +209,7 @@ int main(int argc, char** argv) dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &qci; VkDevice device = VK_NULL_HANDLE; - if (CreateDevice(gpu, &dci, nullptr, &device) != VK_SUCCESS) - { + if (CreateDevice(gpu, &dci, nullptr, &device) != VK_SUCCESS) { std::printf("SKIP vk_capture_perf_test (vkCreateDevice failed)\n"); DestroyInstance(instance, nullptr); return 0; @@ -264,8 +251,8 @@ int main(int argc, char** argv) d.UnmapMemory = DFN(UnmapMemory); d.InvalidateMappedMemoryRanges = DFN(InvalidateMappedMemoryRanges); d.DeviceWaitIdle = DFN(DeviceWaitIdle); - d.GetPhysicalDeviceMemoryProperties = - reinterpret_cast(gipa(instance, "vkGetPhysicalDeviceMemoryProperties")); + d.GetPhysicalDeviceMemoryProperties = reinterpret_cast( + gipa(instance, "vkGetPhysicalDeviceMemoryProperties")); d.GetPhysicalDeviceMemoryProperties(gpu, &g_memprops); @@ -300,13 +287,11 @@ int main(int argc, char** argv) // --- Build the known source image (BGRA gradient) in PRESENT_SRC layout ------------------------ const VkDeviceSize bytes = static_cast(W) * H * 4; std::vector gradient(bytes); - for (std::uint32_t y = 0; y < H; ++y) - { - for (std::uint32_t x = 0; x < W; ++x) - { + for (std::uint32_t y = 0; y < H; ++y) { + for (std::uint32_t x = 0; x < W; ++x) { unsigned char* p = &gradient[(static_cast(y) * W + x) * 4]; - p[0] = static_cast(x & 0xFF); // B - p[1] = static_cast(y & 0xFF); // G + p[0] = static_cast(x & 0xFF); // B + p[1] = static_cast(y & 0xFF); // G p[2] = static_cast((x + y) & 0xFF); // R p[3] = 255; } @@ -347,8 +332,7 @@ int main(int argc, char** argv) ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - if (d.CreateImage(device, &ici, nullptr, &img) != VK_SUCCESS) - { + if (d.CreateImage(device, &ici, nullptr, &img) != VK_SUCCESS) { std::printf("SKIP vk_capture_perf_test (CreateImage failed)\n"); return 0; } @@ -362,8 +346,7 @@ int main(int argc, char** argv) d.AllocateMemory(device, &mai, nullptr, &imgmem); d.BindImageMemory(device, img, imgmem, 0); } - auto barrier = [&](VkCommandBuffer c, VkImageLayout from, VkImageLayout to, VkAccessFlags src, - VkAccessFlags dst) { + auto barrier = [&](VkCommandBuffer c, VkImageLayout from, VkImageLayout to, VkAccessFlags src, VkAccessFlags dst) { VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.srcAccessMask = src; b.dstAccessMask = dst; @@ -373,8 +356,8 @@ int main(int argc, char** argv) b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.image = img; b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - d.CmdPipelineBarrier(c, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, - nullptr, 0, nullptr, 1, &b); + d.CmdPipelineBarrier(c, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, + 0, nullptr, 1, &b); }; { VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; @@ -385,8 +368,8 @@ int main(int argc, char** argv) r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; r.imageExtent = {W, H, 1}; d.CmdCopyBufferToImage(cb, upbuf, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r); - barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT); + barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_MEMORY_READ_BIT); d.EndCommandBuffer(cb); submit_wait(cb); } @@ -417,24 +400,22 @@ int main(int argc, char** argv) bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; d.ResetCommandBuffer(cb, 0); d.BeginCommandBuffer(cb, &bi); - barrier(cb, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); + barrier(cb, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_READ_BIT, + VK_ACCESS_TRANSFER_READ_BIT); VkBufferImageCopy r{}; r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; r.imageExtent = {W, H, 1}; d.CmdCopyImageToBuffer(cb, img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ref_buf, 1, &r); - barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_READ_BIT); + barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_MEMORY_READ_BIT); d.EndCommandBuffer(cb); submit_wait(cb); const auto* src = static_cast(ref_mapped); const size_t row = static_cast(W) * 4; - for (std::uint32_t y = 0; y < H; ++y) - { + for (std::uint32_t y = 0; y < H; ++y) { const unsigned char* in = src + static_cast(y) * row; unsigned char* o = ref_rgba.data() + static_cast(y) * row; - for (std::uint32_t x = 0; x < W; ++x) - { + for (std::uint32_t x = 0; x < 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]; @@ -448,8 +429,7 @@ int main(int argc, char** argv) sync_capture(); // warm double sync_ms = 0; const int iters = 8; - for (int i = 0; i < iters; ++i) - { + for (int i = 0; i < iters; ++i) { const double t0 = now_ms(); sync_capture(); sync_ms += now_ms() - t0; @@ -460,8 +440,7 @@ int main(int argc, char** argv) double sut_ms = sync_ms; // in --sync repro mode the measured path IS the synchronous one bool image_ok = true; - if (!sync_repro) - { + if (!sync_repro) { // --- The fix under test: VkCapture (async reaper) ----------------------------------------- coop::hook::VkCapture cap; cap.init(gpu, device, qfam, capture_fns(d), GetCurrentProcessId(), nullptr); @@ -477,11 +456,9 @@ int main(int argc, char** argv) }; // Warm up (first calls allocate staging / create the D3D texture on the reaper). - for (int i = 0; i < 16; ++i) - { + for (int i = 0; i < 16; ++i) { VkSemaphore sem = VK_NULL_HANDLE; - if (cap.present(img, VK_FORMAT_B8G8R8A8_UNORM, W, H, nullptr, 0, sem)) - { + if (cap.present(img, VK_FORMAT_B8G8R8A8_UNORM, W, H, nullptr, 0, sem)) { consume(sem); } Sleep(2); @@ -490,14 +467,12 @@ int main(int argc, char** argv) int queued = 0; double t = 0; const int loop = 240; - for (int i = 0; i < loop; ++i) - { + for (int i = 0; i < loop; ++i) { VkSemaphore sem = VK_NULL_HANDLE; const double t0 = now_ms(); const bool did = cap.present(img, VK_FORMAT_B8G8R8A8_UNORM, W, H, nullptr, 0, sem); t += now_ms() - t0; - if (did) - { + if (did) { consume(sem); ++queued; } @@ -511,12 +486,9 @@ int main(int argc, char** argv) Sleep(50); std::vector got; std::uint32_t gw = 0, gh = 0; - if (cap.last_frame(got, gw, gh) && gw == W && gh == H) - { + if (cap.last_frame(got, gw, gh) && gw == W && gh == H) { image_ok = std::memcmp(got.data(), ref_rgba.data(), bytes) == 0; - } - else - { + } else { image_ok = false; } check(cap.frames_published() > 0, "VkCapture published frames"); diff --git a/tests/wav_test.cpp b/tests/wav_test.cpp index 58b58ef..31a887e 100644 --- a/tests/wav_test.cpp +++ b/tests/wav_test.cpp @@ -13,15 +13,13 @@ using namespace coop; -namespace -{ +namespace { int g_failures = 0; int g_counter = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++g_failures; } } @@ -30,13 +28,11 @@ std::wstring write_temp(const std::vector& bytes) { wchar_t dir[MAX_PATH] = {}; GetTempPathW(MAX_PATH, dir); - std::wstring path = std::wstring(dir) + L"coop_wavtest_" + std::to_wstring(GetCurrentProcessId()) + L"_" + - std::to_wstring(g_counter++) + L".wav"; + std::wstring path = std::wstring(dir) + L"coop_wavtest_" + std::to_wstring(GetCurrentProcessId()) + L"_" + + std::to_wstring(g_counter++) + L".wav"; FILE* f = nullptr; - if (_wfopen_s(&f, path.c_str(), L"wb") == 0 && f != nullptr) - { - if (!bytes.empty()) - { + if (_wfopen_s(&f, path.c_str(), L"wb") == 0 && f != nullptr) { + if (!bytes.empty()) { std::fwrite(bytes.data(), 1, bytes.size(), f); } std::fclose(f); @@ -52,10 +48,9 @@ void put4(std::vector& b, const char* s) // Build a WAV where the `data` chunk's declared size can differ from the bytes actually appended // (declared_data_size < 0 means "use the real size"), and an optional junk chunk can be inserted // before `data` with a chosen size field (to exercise the chunk walk). -std::vector build_wav(std::uint16_t tag, std::uint16_t channels, std::uint32_t rate, - std::uint16_t bits, const std::vector& data, - long long declared_data_size = -1, const char* junk_id = nullptr, - std::uint32_t junk_size = 0) +std::vector build_wav(std::uint16_t tag, std::uint16_t channels, std::uint32_t rate, std::uint16_t bits, + const std::vector& data, long long declared_data_size = -1, + const char* junk_id = nullptr, std::uint32_t junk_size = 0) { std::vector b; put4(b, "RIFF"); @@ -69,19 +64,17 @@ std::vector build_wav(std::uint16_t tag, std::uint16_t channels, s detail::wav_put_u32(b, rate * channels * (bits / 8)); detail::wav_put_u16(b, static_cast(channels * (bits / 8))); detail::wav_put_u16(b, bits); - if (junk_id != nullptr) - { + if (junk_id != nullptr) { put4(b, junk_id); detail::wav_put_u32(b, junk_size); b.insert(b.end(), junk_size, 0xAB); // junk body - if (junk_size & 1) - { + if (junk_size & 1) { b.push_back(0); // RIFF pads odd chunks to an even boundary (what the reader's & 1 skips) } } put4(b, "data"); detail::wav_put_u32(b, declared_data_size < 0 ? static_cast(data.size()) - : static_cast(declared_data_size)); + : static_cast(declared_data_size)); b.insert(b.end(), data.begin(), data.end()); return b; } diff --git a/tools/audio_probe/main.cpp b/tools/audio_probe/main.cpp index db58b5e..4568ae8 100644 --- a/tools/audio_probe/main.cpp +++ b/tools/audio_probe/main.cpp @@ -25,8 +25,7 @@ #include "coop/shared_memory.hpp" #include "coop/tool_paths.hpp" -namespace -{ +namespace { // coop_hook.dll ships in the deployable bin// root; this probe runs from // bin//tools/, so resolve next-to-self first, then one level up. @@ -47,9 +46,8 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) { const std::wstring helper = sibling_of(dll_path, L"coop_inject_x86.exe"); const std::wstring x86_dll = sibling_of(dll_path, L"coop_hook_x86.dll"); - if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES || - GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES + || GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: x86 helper/dll missing next to the probe.\n"); return false; } @@ -58,8 +56,7 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; - if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) - { + if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { std::printf("ERROR: CreateProcess(coop_inject_x86) failed (%lu).\n", GetLastError()); return false; } @@ -68,8 +65,7 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) GetExitCodeProcess(pi.hProcess, &code); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - if (code != 0) - { + if (code != 0) { std::printf("ERROR: coop_inject_x86 reported failure (exit %lu).\n", code); return false; } @@ -78,37 +74,32 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) bool inject(unsigned long pid, const std::wstring& dll_path) { - if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: coop_hook.dll not found at the probe's directory.\n"); return false; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { std::printf("ERROR: OpenProcess(%lu) failed (%lu). Run as administrator?\n", pid, GetLastError()); return false; } // 32-bit target -> delegate to the x86 helper (a 64-bit process can't inject it). USHORT proc_machine = IMAGE_FILE_MACHINE_UNKNOWN, native_machine = IMAGE_FILE_MACHINE_UNKNOWN; - if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) - { + if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) { CloseHandle(process); return inject_via_helper(pid, dll_path); } const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) - { - auto load_library = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) { + auto load_library = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr); - if (thread != nullptr) - { + if (thread != nullptr) { WaitForSingleObject(thread, INFINITE); DWORD exit_code = 0; GetExitCodeThread(thread, &exit_code); @@ -116,13 +107,11 @@ bool inject(unsigned long pid, const std::wstring& dll_path) ok = (exit_code != 0); } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); - if (!ok) - { + if (!ok) { std::printf("ERROR: injection failed (%lu).\n", GetLastError()); } return ok; @@ -132,8 +121,7 @@ bool inject(unsigned long pid, const std::wstring& dll_path) int wmain(int argc, wchar_t** argv) { - if (argc < 2) - { + if (argc < 2) { std::printf("usage: coop_audio_probe [seconds] [ring_delay_ms]\n" " ring_delay_ms: how long after injecting to create the audio ring\n" " (default 1500 = reproduces the real app, which creates the ring\n" @@ -144,16 +132,14 @@ int wmain(int argc, wchar_t** argv) const int seconds = (argc >= 3) ? std::max(1, _wtoi(argv[2])) : 20; const int ring_delay_ms = (argc >= 4) ? std::max(0, _wtoi(argv[3])) : 1500; const bool audio_enabled = (argc >= 5) ? _wtoi(argv[4]) != 0 : true; // arg5=0 tests unhooking audio - if (pid == 0) - { + if (pid == 0) { std::printf("ERROR: invalid pid.\n"); return 1; } // 1) Input SharedBlock (the hook's worker exits if it can't connect to this). coop::SharedMemory ipc; - if (!ipc.create(coop::shared_memory_name(pid), sizeof(coop::SharedBlock))) - { + if (!ipc.create(coop::shared_memory_name(pid), sizeof(coop::SharedBlock))) { std::printf("ERROR: create input mapping failed (%lu).\n", GetLastError()); return 1; } @@ -161,8 +147,7 @@ int wmain(int argc, wchar_t** argv) block->version = coop::kProtocolVersion; block->pad_count = 0; block->sequence.store(0, std::memory_order_relaxed); - if (!audio_enabled) - { + if (!audio_enabled) { // Request the hook NOT install the audio subsystem (control-channel test). block->control.subsystem_disabled[coop::HookSubsys_Audio].store(1, std::memory_order_release); std::printf("Audio subsystem requested OFF (control channel test).\n"); @@ -173,8 +158,7 @@ int wmain(int argc, wchar_t** argv) coop::SharedMemory log_shm; coop::LogRing* log_ring = nullptr; std::uint64_t log_cursor = 0; - if (log_shm.create(coop::log_ring_name(pid), coop::log_ring_total_size(coop::kLogCapacity))) - { + if (log_shm.create(coop::log_ring_name(pid), coop::log_ring_total_size(coop::kLogCapacity))) { log_ring = log_shm.as(); coop::log_ring_init(*log_ring, coop::kLogCapacity); } @@ -182,13 +166,11 @@ int wmain(int argc, wchar_t** argv) // Enable the hook's file trace (%TEMP%\coop_hook.log) for this debug session. { wchar_t dir[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, dir) != 0) - { + if (GetTempPathW(MAX_PATH, dir) != 0) { const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on"; - HANDLE h = CreateFileW(sentinel.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, - OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (h != INVALID_HANDLE_VALUE) - { + HANDLE h = CreateFileW(sentinel.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (h != INVALID_HANDLE_VALUE) { CloseHandle(h); } } @@ -201,9 +183,7 @@ int wmain(int argc, wchar_t** argv) coop::SharedMemory ring_shm; coop::AudioRingHeader* ring = nullptr; auto create_ring = [&]() -> bool { - if (!ring_shm.create(coop::audio_ring_name(pid), - coop::audio_ring_total_size(coop::kAudioRingCapacity))) - { + if (!ring_shm.create(coop::audio_ring_name(pid), coop::audio_ring_total_size(coop::kAudioRingCapacity))) { std::printf("ERROR: create audio ring mapping failed (%lu).\n", GetLastError()); return false; } @@ -213,24 +193,20 @@ int wmain(int argc, wchar_t** argv) return true; }; - if (ring_delay_ms == 0 && !create_ring()) - { + if (ring_delay_ms == 0 && !create_ring()) { return 1; } // Inject. std::printf("Injecting coop_hook.dll into pid %lu ...\n", pid); - if (!inject(pid, dll_path_next_to_self())) - { + if (!inject(pid, dll_path_next_to_self())) { return 1; } - if (ring_delay_ms > 0) - { + if (ring_delay_ms > 0) { std::printf("Injected. Creating audio ring %d ms later (app-ordering)...\n", ring_delay_ms); Sleep(static_cast(ring_delay_ms)); - if (!create_ring()) - { + if (!create_ring()) { return 1; } } @@ -241,15 +217,13 @@ int wmain(int argc, wchar_t** argv) const coop::HookStatus& status = block->status; std::uint64_t prev_frames[coop::kMaxAudioStreams] = {}; std::vector drain(coop::kAudioRingCapacity); - for (int t = 0; t < seconds * 2; ++t) - { + for (int t = 0; t < seconds * 2; ++t) { Sleep(500); // If audio started disabled, re-enable it at the midpoint to demonstrate // runtime hooking ("hook with a button press"): the worker should install // the audio hooks and capture should start within a tick or two. - if (!audio_enabled && t == seconds) - { + if (!audio_enabled && t == seconds) { block->control.subsystem_disabled[coop::HookSubsys_Audio].store(0, std::memory_order_release); std::printf(">>> re-enabling audio subsystem at runtime <<<\n"); } @@ -257,26 +231,19 @@ int wmain(int argc, wchar_t** argv) // Consume everything available and find the peak sample magnitude. double peak = 0.0; std::uint32_t got = 0; - while ((got = coop::audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) - { - if (ring->format_tag == 3 /*IEEE_FLOAT*/ && ring->bits == 32) - { + while ((got = coop::audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { + if (ring->format_tag == 3 /*IEEE_FLOAT*/ && ring->bits == 32) { const auto* f = reinterpret_cast(drain.data()); - for (std::uint32_t i = 0; i < got / 4; ++i) - { + for (std::uint32_t i = 0; i < got / 4; ++i) { peak = std::max(peak, static_cast(std::abs(f[i]))); } - } - else if (ring->bits == 16) - { + } else if (ring->bits == 16) { const auto* s = reinterpret_cast(drain.data()); - for (std::uint32_t i = 0; i < got / 2; ++i) - { + for (std::uint32_t i = 0; i < got / 2; ++i) { peak = std::max(peak, std::abs(s[i]) / 32768.0); } } - if (got < drain.size()) - { + if (got < drain.size()) { break; } } @@ -291,35 +258,30 @@ int wmain(int argc, wchar_t** argv) std::printf("[%4.1fs] hb=%u streams=%u peak=%.4f ring{fmt=%d %uHz/%uch/%ubit produced=%llu " "overruns=%llu} video{present=%llu gen=%u %ux%u}\n", - (t + 1) * 0.5, heartbeat, streams, peak, fmt_ready ? 1 : 0, ring->sample_rate, - ring->channels, ring->bits, static_cast(produced), - static_cast(overruns), static_cast(vpresent), - vgen, block->video.width, block->video.height); - for (std::uint32_t i = 0; i < coop::kMaxAudioStreams && i < streams; ++i) - { + (t + 1) * 0.5, heartbeat, streams, peak, fmt_ready ? 1 : 0, ring->sample_rate, ring->channels, + ring->bits, static_cast(produced), static_cast(overruns), + static_cast(vpresent), vgen, block->video.width, block->video.height); + for (std::uint32_t i = 0; i < coop::kMaxAudioStreams && i < streams; ++i) { const coop::AudioStreamInfo& s = status.audio_streams[i]; const bool live = s.frames_rendered > prev_frames[i]; prev_frames[i] = s.frames_rendered; std::printf(" stream %u %s %uHz/%uch/%ubit tag=%u frames=%llu %s\n", i, - s.is_primary ? "PRIMARY" : "extra ", s.sample_rate, s.channels, s.bits, - s.format_tag, static_cast(s.frames_rendered), - live ? "" : ""); + s.is_primary ? "PRIMARY" : "extra ", s.sample_rate, s.channels, s.bits, s.format_tag, + static_cast(s.frames_rendered), live ? "" : ""); } } // Dump the hook registry so the installed-hooks list can be verified headless. static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video", "MKB"}; std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count); - for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i) - { + for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i) { const coop::HookEntry& e = status.hook_entries[i]; std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 5 ? kSubsys[e.subsystem] : "?", e.name, e.installed ? "ON " : "off", static_cast(e.calls)); } // Drain the IPC log ring to verify the hook streams its logs to the host. - if (log_ring != nullptr) - { + if (log_ring != nullptr) { std::printf("\nHook log (streamed over IPC):\n"); coop::log_ring_drain(*log_ring, log_cursor, [](const coop::LogRecord& rec) { std::printf(" %s\n", rec.text); }); diff --git a/tools/audio_tone/main.cpp b/tools/audio_tone/main.cpp index 7e61d90..f655926 100644 --- a/tools/audio_tone/main.cpp +++ b/tools/audio_tone/main.cpp @@ -22,45 +22,37 @@ int wmain(int argc, wchar_t** argv) const double freq = (argc > 2) ? _wtof(argv[2]) : 440.0; coop::tone::ToneFormat tf; - if (argc > 3) - { + if (argc > 3) { tf.rate = static_cast(_wtoi(argv[3])); } - if (argc > 4) - { + if (argc > 4) { tf.channels = static_cast(_wtoi(argv[4])); } - if (argc > 5) - { + if (argc > 5) { tf.bits = static_cast(_wtoi(argv[5])); } tf.is_float = (argc > 6) ? (_wcsicmp(argv[6], L"float") == 0) : (tf.bits == 32); // 32-bit -> float default - if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) - { + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { std::fprintf(stderr, "CoInitializeEx failed\n"); return 1; } int rc = 1; coop::tone::ToneSource tone; - if (tone.open(tf, freq)) - { + if (tone.open(tf, freq)) { const coop::tone::ToneFormat& f = tone.format(); std::printf("TONE_RENDERING pid=%lu %.0fHz %uHz %uch %ubit %s\n", GetCurrentProcessId(), freq, f.rate, f.channels, f.bits, f.is_float ? "float" : "pcm"); std::fflush(stdout); const DWORD end_tick = GetTickCount() + static_cast(seconds * 1000.0); - while (GetTickCount() < end_tick) - { + while (GetTickCount() < end_tick) { tone.render_step(200); } tone.close(); rc = 0; - } - else - { + } else { std::fprintf(stderr, "TONE_OPEN_FAILED (endpoint or format unavailable)\n"); } diff --git a/tools/audio_tone/tone_source.hpp b/tools/audio_tone/tone_source.hpp index a7d5ed3..d36af5b 100644 --- a/tools/audio_tone/tone_source.hpp +++ b/tools/audio_tone/tone_source.hpp @@ -13,52 +13,42 @@ #include #include -namespace coop::tone -{ +namespace coop::tone { inline constexpr double kTwoPi = 6.283185307179586; // A field left 0 resolves to the device mix format's value (so {} = play at the device // format). `is_float` only applies when `bits` is set (16 -> PCM, 32 -> float by default). -struct ToneFormat -{ +struct ToneFormat { unsigned rate = 0; unsigned channels = 0; unsigned bits = 0; bool is_float = false; }; -class ToneSource -{ -public: - ~ToneSource() - { - close(); - } +class ToneSource { + public: + ~ToneSource() { close(); } // Open + start a render client at `want` (0 fields resolve to the device mix format, // AUTOCONVERTPCM lets a non-device format be rendered). Returns false if the endpoint // or that specific format isn't available (the caller treats that as a per-format skip). bool open(const ToneFormat& want, double freq_hz = 440.0) { - if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enum_)))) - { + if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&enum_)))) { return false; } - if (FAILED(enum_->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint_))) - { + if (FAILED(enum_->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint_))) { return false; } - if (FAILED(endpoint_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(&client_)))) - { + if (FAILED( + endpoint_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&client_)))) { return false; } WAVEFORMATEX* mix = nullptr; - if (FAILED(client_->GetMixFormat(&mix)) || mix == nullptr) - { + if (FAILED(client_->GetMixFormat(&mix)) || mix == nullptr) { return false; } resolve_format(want, mix); @@ -72,15 +62,13 @@ public: constexpr REFERENCE_TIME kBuffer = 30 * 10000; // 30 ms // AUTOCONVERTPCM makes a shared-mode client render a non-device format (the audio // engine resamples to the endpoint), exactly like the games that need rate detection. - const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | - AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; - if (FAILED(client_->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kBuffer, 0, fmt, nullptr))) - { + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM + | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + if (FAILED(client_->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kBuffer, 0, fmt, nullptr))) { return false; } client_->SetEventHandle(event_); - if (FAILED(client_->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render_)))) - { + if (FAILED(client_->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render_)))) { return false; } client_->GetBufferSize(&buffer_frames_); @@ -90,8 +78,7 @@ public: // so a downstream test can *recover* the channel count by correlation (identical channels // are ambiguous: 2ch@R looks like 1ch@2R). Off by default -> the usual single-tone source. char d[2] = {}; - if (GetEnvironmentVariableA("COOP_TONE_DISTINCT_CH", d, sizeof(d)) > 0 && d[0] == '1') - { + if (GetEnvironmentVariableA("COOP_TONE_DISTINCT_CH", d, sizeof(d)) > 0 && d[0] == '1') { distinct_ = true; } write(buffer_frames_); // pre-roll @@ -103,54 +90,43 @@ public: // timeout/error (the caller keeps looping on its own wall clock). bool render_step(DWORD timeout_ms) { - if (render_ == nullptr) - { + if (render_ == nullptr) { return false; } - if (WaitForSingleObject(event_, timeout_ms) != WAIT_OBJECT_0) - { + if (WaitForSingleObject(event_, timeout_ms) != WAIT_OBJECT_0) { return false; } UINT32 padding = 0; - if (FAILED(client_->GetCurrentPadding(&padding))) - { + if (FAILED(client_->GetCurrentPadding(&padding))) { return false; } write(buffer_frames_ - padding); return true; } - const ToneFormat& format() const - { - return fmt_; - } - bool is_open() const - { - return render_ != nullptr; - } + const ToneFormat& format() const { return fmt_; } + bool is_open() const { return render_ != nullptr; } void close() { - if (client_) - { + if (client_) { client_->Stop(); } rel(render_); rel(client_); rel(endpoint_); rel(enum_); - if (event_) - { + if (event_) { CloseHandle(event_); event_ = nullptr; } } -private: - template static void rel(T*& p) + private: + template + static void rel(T*& p) { - if (p) - { + if (p) { p->Release(); p = nullptr; } @@ -160,18 +136,15 @@ private: { fmt_.rate = want.rate ? want.rate : mix->nSamplesPerSec; fmt_.channels = want.channels ? want.channels : mix->nChannels; - if (want.bits) - { + if (want.bits) { fmt_.bits = want.bits; fmt_.is_float = want.is_float; - } - else - { + } else { fmt_.bits = mix->wBitsPerSample; - fmt_.is_float = - mix->wFormatTag == WAVE_FORMAT_IEEE_FLOAT || - (mix->wFormatTag == WAVE_FORMAT_EXTENSIBLE && - reinterpret_cast(mix)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); + fmt_.is_float = mix->wFormatTag == WAVE_FORMAT_IEEE_FLOAT + || (mix->wFormatTag == WAVE_FORMAT_EXTENSIBLE + && reinterpret_cast(mix)->SubFormat + == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); } float_ = fmt_.is_float; } @@ -184,13 +157,11 @@ private: wfx.Format.wBitsPerSample = static_cast(fmt_.bits); wfx.Format.nBlockAlign = block; wfx.Format.nAvgBytesPerSec = block * fmt_.rate; - if (fmt_.channels > 2 || fmt_.bits > 16) - { + if (fmt_.channels > 2 || fmt_.bits > 16) { wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; wfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); wfx.Samples.wValidBitsPerSample = static_cast(fmt_.bits); - switch (fmt_.channels) - { + switch (fmt_.channels) { case 6: wfx.dwChannelMask = 0x3F; break; @@ -202,9 +173,7 @@ private: break; } wfx.SubFormat = float_ ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; - } - else - { + } else { wfx.Format.wFormatTag = float_ ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM; wfx.Format.cbSize = 0; } @@ -213,37 +182,28 @@ private: void write(UINT32 frames) { BYTE* data = nullptr; - if (frames == 0 || render_ == nullptr || FAILED(render_->GetBuffer(frames, &data))) - { + if (frames == 0 || render_ == nullptr || FAILED(render_->GetBuffer(frames, &data))) { return; } - for (UINT32 i = 0; i < frames; ++i) - { + for (UINT32 i = 0; i < frames; ++i) { const double s = std::sin(phase_) * 0.25; // -12 dB, gentle phase_ += step_; - if (phase_ > kTwoPi) - { + if (phase_ > kTwoPi) { phase_ -= kTwoPi; } - for (unsigned c = 0; c < fmt_.channels; ++c) - { + for (unsigned c = 0; c < fmt_.channels; ++c) { double sc = s; - if (distinct_ && c < 8) - { + if (distinct_ && c < 8) { // Each channel at its own frequency scale -> genuinely different content. sc = std::sin(phase_c_[c]) * 0.25; phase_c_[c] += step_ * (1.0 + 0.37 * static_cast(c)); - if (phase_c_[c] > kTwoPi) - { + if (phase_c_[c] > kTwoPi) { phase_c_[c] -= kTwoPi; } } - if (float_) - { + if (float_) { reinterpret_cast(data)[i * fmt_.channels + c] = static_cast(sc); - } - else - { + } else { reinterpret_cast(data)[i * fmt_.channels + c] = static_cast(sc * 32767.0); } } @@ -261,8 +221,8 @@ private: bool float_ = false; double phase_ = 0.0; double step_ = 0.0; - bool distinct_ = false; // per-channel distinct content (recoverable channel count) - double phase_c_[8] = {}; // per-channel phase when distinct_ + bool distinct_ = false; // per-channel distinct content (recoverable channel count) + double phase_c_[8] = {}; // per-channel phase when distinct_ }; } // namespace coop::tone diff --git a/tools/audio_validate/main.cpp b/tools/audio_validate/main.cpp index 5eed7b0..2f465c3 100644 --- a/tools/audio_validate/main.cpp +++ b/tools/audio_validate/main.cpp @@ -42,23 +42,21 @@ #include "coop/wav.hpp" #include "tone_source.hpp" // in-process sine renderer (shared with coop_tone), for --selfcheck -namespace -{ +namespace { -struct Options -{ - unsigned long pid = 0; // attach to this pid instead of spawning coop_tone - unsigned long listen = 0; // --listen: passively loopback-capture this pid's output (e.g. coop_host) - double freq = 1000.0; // the tone frequency (for pitch analysis) - unsigned rate = 44100; // tone render rate (the Brotato/Godot non-device case by default) +struct Options { + unsigned long pid = 0; // attach to this pid instead of spawning coop_tone + unsigned long listen = 0; // --listen: passively loopback-capture this pid's output (e.g. coop_host) + double freq = 1000.0; // the tone frequency (for pitch analysis) + unsigned rate = 44100; // tone render rate (the Brotato/Godot non-device case by default) unsigned channels = 2; - unsigned bits = 32; // 32 = float, 16 = pcm - int seconds = 6; // capture duration - bool render = false; // --render: measure the host RENDER path (run_hooked), not just capture - bool baseline = false; // --baseline: loopback-capture the tone directly (no hook/mirror) as a floor - bool selfcheck = false; // --selfcheck: render a clean tone in-process + self-capture (control for self-capture) - std::wstring wav_in; // analyze this .wav instead of capturing - std::wstring wav_out; // where to dump the captured audio (default next to the exe) + unsigned bits = 32; // 32 = float, 16 = pcm + int seconds = 6; // capture duration + bool render = false; // --render: measure the host RENDER path (run_hooked), not just capture + bool baseline = false; // --baseline: loopback-capture the tone directly (no hook/mirror) as a floor + bool selfcheck = false; // --selfcheck: render a clean tone in-process + self-capture (control for self-capture) + std::wstring wav_in; // analyze this .wav instead of capturing + std::wstring wav_out; // where to dump the captured audio (default next to the exe) }; std::wstring sibling(const std::wstring& path, const wchar_t* name) @@ -71,20 +69,17 @@ std::wstring sibling(const std::wstring& path, const wchar_t* name) std::wstring find_coop_tone() { const std::wstring here = coop::exe_directory() + L"coop_tone.exe"; - if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) { return here; } std::wstring dir = coop::exe_directory(); - if (!dir.empty()) - { + if (!dir.empty()) { dir.pop_back(); } const std::size_t slash = dir.find_last_of(L"\\/"); const std::wstring root = (slash == std::wstring::npos) ? std::wstring() : dir.substr(0, slash + 1); const std::wstring in_tests = root + L"tests\\coop_tone.exe"; - if (GetFileAttributesW(in_tests.c_str()) != INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(in_tests.c_str()) != INVALID_FILE_ATTRIBUTES) { return in_tests; } return here; @@ -96,9 +91,8 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) { const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe"); const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll"); - if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES || - GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES + || GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: x86 helper/dll missing next to the tool.\n"); return false; } @@ -106,8 +100,7 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; - if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) - { + if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { std::printf("ERROR: CreateProcess(coop_inject_x86) failed (%lu).\n", GetLastError()); return false; } @@ -121,35 +114,30 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) bool inject(unsigned long pid, const std::wstring& dll_path) { - if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: coop_hook.dll not found next to the tool.\n"); return false; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { std::printf("ERROR: OpenProcess(%lu) failed (%lu). Run as administrator?\n", pid, GetLastError()); return false; } USHORT proc_machine = IMAGE_FILE_MACHINE_UNKNOWN, native_machine = IMAGE_FILE_MACHINE_UNKNOWN; - if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) - { + if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) { CloseHandle(process); return inject_via_helper(pid, dll_path); } const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) - { - auto load_library = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) { + auto load_library = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr); - if (thread != nullptr) - { + if (thread != nullptr) { WaitForSingleObject(thread, INFINITE); DWORD exit_code = 0; GetExitCodeThread(thread, &exit_code); @@ -157,8 +145,7 @@ bool inject(unsigned long pid, const std::wstring& dll_path) ok = (exit_code != 0); } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); @@ -168,13 +155,11 @@ bool inject(unsigned long pid, const std::wstring& dll_path) void enable_hook_trace() { wchar_t dir[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, dir) != 0) - { + if (GetTempPathW(MAX_PATH, dir) != 0) { const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on"; HANDLE h = CreateFileW(sentinel.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (h != INVALID_HANDLE_VALUE) - { + if (h != INVALID_HANDLE_VALUE) { CloseHandle(h); } } @@ -186,24 +171,22 @@ void enable_hook_trace() HANDLE spawn_tone(const Options& o, unsigned long& tone_pid) { const std::wstring exe = find_coop_tone(); - if (GetFileAttributesW(exe.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(exe.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: coop_tone.exe not found (looked next to the tool and in ../tests/).\n"); return nullptr; } HANDLE rd = nullptr, wr = nullptr; SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE}; - if (!CreatePipe(&rd, &wr, &sa, 0)) - { + if (!CreatePipe(&rd, &wr, &sa, 0)) { return nullptr; } SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); // coop_tone [seconds] [freq] [rate] [channels] [bits] [float|pcm] const wchar_t* kind = (o.bits == 32) ? L"float" : L"pcm"; - std::wstring cmd = L"\"" + exe + L"\" " + std::to_wstring(o.seconds + 4) + L" " + - std::to_wstring(static_cast(o.freq)) + L" " + std::to_wstring(o.rate) + L" " + - std::to_wstring(o.channels) + L" " + std::to_wstring(o.bits) + L" " + kind; + std::wstring cmd = L"\"" + exe + L"\" " + std::to_wstring(o.seconds + 4) + L" " + + std::to_wstring(static_cast(o.freq)) + L" " + std::to_wstring(o.rate) + L" " + + std::to_wstring(o.channels) + L" " + std::to_wstring(o.bits) + L" " + kind; STARTUPINFOW si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; @@ -213,8 +196,7 @@ HANDLE spawn_tone(const Options& o, unsigned long& tone_pid) const BOOL launched = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi); CloseHandle(wr); - if (!launched) - { + if (!launched) { std::printf("ERROR: CreateProcess(coop_tone) failed (%lu).\n", GetLastError()); CloseHandle(rd); return nullptr; @@ -227,27 +209,20 @@ HANDLE spawn_tone(const Options& o, unsigned long& tone_pid) char ch = 0; DWORD got = 0; const DWORD start = GetTickCount(); - while (GetTickCount() - start < 5000) - { - if (ReadFile(rd, &ch, 1, &got, nullptr) && got == 1) - { - if (ch == '\n') - { + while (GetTickCount() - start < 5000) { + if (ReadFile(rd, &ch, 1, &got, nullptr) && got == 1) { + if (ch == '\n') { break; } - if (ch != '\r') - { + if (ch != '\r') { line.push_back(ch); } - } - else - { + } else { break; } } CloseHandle(rd); - if (line.rfind("TONE_RENDERING", 0) == 0) - { + if (line.rfind("TONE_RENDERING", 0) == 0) { std::printf("coop_tone: %s\n", line.c_str()); return pi.hProcess; } @@ -261,16 +236,13 @@ HANDLE spawn_tone(const Options& o, unsigned long& tone_pid) // Capture the hook's audio ring for `seconds`, draining frequently so the tool itself // never causes an overrun -- the captured buffer is then exactly what the hook produced. // Fills `pcm` (interleaved) and reports the declared format. Returns false if no format. -bool capture_ring(coop::AudioRingHeader* ring, int seconds, std::vector& pcm, - std::uint32_t& rate, std::uint32_t& channels, std::uint32_t& bits, - std::uint32_t& format_tag, std::uint64_t& overruns) +bool capture_ring(coop::AudioRingHeader* ring, int seconds, std::vector& pcm, std::uint32_t& rate, + std::uint32_t& channels, std::uint32_t& bits, std::uint32_t& format_tag, std::uint64_t& overruns) { // Wait up to 8 s for the hook to publish a format (late attach measures the rate first). const DWORD wait_end = GetTickCount() + 8000; - while (!coop::audio_ring_format_ready(*ring)) - { - if (GetTickCount() >= wait_end) - { + while (!coop::audio_ring_format_ready(*ring)) { + if (GetTickCount() >= wait_end) { std::printf("ERROR: hook never published an audio format (no stream captured).\n"); return false; } @@ -280,48 +252,40 @@ bool capture_ring(coop::AudioRingHeader* ring, int seconds, std::vectorchannels; bits = ring->bits; format_tag = ring->format_tag; - std::printf("Hook published format: %u Hz / %u ch / %u-bit / tag %u. Capturing %d s...\n", rate, channels, - bits, format_tag, seconds); + std::printf("Hook published format: %u Hz / %u ch / %u-bit / tag %u. Capturing %d s...\n", rate, channels, bits, + format_tag, seconds); std::vector scratch(coop::kAudioRingCapacity); const DWORD cap_end = GetTickCount() + static_cast(seconds) * 1000; - while (GetTickCount() < cap_end) - { + while (GetTickCount() < cap_end) { std::uint32_t got = coop::audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size())); - if (got > 0) - { + if (got > 0) { pcm.insert(pcm.end(), scratch.begin(), scratch.begin() + got); - } - else - { + } else { Sleep(2); // ring momentarily empty; poll again shortly } } // Drain any tail. std::uint32_t got = 0; - while ((got = coop::audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) - { + while ((got = coop::audio_ring_pop(*ring, scratch.data(), static_cast(scratch.size()))) > 0) { pcm.insert(pcm.end(), scratch.begin(), scratch.begin() + got); } overruns = ring->overruns.load(std::memory_order_relaxed); return !pcm.empty(); } -void print_report(const coop::ToneReport& r, double expected_hz, std::uint32_t declared_rate, - std::uint64_t overruns) +void print_report(const coop::ToneReport& r, double expected_hz, std::uint32_t declared_rate, std::uint64_t overruns) { std::printf("\n================ FIDELITY REPORT ================\n"); std::printf(" samples analyzed : %zu frames (%.2f s @ %u Hz)\n", r.frames, r.duration_sec, r.sample_rate); std::printf(" level : RMS %.4f peak %.4f clipped %.3f%%\n", r.rms, r.peak, r.clipped_fraction * 100.0); - if (expected_hz > 0.0) - { - std::printf(" PITCH : %.2f Hz captured vs %.2f Hz played -> %+.1f cents (x%.4f)\n", - r.dominant_hz, expected_hz, r.pitch_error_cents, r.pitch_error_ratio); + if (expected_hz > 0.0) { + std::printf(" PITCH : %.2f Hz captured vs %.2f Hz played -> %+.1f cents (x%.4f)\n", r.dominant_hz, + expected_hz, r.pitch_error_cents, r.pitch_error_ratio); // If the pitch is off, the most likely cause is a wrong declared rate. Show the rate // the captured pitch implies, so a misdetection is obvious at a glance. - if (r.pitch_error_ratio > 0.0) - { + if (r.pitch_error_ratio > 0.0) { const double implied_true_rate = declared_rate / r.pitch_error_ratio; std::printf(" implied true rate: ~%.0f Hz (declared %u Hz)%s\n", implied_true_rate, declared_rate, std::fabs(r.pitch_error_cents) > 15.0 ? " <-- MISMATCH" : ""); @@ -330,69 +294,58 @@ void print_report(const coop::ToneReport& r, double expected_hz, std::uint32_t d } std::printf(" discontinuities : %u clicks (%.2f/s)\n", r.glitch_count, r.glitch_rate_per_sec); std::printf(" dropouts : %u gaps, %.1f ms total\n", r.dropout_count, r.dropout_ms); - if (overruns != UINT64_MAX) - { + if (overruns != UINT64_MAX) { std::printf(" ring overruns : %llu (host fell behind -> dropped packets)\n", static_cast(overruns)); } std::printf("------------------- VERDICT --------------------\n"); int problems = 0; - if (expected_hz > 0.0 && std::fabs(r.pitch_error_cents) > 15.0) - { + if (expected_hz > 0.0 && std::fabs(r.pitch_error_cents) > 15.0) { std::printf(" [X] PITCH SHIFT: captured rate is wrong (audible). Likely a mis-measured\n" " late-attach rate -- see implied true rate above.\n"); ++problems; } - if (r.dropout_count > 0) - { - std::printf(" [X] DROPOUTS: %u silence gap(s) -- choppy / 'metallic' under-run artifacts.\n", - r.dropout_count); + if (r.dropout_count > 0) { + std::printf(" [X] DROPOUTS: %u silence gap(s) -- choppy / 'metallic' under-run artifacts.\n", r.dropout_count); ++problems; } - if (r.glitch_rate_per_sec > 1.0) - { + if (r.glitch_rate_per_sec > 1.0) { std::printf(" [X] CLICKS: %.1f discontinuities/s -- torn/dropped packets.\n", r.glitch_rate_per_sec); ++problems; } - if (expected_hz > 0.0 && r.snr_db < 40.0) - { + if (expected_hz > 0.0 && r.snr_db < 40.0) { std::printf(" [X] DISTORTION: SNR %.1f dB is low for a pure tone.\n", r.snr_db); ++problems; } - if (problems == 0) - { + if (problems == 0) { std::printf(" [OK] Captured audio is faithful (pitch, purity, continuity all good).\n"); } std::printf("=================================================\n"); } // Resolve a WAVEFORMATEX (possibly EXTENSIBLE) to the scalar fields the analyzer wants. -void resolve_waveformat(const WAVEFORMATEX* w, std::uint32_t& rate, std::uint32_t& channels, - std::uint32_t& bits, std::uint32_t& tag) +void resolve_waveformat(const WAVEFORMATEX* w, std::uint32_t& rate, std::uint32_t& channels, std::uint32_t& bits, + std::uint32_t& tag) { rate = w->nSamplesPerSec; channels = w->nChannels; bits = w->wBitsPerSample; tag = w->wFormatTag; - if (w->wFormatTag == WAVE_FORMAT_EXTENSIBLE && w->cbSize >= 22) - { + if (w->wFormatTag == WAVE_FORMAT_EXTENSIBLE && w->cbSize >= 22) { const auto* ext = reinterpret_cast(w); - tag = (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) ? coop::kToneFormatFloat - : coop::kToneFormatPcm; + tag = (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) ? coop::kToneFormatFloat : coop::kToneFormatPcm; } } // Loopback-capture `pid`'s render output (device-clock faithful, gaps included) for // `seconds`, into `pcm`, and report the device format. Shared by --render (self) and // --baseline (the tone directly). Assumes COM is already initialized on this thread. -bool loopback_capture_pid(unsigned long pid, int seconds, std::vector& pcm, - std::uint32_t& rate, std::uint32_t& channels, std::uint32_t& bits, - std::uint32_t& tag, std::uint32_t& block_align) +bool loopback_capture_pid(unsigned long pid, int seconds, std::vector& pcm, std::uint32_t& rate, + std::uint32_t& channels, std::uint32_t& bits, std::uint32_t& tag, std::uint32_t& block_align) { WAVEFORMATEX* mix = coop::default_render_format(); - if (mix == nullptr) - { + if (mix == nullptr) { std::printf("ERROR: could not get the default render format.\n"); return false; } @@ -403,17 +356,13 @@ bool loopback_capture_pid(unsigned long pid, int seconds, std::vector(frames) * mix->nBlockAlign; std::lock_guard lk(m); - if (silent || data == nullptr) - { + if (silent || data == nullptr) { pcm.insert(pcm.end(), bytes, 0); - } - else - { + } else { pcm.insert(pcm.end(), data, data + bytes); } }); - if (ok) - { + if (ok) { Sleep(static_cast(seconds) * 1000); } cap.stop(); @@ -429,36 +378,32 @@ bool loopback_capture_pid(unsigned long pid, int seconds, std::vector pcm; std::uint32_t rate = 0, channels = 0, bits = 0, tag = 0, block = 0; const bool ok = loopback_capture_pid(o.listen, o.seconds, pcm, rate, channels, bits, tag, block); int rc = 1; - if (ok && !pcm.empty()) - { + if (ok && !pcm.empty()) { std::wstring out = o.wav_out.empty() ? (coop::exe_directory() + L"coop_listen.wav") : o.wav_out; - if (coop::wav_write(out, pcm.data(), pcm.size(), rate, channels, bits, tag)) - { + if (coop::wav_write(out, pcm.data(), pcm.size(), rate, channels, bits, tag)) { std::wprintf(L"Wrote captured output: %ls\n", out.c_str()); } // Trim the first ~0.7 s for analysis (loopback capture ramp-up) -- the .wav keeps it all. - const std::size_t skip = - std::min(pcm.size(), static_cast(rate) * block * 7 / 10); + const std::size_t skip = std::min(pcm.size(), static_cast(rate) * block * 7 / 10); auto mono = coop::decode_channel(pcm.data() + skip, pcm.size() - skip, tag, bits, channels, 0); - if (!mono.empty()) - { + if (!mono.empty()) { std::printf("\n[LISTEN] pid %lu render output (what the guest hears):\n", o.listen); const coop::ToneReport r = coop::analyze_tone(mono.data(), mono.size(), rate, o.freq); print_report(r, o.freq, rate, UINT64_MAX); rc = 0; } - } - else - { + } else { std::printf("ERROR: no audio captured from pid %lu (is it rendering?).\n", o.listen); } - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return rc; } @@ -471,16 +416,13 @@ int run_selfcheck_mode(const Options& o) const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)); std::atomic stop{false}; std::thread renderer([&]() { - if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) - { + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { return; } coop::tone::ToneSource tone; coop::tone::ToneFormat tf; // {} = device mix format (no resample), cleanest reference - if (tone.open(tf, o.freq)) - { - while (!stop.load(std::memory_order_relaxed)) - { + if (tone.open(tf, o.freq)) { + while (!stop.load(std::memory_order_relaxed)) { tone.render_step(100); } tone.close(); @@ -497,23 +439,21 @@ int run_selfcheck_mode(const Options& o) renderer.join(); int rc = 1; - if (ok && !pcm.empty()) - { + if (ok && !pcm.empty()) { const std::size_t skip = std::min(pcm.size(), static_cast(rate) * block * 7 / 10); auto mono = coop::decode_channel(pcm.data() + skip, pcm.size() - skip, tag, bits, channels, 0); - if (!mono.empty()) - { + if (!mono.empty()) { std::printf("\n[SELFCHECK] in-process tone via the self-capture path (control):\n"); const coop::ToneReport r = coop::analyze_tone(mono.data(), mono.size(), rate, o.freq); print_report(r, o.freq, rate, UINT64_MAX); rc = 0; } - } - else - { + } else { std::printf("ERROR: selfcheck produced no audio.\n"); } - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return rc; } @@ -529,28 +469,25 @@ int run_baseline_mode(const Options& o, HANDLE tone_proc, unsigned long target_p std::uint32_t rate = 0, channels = 0, bits = 0, tag = 0, block = 0; const bool ok = loopback_capture_pid(target_pid, o.seconds, pcm, rate, channels, bits, tag, block); int rc = 1; - if (ok && !pcm.empty()) - { + if (ok && !pcm.empty()) { const std::size_t skip = std::min(pcm.size(), static_cast(rate) * block * 7 / 10); auto mono = coop::decode_channel(pcm.data() + skip, pcm.size() - skip, tag, bits, channels, 0); - if (!mono.empty()) - { + if (!mono.empty()) { std::printf("\n[BASELINE] tone direct (measurement floor):\n"); const coop::ToneReport r = coop::analyze_tone(mono.data(), mono.size(), rate, o.freq); print_report(r, o.freq, rate, UINT64_MAX); rc = 0; } - } - else - { + } else { std::printf("ERROR: baseline loopback capture produced no audio.\n"); } - if (tone_proc != nullptr) - { + if (tone_proc != nullptr) { TerminateProcess(tone_proc, 0); CloseHandle(tone_proc); } - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return rc; } @@ -565,10 +502,11 @@ int run_render_mode(const Options& o, HANDLE tone_proc, unsigned long target_pid const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)); WAVEFORMATEX* mix = coop::default_render_format(); - if (mix == nullptr) - { + if (mix == nullptr) { std::printf("ERROR: could not get the default render format.\n"); - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return 1; } std::uint32_t rate = 0, channels = 0, bits = 0, tag = 0; @@ -580,38 +518,34 @@ int run_render_mode(const Options& o, HANDLE tone_proc, unsigned long target_pid std::vector rendered; std::mutex rendered_mutex; coop::ProcessLoopbackCapture selfcap; - const bool cap_ok = selfcap.start(GetCurrentProcessId(), mix, - [&](const BYTE* data, std::uint32_t frames, bool silent) { - const std::size_t bytes = static_cast(frames) * mix->nBlockAlign; - std::lock_guard lk(rendered_mutex); - if (silent || data == nullptr) - { - rendered.insert(rendered.end(), bytes, 0); - } - else - { - rendered.insert(rendered.end(), data, data + bytes); - } - }); - if (!cap_ok) - { + const bool cap_ok = + selfcap.start(GetCurrentProcessId(), mix, [&](const BYTE* data, std::uint32_t frames, bool silent) { + const std::size_t bytes = static_cast(frames) * mix->nBlockAlign; + std::lock_guard lk(rendered_mutex); + if (silent || data == nullptr) { + rendered.insert(rendered.end(), bytes, 0); + } else { + rendered.insert(rendered.end(), data, data + bytes); + } + }); + if (!cap_ok) { std::printf("ERROR: self-loopback capture failed to start.\n"); CoTaskMemFree(mix); - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return 1; } // Drive the real mirror: it discovers the hook's ring, re-renders it (silencing the game). coop::AudioMirror mirror; - if (!mirror.start(target_pid)) - { + if (!mirror.start(target_pid)) { std::printf("ERROR: AudioMirror failed to start.\n"); } - std::printf("Rendering through the real AudioMirror for %d s (source warms up, then measure)...\n", - o.seconds); + std::printf("Rendering through the real AudioMirror for %d s (source warms up, then measure)...\n", o.seconds); Sleep(static_cast(o.seconds) * 1000); - std::printf(" mirror: source=%s status=\"%s\" buffered=%u ms\n", mirror.source_name(), - mirror.status().c_str(), mirror.buffered_ms()); + std::printf(" mirror: source=%s status=\"%s\" buffered=%u ms\n", mirror.source_name(), mirror.status().c_str(), + mirror.buffered_ms()); mirror.stop(); selfcap.stop(); @@ -623,100 +557,70 @@ int run_render_mode(const Options& o, HANDLE tone_proc, unsigned long target_pid // Trim the first ~0.7 s: it contains start-up priming / the loopback warming up, which // would otherwise read as a spurious leading dropout. - const std::size_t skip = std::min(pcm.size(), static_cast(rate) * - mix->nBlockAlign * 7 / 10); + const std::size_t skip = + std::min(pcm.size(), static_cast(rate) * mix->nBlockAlign * 7 / 10); const std::uint8_t* body = pcm.data() + skip; const std::size_t body_bytes = pcm.size() - skip; std::wstring out = o.wav_out.empty() ? (coop::exe_directory() + L"coop_render.wav") : o.wav_out; - if (coop::wav_write(out, body, body_bytes, rate, channels, bits, tag)) - { + if (coop::wav_write(out, body, body_bytes, rate, channels, bits, tag)) { std::wprintf(L"Wrote rendered output: %ls\n", out.c_str()); } auto mono = coop::decode_channel(body, body_bytes, tag, bits, channels, 0); - if (mono.empty()) - { + if (mono.empty()) { std::printf("NOTE: render format isn't float32/int16; WAV written, analysis skipped.\n"); - } - else - { + } else { std::printf("\n[RENDER PATH] what the guest actually hears (real AudioMirror output):\n"); const coop::ToneReport r = coop::analyze_tone(mono.data(), mono.size(), rate, o.freq); print_report(r, o.freq, rate, UINT64_MAX); } CoTaskMemFree(mix); - if (tone_proc != nullptr) - { + if (tone_proc != nullptr) { TerminateProcess(tone_proc, 0); CloseHandle(tone_proc); } - if (com_ok) { CoUninitialize(); } + if (com_ok) { + CoUninitialize(); + } return mono.empty() ? 1 : 0; } bool parse_args(int argc, wchar_t** argv, Options& o) { - for (int i = 1; i < argc; ++i) - { + for (int i = 1; i < argc; ++i) { const std::wstring a = argv[i]; auto next = [&](unsigned& dst) { - if (i + 1 < argc) - { + if (i + 1 < argc) { dst = static_cast(_wtoi(argv[++i])); } }; - if (a == L"--pid" && i + 1 < argc) - { + if (a == L"--pid" && i + 1 < argc) { o.pid = std::wcstoul(argv[++i], nullptr, 10); - } - else if (a == L"--listen" && i + 1 < argc) - { + } else if (a == L"--listen" && i + 1 < argc) { o.listen = std::wcstoul(argv[++i], nullptr, 10); - } - else if (a == L"--freq" && i + 1 < argc) - { + } else if (a == L"--freq" && i + 1 < argc) { o.freq = _wtof(argv[++i]); - } - else if (a == L"--rate") - { + } else if (a == L"--rate") { next(o.rate); - } - else if (a == L"--channels") - { + } else if (a == L"--channels") { next(o.channels); - } - else if (a == L"--bits") - { + } else if (a == L"--bits") { next(o.bits); - } - else if (a == L"--seconds" && i + 1 < argc) - { + } else if (a == L"--seconds" && i + 1 < argc) { o.seconds = std::max(1, _wtoi(argv[++i])); - } - else if (a == L"--render") - { + } else if (a == L"--render") { o.render = true; - } - else if (a == L"--baseline") - { + } else if (a == L"--baseline") { o.baseline = true; - } - else if (a == L"--selfcheck") - { + } else if (a == L"--selfcheck") { o.selfcheck = true; - } - else if (a == L"--wav" && i + 1 < argc) - { + } else if (a == L"--wav" && i + 1 < argc) { o.wav_in = argv[++i]; - } - else if (a == L"--out" && i + 1 < argc) - { + } else if (a == L"--out" && i + 1 < argc) { o.wav_out = argv[++i]; - } - else if (a == L"--help" || a == L"-h") - { + } else if (a == L"--help" || a == L"-h") { return false; } } @@ -728,8 +632,7 @@ bool parse_args(int argc, wchar_t** argv, Options& o) int wmain(int argc, wchar_t** argv) { Options o; - if (!parse_args(argc, argv, o)) - { + if (!parse_args(argc, argv, o)) { std::printf("usage: coop_audio_validate [--pid N] [--listen N] [--freq Hz] [--rate Hz]\n" " [--channels N] [--bits 16|32] [--seconds N] [--render | --baseline | --selfcheck]\n" " [--wav file] [--out file]\n" @@ -745,19 +648,16 @@ int wmain(int argc, wchar_t** argv) } // --- Mode C: analyze a recorded .wav --------------------------------------------- - if (!o.wav_in.empty()) - { + if (!o.wav_in.empty()) { coop::WavData wd; - if (!coop::wav_read(o.wav_in, wd)) - { + if (!coop::wav_read(o.wav_in, wd)) { std::wprintf(L"ERROR: could not read WAV '%ls'.\n", o.wav_in.c_str()); return 1; } - std::printf("Loaded WAV: %u Hz / %u ch / %u-bit / tag %u, %zu bytes\n", wd.sample_rate, wd.channels, - wd.bits, wd.format_tag, wd.pcm.size()); + std::printf("Loaded WAV: %u Hz / %u ch / %u-bit / tag %u, %zu bytes\n", wd.sample_rate, wd.channels, wd.bits, + wd.format_tag, wd.pcm.size()); auto mono = coop::decode_channel(wd.pcm.data(), wd.pcm.size(), wd.format_tag, wd.bits, wd.channels, 0); - if (mono.empty()) - { + if (mono.empty()) { std::printf("ERROR: unsupported WAV sample format (need 16-bit PCM or 32-bit float).\n"); return 1; } @@ -767,44 +667,36 @@ int wmain(int argc, wchar_t** argv) } // --- Control: render a clean tone in-process + self-capture (no target needed) ---- - if (o.selfcheck) - { + if (o.selfcheck) { return run_selfcheck_mode(o); } // --- Live: passively record an already-running process's output (e.g. coop_host) -- - if (o.listen != 0) - { + if (o.listen != 0) { return run_listen_mode(o); } // --- Acquire a target: spawn coop_tone, or attach to a given pid ------------------ HANDLE tone_proc = nullptr; unsigned long target_pid = o.pid; - if (target_pid == 0) - { + if (target_pid == 0) { tone_proc = spawn_tone(o, target_pid); - if (tone_proc == nullptr) - { + if (tone_proc == nullptr) { return 1; } Sleep(700); // let the tone reach steady state before we inject - } - else - { + } else { std::printf("Attaching to existing pid %lu (tone freq assumed %.0f Hz).\n", target_pid, o.freq); } // --- Mode: measurement floor (no hook, no mirror) --------------------------------- - if (o.baseline) - { + if (o.baseline) { return run_baseline_mode(o, tone_proc, target_pid); } // --- Set up the IPC the hook expects, then inject (late attach: ring AFTER inject) - coop::SharedMemory ipc; - if (!ipc.create(coop::shared_memory_name(target_pid), sizeof(coop::SharedBlock))) - { + if (!ipc.create(coop::shared_memory_name(target_pid), sizeof(coop::SharedBlock))) { std::printf("ERROR: create input mapping failed (%lu).\n", GetLastError()); return 1; } @@ -817,15 +709,13 @@ int wmain(int argc, wchar_t** argv) enable_hook_trace(); std::printf("Injecting coop_hook.dll into pid %lu ...\n", target_pid); - if (!inject(target_pid, coop::deployed_artifact_path(L"coop_hook.dll"))) - { + if (!inject(target_pid, coop::deployed_artifact_path(L"coop_hook.dll"))) { std::printf("ERROR: injection failed.\n"); return 1; } // --- Mode B: measure the host RENDER path (real AudioMirror) ---------------------- - if (o.render) - { + if (o.render) { Sleep(1200); // let the hook register the stream before the mirror reads it const int rc = run_render_mode(o, tone_proc, target_pid); block->magic = 0; @@ -837,9 +727,7 @@ int wmain(int argc, wchar_t** argv) // hook's late-attach guess+measure path (the exact Brotato scenario). Sleep(1500); coop::SharedMemory ring_shm; - if (!ring_shm.create(coop::audio_ring_name(target_pid), - coop::audio_ring_total_size(coop::kAudioRingCapacity))) - { + if (!ring_shm.create(coop::audio_ring_name(target_pid), coop::audio_ring_total_size(coop::kAudioRingCapacity))) { std::printf("ERROR: create audio ring mapping failed (%lu).\n", GetLastError()); return 1; } @@ -853,30 +741,24 @@ int wmain(int argc, wchar_t** argv) std::uint64_t overruns = 0; const bool captured = capture_ring(ring, o.seconds, pcm, rate, channels, bits, format_tag, overruns); - if (captured) - { + if (captured) { // Dump the captured audio so it can be listened to. std::wstring out = o.wav_out.empty() ? (coop::exe_directory() + L"coop_capture.wav") : o.wav_out; - if (coop::wav_write(out, pcm.data(), pcm.size(), rate, channels, bits, format_tag)) - { + if (coop::wav_write(out, pcm.data(), pcm.size(), rate, channels, bits, format_tag)) { std::wprintf(L"Wrote captured audio: %ls\n", out.c_str()); } auto mono = coop::decode_channel(pcm.data(), pcm.size(), format_tag, bits, channels, 0); - if (mono.empty()) - { + if (mono.empty()) { std::printf("NOTE: captured format isn't float32/int16, can't decode for analysis (WAV still written).\n"); - } - else - { + } else { const coop::ToneReport r = coop::analyze_tone(mono.data(), mono.size(), rate, o.freq); print_report(r, o.freq, rate, overruns); } } block->magic = 0; // invalidate so a late hook read won't trust stale data - if (tone_proc != nullptr) - { + if (tone_proc != nullptr) { TerminateProcess(tone_proc, 0); CloseHandle(tone_proc); } diff --git a/tools/inject_helper/main.cpp b/tools/inject_helper/main.cpp index 8229a89..02985ec 100644 --- a/tools/inject_helper/main.cpp +++ b/tools/inject_helper/main.cpp @@ -13,21 +13,18 @@ #include -namespace -{ +namespace { int inject(unsigned long pid, const std::wstring& dll_path) { - if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { std::fprintf(stderr, "coop_inject_x86: dll not found: %ls\n", dll_path.c_str()); return 1; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { std::fprintf(stderr, "coop_inject_x86: OpenProcess(%lu) failed (%lu)\n", pid, GetLastError()); return 1; } @@ -35,28 +32,23 @@ int inject(unsigned long pid, const std::wstring& dll_path) int result = 1; const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) - { + if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) { // In a 32-bit process kernel32 is mapped at the same base as in this 32-bit // helper, so LoadLibraryW's address here is valid as the remote start routine. - auto load_library = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + auto load_library = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr); - if (thread != nullptr) - { + if (thread != nullptr) { WaitForSingleObject(thread, INFINITE); DWORD exit_code = 0; GetExitCodeThread(thread, &exit_code); CloseHandle(thread); result = (exit_code != 0) ? 0 : 1; // LoadLibraryW returns the module handle - } - else - { + } else { std::fprintf(stderr, "coop_inject_x86: CreateRemoteThread failed (%lu)\n", GetLastError()); } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); @@ -67,14 +59,12 @@ int inject(unsigned long pid, const std::wstring& dll_path) int wmain(int argc, wchar_t** argv) { - if (argc < 3) - { + if (argc < 3) { std::printf("usage: coop_inject_x86 \n"); return 2; } const unsigned long pid = std::wcstoul(argv[1], nullptr, 10); - if (pid == 0) - { + if (pid == 0) { std::fprintf(stderr, "coop_inject_x86: invalid pid\n"); return 2; } diff --git a/tools/input_probe/main.cpp b/tools/input_probe/main.cpp index 94b1730..d178513 100644 --- a/tools/input_probe/main.cpp +++ b/tools/input_probe/main.cpp @@ -23,8 +23,7 @@ #include "coop/shared_memory.hpp" #include "coop/tool_paths.hpp" -namespace -{ +namespace { // coop_hook.dll ships in the deployable bin// root; this probe runs from // bin//tools/, so resolve next-to-self first, then one level up. @@ -44,9 +43,8 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) { const std::wstring helper = sibling_of(dll_path, L"coop_inject_x86.exe"); const std::wstring x86_dll = sibling_of(dll_path, L"coop_hook_x86.dll"); - if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES || - GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES + || GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: x86 helper/dll missing next to the probe.\n"); return false; } @@ -55,8 +53,7 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) STARTUPINFOW si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; - if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) - { + if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { std::printf("ERROR: CreateProcess(coop_inject_x86) failed (%lu).\n", GetLastError()); return false; } @@ -65,8 +62,7 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) GetExitCodeProcess(pi.hProcess, &code); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - if (code != 0) - { + if (code != 0) { std::printf("ERROR: coop_inject_x86 reported failure (exit %lu).\n", code); return false; } @@ -75,36 +71,31 @@ bool inject_via_helper(unsigned long pid, const std::wstring& dll_path) bool inject(unsigned long pid, const std::wstring& dll_path) { - if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) - { + if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { std::printf("ERROR: coop_hook.dll not found at the probe's directory.\n"); return false; } - const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ; + const DWORD access = + PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { std::printf("ERROR: OpenProcess(%lu) failed (%lu). Run as administrator?\n", pid, GetLastError()); return false; } USHORT proc_machine = IMAGE_FILE_MACHINE_UNKNOWN, native_machine = IMAGE_FILE_MACHINE_UNKNOWN; - if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) - { + if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN) { CloseHandle(process); return inject_via_helper(pid, dll_path); } const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) - { - auto load_library = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) { + auto load_library = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr); - if (thread != nullptr) - { + if (thread != nullptr) { WaitForSingleObject(thread, INFINITE); DWORD exit_code = 0; GetExitCodeThread(thread, &exit_code); @@ -112,13 +103,11 @@ bool inject(unsigned long pid, const std::wstring& dll_path) ok = (exit_code != 0); } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); - if (!ok) - { + if (!ok) { std::printf("ERROR: injection failed (%lu).\n", GetLastError()); } return ok; @@ -128,8 +117,7 @@ bool inject(unsigned long pid, const std::wstring& dll_path) int wmain(int argc, wchar_t** argv) { - if (argc < 2) - { + if (argc < 2) { std::printf("usage: coop_input_probe [seconds] [disable_mask]\n" " Injects coop_hook.dll, reports one connected pad, and toggles a\n" " button every second so the game processes a real state change.\n" @@ -140,8 +128,7 @@ int wmain(int argc, wchar_t** argv) const unsigned long pid = std::wcstoul(argv[1], nullptr, 10); const int seconds = (argc >= 3) ? std::max(1, _wtoi(argv[2])) : 30; const unsigned disable_mask = (argc >= 4) ? std::wcstoul(argv[3], nullptr, 0) : 0u; - if (pid == 0) - { + if (pid == 0) { std::printf("ERROR: invalid pid.\n"); return 1; } @@ -149,8 +136,7 @@ int wmain(int argc, wchar_t** argv) // 1) Input SharedBlock: report one connected pad up front (buttons still zero), // so the game sees a controller arrive before we start pressing anything. coop::SharedMemory ipc; - if (!ipc.create(coop::shared_memory_name(pid), sizeof(coop::SharedBlock))) - { + if (!ipc.create(coop::shared_memory_name(pid), sizeof(coop::SharedBlock))) { std::printf("ERROR: create input mapping failed (%lu).\n", GetLastError()); return 1; } @@ -162,8 +148,7 @@ int wmain(int argc, wchar_t** argv) // (0x1=input 0x2=focus 0x4=audio 0x8=video). Lets us bisect which injected // subsystem freezes a given game. static const char* kSubsysNames[] = {"input", "focus", "audio", "video", "mkb"}; - for (std::uint32_t i = 0; i < coop::HookSubsys_Count; ++i) - { + for (std::uint32_t i = 0; i < coop::HookSubsys_Count; ++i) { // MKB forwarding needs the host to stream events, which this probe doesn't, so // keep it off here regardless of the mask (avoids confounding crash bisection). const bool disabled = (i == coop::HookSubsys_Mkb) || (disable_mask & (1u << i)) != 0; @@ -181,8 +166,7 @@ int wmain(int argc, wchar_t** argv) coop::SharedMemory log_shm; coop::LogRing* log_ring = nullptr; std::uint64_t log_cursor = 0; - if (log_shm.create(coop::log_ring_name(pid), coop::log_ring_total_size(coop::kLogCapacity))) - { + if (log_shm.create(coop::log_ring_name(pid), coop::log_ring_total_size(coop::kLogCapacity))) { log_ring = log_shm.as(); coop::log_ring_init(*log_ring, coop::kLogCapacity); } @@ -190,29 +174,25 @@ int wmain(int argc, wchar_t** argv) // Enable the hook's file trace for this session. { wchar_t dir[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, dir) != 0) - { + if (GetTempPathW(MAX_PATH, dir) != 0) { const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on"; HANDLE h = CreateFileW(sentinel.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (h != INVALID_HANDLE_VALUE) - { + if (h != INVALID_HANDLE_VALUE) { CloseHandle(h); } } } std::printf("Injecting coop_hook.dll into pid %lu ...\n", pid); - if (!inject(pid, dll_path_next_to_self())) - { + if (!inject(pid, dll_path_next_to_self())) { return 1; } std::printf("Injected. Reporting pad 0 connected; toggling button A each second for %d s.\n", seconds); std::printf("Hook trace: %%TEMP%%\\coop_hook.log\n\n"); const coop::HookStatus& status = block->status; - for (int t = 0; t < seconds; ++t) - { + for (int t = 0; t < seconds; ++t) { // Toggle A (0x1000) every other second so the game's input layer sees a real // edge -- this is the "press a button" event the crash report points at. const bool press = (t % 2) == 1; @@ -232,8 +212,7 @@ int wmain(int argc, wchar_t** argv) static_cast(gc0), pads[0].buttons); // Surface the hook's log lines as they arrive (shows where it got to). - if (log_ring != nullptr) - { + if (log_ring != nullptr) { coop::log_ring_drain(*log_ring, log_cursor, [](const coop::LogRecord& rec) { std::printf(" | %s\n", rec.text); }); } @@ -241,8 +220,7 @@ int wmain(int argc, wchar_t** argv) std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count); static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video", "MKB"}; - for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i) - { + for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i) { const coop::HookEntry& e = status.hook_entries[i]; std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 5 ? kSubsys[e.subsystem] : "?", e.name, e.installed ? "ON " : "off", static_cast(e.calls)); diff --git a/tools/mock_game/main.cpp b/tools/mock_game/main.cpp index fc4a9b4..c09df03 100644 --- a/tools/mock_game/main.cpp +++ b/tools/mock_game/main.cpp @@ -21,8 +21,7 @@ #include "render_backend.hpp" #include "tone_source.hpp" -namespace -{ +namespace { std::atomic g_running{true}; // Diagnostic: on an access violation, log the faulting address and the caller (return address on the @@ -30,17 +29,15 @@ std::atomic g_running{true}; // crash so the test still detects it. LONG WINAPI crash_logger(EXCEPTION_POINTERS* ep) { - if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) - { + if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) { return EXCEPTION_CONTINUE_SEARCH; } auto mod = [](void* p, char* out, size_t n) -> unsigned long long { HMODULE m = nullptr; - if (p != nullptr && - GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(p), &m) && - m != nullptr) - { + if (p != nullptr + && GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(p), &m) + && m != nullptr) { char path[MAX_PATH] = {}; GetModuleFileNameA(m, path, MAX_PATH); const char* base = std::strrchr(path, '\\'); @@ -62,8 +59,7 @@ LONG WINAPI crash_logger(EXCEPTION_POINTERS* ep) LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { - if (msg == WM_DESTROY) - { + if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; } @@ -73,23 +69,18 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) // Spawns the audio render loop on its own thread (WASAPI wants its own COM apartment). void audio_thread(coop::tone::ToneFormat want) { - if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) - { + if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { return; } coop::tone::ToneSource tone; - if (tone.open(want, 440.0)) - { + if (tone.open(want, 440.0)) { std::printf("MOCK_GAME audio: %u Hz %u ch %u-bit %s\n", tone.format().rate, tone.format().channels, tone.format().bits, tone.format().is_float ? "float" : "pcm"); std::fflush(stdout); - while (g_running.load(std::memory_order_relaxed)) - { + while (g_running.load(std::memory_order_relaxed)) { tone.render_step(200); } - } - else - { + } else { std::printf("MOCK_GAME audio: failed to open requested format\n"); } tone.close(); @@ -103,11 +94,11 @@ void audio_thread(coop::tone::ToneFormat want) void poll_input() { XINPUT_STATE xs{}; - (void)XInputGetState(0, &xs); // XInput hook (XInputGetState/Ex) - (void)GetAsyncKeyState(VK_SPACE); // MKB hook (GetAsyncKeyState) + (void)XInputGetState(0, &xs); // XInput hook (XInputGetState/Ex) + (void)GetAsyncKeyState(VK_SPACE); // MKB hook (GetAsyncKeyState) BYTE kb[256] = {}; - (void)GetKeyboardState(kb); // MKB hook (GetKeyboardState) - (void)GetForegroundWindow(); // focus hook (GetForegroundWindow) + (void)GetKeyboardState(kb); // MKB hook (GetKeyboardState) + (void)GetForegroundWindow(); // focus hook (GetForegroundWindow) } } // namespace @@ -118,8 +109,7 @@ int main(int argc, char** argv) const double seconds = argc > 2 ? std::strtod(argv[2], nullptr) : 0.0; const bool want_audio = argc > 3; coop::tone::ToneFormat audio_fmt; - if (want_audio) - { + if (want_audio) { audio_fmt.rate = static_cast(std::strtoul(argv[3], nullptr, 10)); audio_fmt.channels = argc > 4 ? static_cast(std::strtoul(argv[4], nullptr, 10)) : 2; audio_fmt.bits = argc > 5 ? static_cast(std::strtoul(argv[5], nullptr, 10)) : 32; @@ -139,11 +129,9 @@ int main(int argc, char** argv) RECT r = {0, 0, static_cast(kW), static_cast(kH)}; AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); - HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"CoopMockGame", WS_OVERLAPPEDWINDOW | WS_VISIBLE, - CW_USEDEFAULT, CW_USEDEFAULT, r.right - r.left, r.bottom - r.top, nullptr, - nullptr, inst, nullptr); - if (hwnd == nullptr) - { + HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"CoopMockGame", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, + CW_USEDEFAULT, r.right - r.left, r.bottom - r.top, nullptr, nullptr, inst, nullptr); + if (hwnd == nullptr) { std::printf("MOCK_GAME error: CreateWindow failed\n"); return 1; } @@ -151,26 +139,23 @@ int main(int argc, char** argv) // Test hook (early-load): Vulkan caches its present pointer at init, so the capture hook must // be in place before vkCreateInstance. Under COOP_MOCK_VK_EARLY the mock loads vulkan-1.dll // now and waits, giving an already-injected hook time to hook vkGetInstanceProcAddr first. - if (backend_name == "vk" && GetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr, 0) != 0) - { + if (backend_name == "vk" && GetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr, 0) != 0) { LoadLibraryW(L"vulkan-1.dll"); Sleep(1500); } auto backend = coop::mock::RenderBackend::create(backend_name); - if (!backend || !backend->init(hwnd, kW, kH)) - { + if (!backend || !backend->init(hwnd, kW, kH)) { std::printf("MOCK_GAME error: backend '%s' unavailable\n", backend_name.c_str()); return 2; } - std::printf("MOCK_GAME pid=%lu backend=%s w=%u h=%u audio=%s\n", GetCurrentProcessId(), backend->name(), - kW, kH, want_audio ? "yes" : "no"); + std::printf("MOCK_GAME pid=%lu backend=%s w=%u h=%u audio=%s\n", GetCurrentProcessId(), backend->name(), kW, kH, + want_audio ? "yes" : "no"); std::fflush(stdout); std::thread audio; - if (want_audio) - { + if (want_audio) { audio = std::thread(audio_thread, audio_fmt); } @@ -178,20 +163,16 @@ int main(int argc, char** argv) std::uint32_t frame = 0; ULONGLONG fps_window_start = start; // window-title fps: frames in the last ~second std::uint32_t fps_window_frames = 0; - for (;;) - { + for (;;) { MSG msg; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) - { - if (msg.message == WM_QUIT) - { + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { g_running.store(false, std::memory_order_relaxed); } TranslateMessage(&msg); DispatchMessageW(&msg); } - if (!g_running.load(std::memory_order_relaxed)) - { + if (!g_running.load(std::memory_order_relaxed)) { break; } poll_input(); // drive the input/focus/MKB detours each frame, like a real game @@ -200,8 +181,7 @@ int main(int argc, char** argv) // Show the backend + a once-per-second-smoothed fps in the title bar. ++fps_window_frames; const ULONGLONG now = GetTickCount64(); - if (now - fps_window_start >= 1000) - { + if (now - fps_window_start >= 1000) { const double fps = fps_window_frames * 1000.0 / static_cast(now - fps_window_start); wchar_t title[128]; swprintf(title, 128, L"CoopMockGame [%hs] - %.0f fps", backend->name(), fps); @@ -209,15 +189,13 @@ int main(int argc, char** argv) fps_window_start = now; fps_window_frames = 0; } - if (seconds > 0.0 && (GetTickCount64() - start) >= static_cast(seconds * 1000.0)) - { + if (seconds > 0.0 && (GetTickCount64() - start) >= static_cast(seconds * 1000.0)) { break; } } g_running.store(false, std::memory_order_relaxed); - if (audio.joinable()) - { + if (audio.joinable()) { audio.join(); } std::printf("MOCK_GAME done: %u frames\n", frame); diff --git a/tools/mock_game/render_backend.cpp b/tools/mock_game/render_backend.cpp index ca0216f..788f767 100644 --- a/tools/mock_game/render_backend.cpp +++ b/tools/mock_game/render_backend.cpp @@ -1,36 +1,28 @@ #include "render_backend.hpp" -namespace coop::mock -{ +namespace coop::mock { std::unique_ptr RenderBackend::create(const std::string& name) { - if (name == "dx11") - { + if (name == "dx11") { return create_dx11_backend(); } - if (name == "dx12") - { + if (name == "dx12") { return create_dx12_backend(); } - if (name == "dx10") - { + if (name == "dx10") { return create_dx10_backend(); } - if (name == "dx9ex") - { + if (name == "dx9ex") { return create_dx9_backend(/*ex=*/true); } - if (name == "dx9") - { + if (name == "dx9") { return create_dx9_backend(/*ex=*/false); } - if (name == "gl" || name == "opengl") - { + if (name == "gl" || name == "opengl") { return create_gl_backend(); } - if (name == "vk" || name == "vulkan") - { + if (name == "vk" || name == "vulkan") { return create_vk_backend(); } return nullptr; diff --git a/tools/mock_game/render_backend.hpp b/tools/mock_game/render_backend.hpp index 5d7237b..8ccefdc 100644 --- a/tools/mock_game/render_backend.hpp +++ b/tools/mock_game/render_backend.hpp @@ -15,8 +15,7 @@ #include -namespace coop::mock -{ +namespace coop::mock { // Encode a frame counter into an RGB triple (and back). R/G/B are the low 24 bits, so it // is unambiguous for ~16M frames. The swap chain is UNORM (not sRGB), so the bytes survive @@ -30,8 +29,7 @@ inline void frame_to_rgb(std::uint32_t frame, std::uint8_t& r, std::uint8_t& g, inline std::uint32_t rgb_to_frame(std::uint8_t r, std::uint8_t g, std::uint8_t b) { - return static_cast(r) | (static_cast(g) << 8) | - (static_cast(b) << 16); + return static_cast(r) | (static_cast(g) << 8) | (static_cast(b) << 16); } // Size (px) of the top-left frame-counter block the test samples. @@ -44,8 +42,7 @@ inline constexpr std::uint32_t kBarWidth = 24; // IDENTICAL image (the capture test compares backends). Each backend consumes these values with its // own API's clear/fill calls: an animated full-screen background, a kBarWidth-wide full-height white // bar at bar_x, and the top-left kFrameBlock-square frame-counter block whose colour encodes `frame`. -struct FramePattern -{ +struct FramePattern { std::uint8_t bg_r, bg_g, bg_b; // background; each channel sweeps at a different rate -> motion std::uint32_t bar_x; // left edge of the moving bar std::uint8_t code_r, code_g, code_b; // frame-counter block colour (frame_to_rgb) @@ -63,9 +60,8 @@ inline FramePattern frame_pattern(std::uint32_t frame, std::uint32_t width) return p; } -class RenderBackend -{ -public: +class RenderBackend { + public: virtual ~RenderBackend() = default; // Bring up the device + swap chain on `hwnd` at the given client size. False on failure. diff --git a/tools/mock_game/render_dx09.cpp b/tools/mock_game/render_dx09.cpp index aa18f4d..0bcb4fd 100644 --- a/tools/mock_game/render_dx09.cpp +++ b/tools/mock_game/render_dx09.cpp @@ -10,21 +10,16 @@ using Microsoft::WRL::ComPtr; -namespace coop::mock -{ -namespace -{ +namespace coop::mock { +namespace { D3DCOLOR opaque(std::uint8_t r, std::uint8_t g, std::uint8_t b) { return D3DCOLOR_ARGB(255, r, g, b); } -class Dx9Backend : public RenderBackend -{ -public: - explicit Dx9Backend(bool ex) : ex_(ex) - { - } +class Dx9Backend : public RenderBackend { + public: + explicit Dx9Backend(bool ex) : ex_(ex) {} bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { @@ -41,32 +36,24 @@ public: pp.Windowed = TRUE; pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // uncapped: the mock must be fast - if (ex_) - { + if (ex_) { ComPtr d3d; - if (FAILED(Direct3DCreate9Ex(D3D_SDK_VERSION, d3d.GetAddressOf()))) - { + if (FAILED(Direct3DCreate9Ex(D3D_SDK_VERSION, d3d.GetAddressOf()))) { return false; } ComPtr dev; if (FAILED(d3d->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, - D3DCREATE_HARDWARE_VERTEXPROCESSING, &pp, nullptr, - dev.GetAddressOf()))) - { + D3DCREATE_HARDWARE_VERTEXPROCESSING, &pp, nullptr, dev.GetAddressOf()))) { return false; } dev_ = dev; // IDirect3DDevice9Ex derives from IDirect3DDevice9 - } - else - { + } else { ComPtr d3d(Direct3DCreate9(D3D_SDK_VERSION)); - if (!d3d) - { + if (!d3d) { return false; } - if (FAILED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, - D3DCREATE_HARDWARE_VERTEXPROCESSING, &pp, dev_.GetAddressOf()))) - { + if (FAILED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, + &pp, dev_.GetAddressOf()))) { return false; } } @@ -79,8 +66,7 @@ public: dev_->Clear(0, nullptr, D3DCLEAR_TARGET, opaque(p.bg_r, p.bg_g, p.bg_b), 1.0f, 0); ComPtr back; - if (SUCCEEDED(dev_->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, back.GetAddressOf()))) - { + if (SUCCEEDED(dev_->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, back.GetAddressOf()))) { const LONG bx = static_cast(p.bar_x); RECT bar = {bx, 0, bx + static_cast(kBarWidth), static_cast(height_)}; // moving vertical bar dev_->ColorFill(back.Get(), &bar, opaque(255, 255, 255)); @@ -92,12 +78,9 @@ public: dev_->Present(nullptr, nullptr, nullptr, nullptr); } - [[nodiscard]] const char* name() const override - { - return ex_ ? "dx9ex" : "dx9"; - } + [[nodiscard]] const char* name() const override { return ex_ ? "dx9ex" : "dx9"; } -private: + private: bool ex_; std::uint32_t width_ = 0; std::uint32_t height_ = 0; diff --git a/tools/mock_game/render_dx10.cpp b/tools/mock_game/render_dx10.cpp index 0d31679..4ee738e 100644 --- a/tools/mock_game/render_dx10.cpp +++ b/tools/mock_game/render_dx10.cpp @@ -13,20 +13,17 @@ using Microsoft::WRL::ComPtr; -namespace coop::mock -{ -namespace -{ +namespace coop::mock { +namespace { std::uint32_t pack(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a = 255) { // R8G8B8A8_UNORM byte order: R in the low byte. - return static_cast(r) | (static_cast(g) << 8) | - (static_cast(b) << 16) | (static_cast(a) << 24); + return static_cast(r) | (static_cast(g) << 8) | (static_cast(b) << 16) + | (static_cast(a) << 24); } -class Dx10Backend : public RenderBackend -{ -public: +class Dx10Backend : public RenderBackend { + public: bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { width_ = width; @@ -46,10 +43,8 @@ public: desc.Windowed = TRUE; desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; // blt model: GetBuffer(0) is the back buffer - if (FAILED(D3D10CreateDeviceAndSwapChain(nullptr, D3D10_DRIVER_TYPE_HARDWARE, nullptr, 0, - D3D10_SDK_VERSION, &desc, swap_.GetAddressOf(), - device_.GetAddressOf()))) - { + if (FAILED(D3D10CreateDeviceAndSwapChain(nullptr, D3D10_DRIVER_TYPE_HARDWARE, nullptr, 0, D3D10_SDK_VERSION, + &desc, swap_.GetAddressOf(), device_.GetAddressOf()))) { return false; } @@ -75,18 +70,14 @@ public: const std::uint32_t code = pack(p.code_r, p.code_g, p.code_b); const std::uint32_t bx = p.bar_x; // moving vertical bar - for (std::uint32_t y = 0; y < height_; ++y) - { + for (std::uint32_t y = 0; y < height_; ++y) { std::uint32_t* row = px_.data() + static_cast(y) * width_; - for (std::uint32_t x = 0; x < width_; ++x) - { + for (std::uint32_t x = 0; x < width_; ++x) { std::uint32_t c = bg; - if (x >= bx && x < bx + kBarWidth) - { + if (x >= bx && x < bx + kBarWidth) { c = whitepx; } - if (x < kFrameBlock && y < kFrameBlock) - { + if (x < kFrameBlock && y < kFrameBlock) { c = code; // top-left frame-counter block } row[x] = c; @@ -95,19 +86,15 @@ public: device_->UpdateSubresource(scratch_.Get(), 0, nullptr, px_.data(), static_cast(width_ * 4), 0); ComPtr back; - if (SUCCEEDED(swap_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf())))) - { + if (SUCCEEDED(swap_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf())))) { device_->CopyResource(back.Get(), scratch_.Get()); } swap_->Present(0, 0); // uncapped (BLT model): the mock does nothing -> it must be fast } - [[nodiscard]] const char* name() const override - { - return "dx10"; - } + [[nodiscard]] const char* name() const override { return "dx10"; } -private: + private: std::uint32_t width_ = 0; std::uint32_t height_ = 0; std::vector px_; diff --git a/tools/mock_game/render_dx11.cpp b/tools/mock_game/render_dx11.cpp index 593ab2a..74f97df 100644 --- a/tools/mock_game/render_dx11.cpp +++ b/tools/mock_game/render_dx11.cpp @@ -9,13 +9,10 @@ using Microsoft::WRL::ComPtr; -namespace coop::mock -{ -namespace -{ -class Dx11Backend : public RenderBackend -{ -public: +namespace coop::mock { +namespace { +class Dx11Backend : public RenderBackend { + public: bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { width_ = width; @@ -24,9 +21,8 @@ public: const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; ComPtr ctx0; if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, - static_cast(std::size(levels)), D3D11_SDK_VERSION, - device_.GetAddressOf(), nullptr, ctx0.GetAddressOf()))) - { + static_cast(std::size(levels)), D3D11_SDK_VERSION, device_.GetAddressOf(), + nullptr, ctx0.GetAddressOf()))) { return false; } if (FAILED(ctx0.As(&ctx_))) // ClearView needs ID3D11DeviceContext1 @@ -37,9 +33,8 @@ public: ComPtr dxgi_device; ComPtr adapter; ComPtr factory; - if (FAILED(device_.As(&dxgi_device)) || FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf())) || - FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf())))) - { + if (FAILED(device_.As(&dxgi_device)) || FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf())) + || FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf())))) { return false; } @@ -47,8 +42,7 @@ public: // game) -- a flip-model swapchain only tears free of vsync with ALLOW_TEARING, so require it. ComPtr factory5; BOOL tearing = FALSE; - if (SUCCEEDED(factory.As(&factory5))) - { + if (SUCCEEDED(factory.As(&factory5))) { factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &tearing, sizeof(tearing)); } tearing_ = tearing != 0; @@ -62,18 +56,16 @@ public: desc.BufferCount = 2; desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; desc.Flags = tearing_ ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0u; - if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd, &desc, nullptr, nullptr, - swap_.GetAddressOf()))) - { + if (FAILED( + factory->CreateSwapChainForHwnd(device_.Get(), hwnd, &desc, nullptr, nullptr, swap_.GetAddressOf()))) { return false; } factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER); // D3D11 flip-model: GetBuffer(0) stays the live back buffer, so one RTV is reused. ComPtr back; - if (FAILED(swap_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf()))) || - FAILED(device_->CreateRenderTargetView(back.Get(), nullptr, rtv_.GetAddressOf()))) - { + if (FAILED(swap_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf()))) + || FAILED(device_->CreateRenderTargetView(back.Get(), nullptr, rtv_.GetAddressOf()))) { return false; } return true; @@ -98,12 +90,9 @@ public: swap_->Present(0, tearing_ ? DXGI_PRESENT_ALLOW_TEARING : 0u); // uncapped: the mock must be fast } - [[nodiscard]] const char* name() const override - { - return "dx11"; - } + [[nodiscard]] const char* name() const override { return "dx11"; } -private: + private: std::uint32_t width_ = 0; std::uint32_t height_ = 0; bool tearing_ = false; diff --git a/tools/mock_game/render_dx12.cpp b/tools/mock_game/render_dx12.cpp index 819146f..266eaea 100644 --- a/tools/mock_game/render_dx12.cpp +++ b/tools/mock_game/render_dx12.cpp @@ -9,43 +9,36 @@ using Microsoft::WRL::ComPtr; -namespace coop::mock -{ -namespace -{ +namespace coop::mock { +namespace { constexpr UINT kBackBuffers = 3; // DX12 rotates these explicitly -- the case the capture must get right -class Dx12Backend : public RenderBackend -{ -public: +class Dx12Backend : public RenderBackend { + public: bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { width_ = width; height_ = height; - if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device_.GetAddressOf())))) - { + if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device_.GetAddressOf())))) { return false; } D3D12_COMMAND_QUEUE_DESC qd = {}; qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; - if (FAILED(device_->CreateCommandQueue(&qd, IID_PPV_ARGS(queue_.GetAddressOf())))) - { + if (FAILED(device_->CreateCommandQueue(&qd, IID_PPV_ARGS(queue_.GetAddressOf())))) { return false; } ComPtr factory; - if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf())))) - { + if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf())))) { return false; } // Uncapped (the mock is a perf fixture): a flip-model swapchain needs ALLOW_TEARING to run // free of vsync. ComPtr factory5; BOOL tearing = FALSE; - if (SUCCEEDED(factory.As(&factory5))) - { + if (SUCCEEDED(factory.As(&factory5))) { factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &tearing, sizeof(tearing)); } tearing_ = tearing != 0; @@ -60,10 +53,8 @@ public: desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; desc.Flags = tearing_ ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0u; ComPtr sc1; - if (FAILED(factory->CreateSwapChainForHwnd(queue_.Get(), hwnd, &desc, nullptr, nullptr, - sc1.GetAddressOf())) || - FAILED(sc1.As(&swap_))) - { + if (FAILED(factory->CreateSwapChainForHwnd(queue_.Get(), hwnd, &desc, nullptr, nullptr, sc1.GetAddressOf())) + || FAILED(sc1.As(&swap_))) { return false; } factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER); @@ -71,36 +62,30 @@ public: D3D12_DESCRIPTOR_HEAP_DESC hd = {}; hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; hd.NumDescriptors = kBackBuffers; - if (FAILED(device_->CreateDescriptorHeap(&hd, IID_PPV_ARGS(rtv_heap_.GetAddressOf())))) - { + if (FAILED(device_->CreateDescriptorHeap(&hd, IID_PPV_ARGS(rtv_heap_.GetAddressOf())))) { return false; } rtv_stride_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); D3D12_CPU_DESCRIPTOR_HANDLE h = rtv_heap_->GetCPUDescriptorHandleForHeapStart(); - for (UINT i = 0; i < kBackBuffers; ++i) - { - if (FAILED(swap_->GetBuffer(i, IID_PPV_ARGS(targets_[i].GetAddressOf())))) - { + for (UINT i = 0; i < kBackBuffers; ++i) { + if (FAILED(swap_->GetBuffer(i, IID_PPV_ARGS(targets_[i].GetAddressOf())))) { return false; } device_->CreateRenderTargetView(targets_[i].Get(), nullptr, h); rtv_handles_[i] = h; h.ptr += rtv_stride_; if (FAILED(device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, - IID_PPV_ARGS(allocs_[i].GetAddressOf())))) - { + IID_PPV_ARGS(allocs_[i].GetAddressOf())))) { return false; } } if (FAILED(device_->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocs_[0].Get(), nullptr, - IID_PPV_ARGS(list_.GetAddressOf())))) - { + IID_PPV_ARGS(list_.GetAddressOf())))) { return false; } list_->Close(); - if (FAILED(device_->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence_.GetAddressOf())))) - { + if (FAILED(device_->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence_.GetAddressOf())))) { return false; } fence_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr); @@ -138,24 +123,19 @@ public: wait_for_gpu(); // simple per-frame sync (mock game: correctness over throughput) } - [[nodiscard]] const char* name() const override - { - return "dx12"; - } + [[nodiscard]] const char* name() const override { return "dx12"; } ~Dx12Backend() override { - if (fence_ != nullptr) - { + if (fence_ != nullptr) { wait_for_gpu(); } - if (fence_event_ != nullptr) - { + if (fence_event_ != nullptr) { CloseHandle(fence_event_); } } -private: + private: void transition(ID3D12Resource* res, D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to) { D3D12_RESOURCE_BARRIER b = {}; @@ -171,8 +151,7 @@ private: { const UINT64 v = ++fence_value_; queue_->Signal(fence_.Get(), v); - if (fence_->GetCompletedValue() < v) - { + if (fence_->GetCompletedValue() < v) { fence_->SetEventOnCompletion(v, fence_event_); WaitForSingleObject(fence_event_, INFINITE); } diff --git a/tools/mock_game/render_gl.cpp b/tools/mock_game/render_gl.cpp index 534fa75..231d250 100644 --- a/tools/mock_game/render_gl.cpp +++ b/tools/mock_game/render_gl.cpp @@ -12,21 +12,17 @@ #include -namespace coop::mock -{ -namespace -{ -class GlBackend : public RenderBackend -{ -public: +namespace coop::mock { +namespace { +class GlBackend : public RenderBackend { + public: bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { width_ = width; height_ = height; hwnd_ = hwnd; hdc_ = GetDC(hwnd); - if (hdc_ == nullptr) - { + if (hdc_ == nullptr) { return false; } PIXELFORMATDESCRIPTOR pfd = {}; @@ -38,20 +34,17 @@ public: pfd.cAlphaBits = 8; pfd.iLayerType = PFD_MAIN_PLANE; const int pf = ChoosePixelFormat(hdc_, &pfd); - if (pf == 0 || !SetPixelFormat(hdc_, pf, &pfd)) - { + if (pf == 0 || !SetPixelFormat(hdc_, pf, &pfd)) { return false; } hglrc_ = wglCreateContext(hdc_); // legacy context is enough for GL 1.1 clears - if (hglrc_ == nullptr || !wglMakeCurrent(hdc_, hglrc_)) - { + if (hglrc_ == nullptr || !wglMakeCurrent(hdc_, hglrc_)) { return false; } // Uncapped: the mock is a perf fixture and must run as fast as it can (disable vsync), so a // capture-induced slowdown is visible. Runtime extension lookup -- no loader/submodule needed. using PFN_wglSwapIntervalEXT = BOOL(WINAPI*)(int); - if (auto swap_interval = reinterpret_cast(wglGetProcAddress("wglSwapIntervalEXT"))) - { + if (auto swap_interval = reinterpret_cast(wglGetProcAddress("wglSwapIntervalEXT"))) { swap_interval(0); } return true; @@ -87,25 +80,20 @@ public: SwapBuffers(hdc_); // the capture hook intercepts this } - [[nodiscard]] const char* name() const override - { - return "gl"; - } + [[nodiscard]] const char* name() const override { return "gl"; } ~GlBackend() override { wglMakeCurrent(nullptr, nullptr); - if (hglrc_ != nullptr) - { + if (hglrc_ != nullptr) { wglDeleteContext(hglrc_); } - if (hdc_ != nullptr && hwnd_ != nullptr) - { + if (hdc_ != nullptr && hwnd_ != nullptr) { ReleaseDC(hwnd_, hdc_); } } -private: + private: std::uint32_t width_ = 0; std::uint32_t height_ = 0; HWND hwnd_ = nullptr; diff --git a/tools/mock_game/render_vk.cpp b/tools/mock_game/render_vk.cpp index a61fd67..c3f0d76 100644 --- a/tools/mock_game/render_vk.cpp +++ b/tools/mock_game/render_vk.cpp @@ -10,19 +10,15 @@ #include -namespace coop::mock -{ -namespace -{ -class VkBackend : public RenderBackend -{ -public: +namespace coop::mock { +namespace { +class VkBackend : public RenderBackend { + public: bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override { width_ = width; height_ = height; - if (volkInitialize() != VK_SUCCESS) - { + if (volkInitialize() != VK_SUCCESS) { return false; // no Vulkan loader on this machine } @@ -34,8 +30,7 @@ public: ici.pApplicationInfo = &app; ici.enabledExtensionCount = 2; ici.ppEnabledExtensionNames = inst_ext; - if (vkCreateInstance(&ici, nullptr, &instance_) != VK_SUCCESS) - { + if (vkCreateInstance(&ici, nullptr, &instance_) != VK_SUCCESS) { return false; } volkLoadInstance(instance_); @@ -43,13 +38,11 @@ public: VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; sci.hinstance = GetModuleHandleW(nullptr); sci.hwnd = hwnd; - if (vkCreateWin32SurfaceKHR(instance_, &sci, nullptr, &surface_) != VK_SUCCESS) - { + if (vkCreateWin32SurfaceKHR(instance_, &sci, nullptr, &surface_) != VK_SUCCESS) { return false; } - if (!pick_device() || !create_device() || !create_swapchain() || !create_commands()) - { + if (!pick_device() || !create_device() || !create_swapchain() || !create_commands()) { return false; } return true; @@ -57,20 +50,17 @@ public: void render_and_present(std::uint32_t frame) override { - if (device_ == VK_NULL_HANDLE || swapchain_ == VK_NULL_HANDLE) - { + if (device_ == VK_NULL_HANDLE || swapchain_ == VK_NULL_HANDLE) { return; } vkWaitForFences(device_, 1, &in_flight_, VK_TRUE, UINT64_MAX); std::uint32_t idx = 0; VkResult acq = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, acquire_sem_, VK_NULL_HANDLE, &idx); - if (acq == VK_ERROR_OUT_OF_DATE_KHR || acq == VK_SUBOPTIMAL_KHR) - { + if (acq == VK_ERROR_OUT_OF_DATE_KHR || acq == VK_SUBOPTIMAL_KHR) { return; // skip this frame (the mock window isn't resized in practice) } - if (acq != VK_SUCCESS) - { + if (acq != VK_SUCCESS) { return; } vkResetFences(device_, 1, &in_flight_); @@ -118,15 +108,11 @@ public: vkQueuePresentKHR(queue_, &pi); } - [[nodiscard]] const char* name() const override - { - return "vk"; - } + [[nodiscard]] const char* name() const override { return "vk"; } ~VkBackend() override { - if (device_ != VK_NULL_HANDLE) - { + if (device_ != VK_NULL_HANDLE) { vkDeviceWaitIdle(device_); if (in_flight_ != VK_NULL_HANDLE) vkDestroyFence(device_, in_flight_, nullptr); @@ -146,25 +132,22 @@ public: vkDestroyInstance(instance_, nullptr); } -private: + private: bool pick_device() { std::uint32_t n = 0; vkEnumeratePhysicalDevices(instance_, &n, nullptr); std::vector devs(n); vkEnumeratePhysicalDevices(instance_, &n, devs.data()); - for (VkPhysicalDevice pd : devs) - { + for (VkPhysicalDevice pd : devs) { std::uint32_t qn = 0; vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, nullptr); std::vector qf(qn); vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, qf.data()); - for (std::uint32_t i = 0; i < qn; ++i) - { + for (std::uint32_t i = 0; i < qn; ++i) { VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(pd, i, surface_, &present); - if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) - { + if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) { phys_ = pd; qfam_ = i; return true; @@ -187,8 +170,7 @@ private: dci.pQueueCreateInfos = &qci; dci.enabledExtensionCount = 1; dci.ppEnabledExtensionNames = dev_ext; - if (vkCreateDevice(phys_, &dci, nullptr, &device_) != VK_SUCCESS) - { + if (vkCreateDevice(phys_, &dci, nullptr, &device_) != VK_SUCCESS) { return false; } volkLoadDevice(device_); @@ -205,13 +187,11 @@ private: vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, nullptr); std::vector formats(fn); vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, formats.data()); - VkSurfaceFormatKHR chosen = formats.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, - VK_COLOR_SPACE_SRGB_NONLINEAR_KHR} - : formats[0]; - for (const VkSurfaceFormatKHR& f : formats) - { - if (f.format == VK_FORMAT_B8G8R8A8_UNORM || f.format == VK_FORMAT_R8G8B8A8_UNORM) - { + VkSurfaceFormatKHR chosen = + formats.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR} + : formats[0]; + for (const VkSurfaceFormatKHR& f : formats) { + if (f.format == VK_FORMAT_B8G8R8A8_UNORM || f.format == VK_FORMAT_R8G8B8A8_UNORM) { chosen = f; break; } @@ -219,8 +199,7 @@ private: format_ = chosen.format; std::uint32_t want = caps.minImageCount + 1; - if (caps.maxImageCount > 0 && want > caps.maxImageCount) - { + if (caps.maxImageCount > 0 && want > caps.maxImageCount) { want = caps.maxImageCount; } VkSwapchainCreateInfoKHR sc{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; @@ -228,9 +207,7 @@ private: sc.minImageCount = want; sc.imageFormat = chosen.format; sc.imageColorSpace = chosen.colorSpace; - sc.imageExtent = caps.currentExtent.width != 0xFFFFFFFFu - ? caps.currentExtent - : VkExtent2D{width_, height_}; + sc.imageExtent = caps.currentExtent.width != 0xFFFFFFFFu ? caps.currentExtent : VkExtent2D{width_, height_}; sc.imageArrayLayers = 1; // TRANSFER_DST so we can clear it; TRANSFER_SRC so the capture hook can copy it out. sc.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; @@ -249,12 +226,11 @@ private: return true; return false; }; - sc.presentMode = has_mode(VK_PRESENT_MODE_IMMEDIATE_KHR) ? VK_PRESENT_MODE_IMMEDIATE_KHR + sc.presentMode = has_mode(VK_PRESENT_MODE_IMMEDIATE_KHR) ? VK_PRESENT_MODE_IMMEDIATE_KHR : has_mode(VK_PRESENT_MODE_MAILBOX_KHR) ? VK_PRESENT_MODE_MAILBOX_KHR - : VK_PRESENT_MODE_FIFO_KHR; + : VK_PRESENT_MODE_FIFO_KHR; sc.clipped = VK_TRUE; - if (vkCreateSwapchainKHR(device_, &sc, nullptr, &swapchain_) != VK_SUCCESS) - { + if (vkCreateSwapchainKHR(device_, &sc, nullptr, &swapchain_) != VK_SUCCESS) { return false; } std::uint32_t in = 0; @@ -269,29 +245,26 @@ private: VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = qfam_; - if (vkCreateCommandPool(device_, &pci, nullptr, &pool_) != VK_SUCCESS) - { + if (vkCreateCommandPool(device_, &pci, nullptr, &pool_) != VK_SUCCESS) { return false; } VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; ai.commandPool = pool_; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; - if (vkAllocateCommandBuffers(device_, &ai, &cmd_) != VK_SUCCESS) - { + if (vkAllocateCommandBuffers(device_, &ai, &cmd_) != VK_SUCCESS) { return false; } VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; - return vkCreateSemaphore(device_, &si, nullptr, &acquire_sem_) == VK_SUCCESS && - vkCreateSemaphore(device_, &si, nullptr, &submit_sem_) == VK_SUCCESS && - vkCreateFence(device_, &fi, nullptr, &in_flight_) == VK_SUCCESS; + return vkCreateSemaphore(device_, &si, nullptr, &acquire_sem_) == VK_SUCCESS + && vkCreateSemaphore(device_, &si, nullptr, &submit_sem_) == VK_SUCCESS + && vkCreateFence(device_, &fi, nullptr, &in_flight_) == VK_SUCCESS; } - static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, - VkAccessFlags src_access, VkAccessFlags dst_access, VkPipelineStageFlags src_stage, - VkPipelineStageFlags dst_stage) + static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkAccessFlags src_access, + VkAccessFlags dst_access, VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage) { VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.srcAccessMask = src_access; diff --git a/tools/sh_concurrency_repro/main.cpp b/tools/sh_concurrency_repro/main.cpp index 6458605..2681fae 100644 --- a/tools/sh_concurrency_repro/main.cpp +++ b/tools/sh_concurrency_repro/main.cpp @@ -24,8 +24,7 @@ #include -namespace -{ +namespace { safetyhook::InlineHook g_hook; // A real, relocatable, never-inlined target so SafetyHook steals a genuine prologue. @@ -52,17 +51,27 @@ int main(int argc, char** argv) std::atomic stop{false}; std::atomic calls{0}; - std::thread c1([&] { volatile int s = 0; while (!stop.load(std::memory_order_relaxed)) s = target_fn(static_cast(calls.fetch_add(1))); (void)s; }); - std::thread c2([&] { volatile int s = 0; while (!stop.load(std::memory_order_relaxed)) s = target_fn(static_cast(calls.fetch_add(1))); (void)s; }); + std::thread c1([&] { + volatile int s = 0; + while (!stop.load(std::memory_order_relaxed)) + s = target_fn(static_cast(calls.fetch_add(1))); + (void)s; + }); + std::thread c2([&] { + volatile int s = 0; + while (!stop.load(std::memory_order_relaxed)) + s = target_fn(static_cast(calls.fetch_add(1))); + (void)s; + }); std::thread tog; - if (!call_only) - { + if (!call_only) { tog = std::thread([&] { long long n = 0; - while (!stop.load(std::memory_order_relaxed)) - { - if (!g_hook.disable()) {} - if (!g_hook.enable()) {} + while (!stop.load(std::memory_order_relaxed)) { + if (!g_hook.disable()) { + } + if (!g_hook.enable()) { + } ++n; } std::printf("toggles=%lld\n", n); @@ -72,7 +81,8 @@ int main(int argc, char** argv) stop.store(true); c1.join(); c2.join(); - if (tog.joinable()) tog.join(); + if (tog.joinable()) + tog.join(); g_hook = {}; std::printf("survived %lld calls (mode=%s)\n", calls.load(), call_only ? "callonly" : "toggle"); return 0; diff --git a/tools/steam_input_probe/main.cpp b/tools/steam_input_probe/main.cpp index 6a7d0ca..27e8a55 100644 --- a/tools/steam_input_probe/main.cpp +++ b/tools/steam_input_probe/main.cpp @@ -11,16 +11,14 @@ #include "input/steam_input_source.hpp" -namespace -{ +namespace { std::string manifest_path() { char buffer[MAX_PATH] = {}; const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH); std::string path(buffer, len); const std::size_t slash = path.find_last_of("\\/"); - if (slash != std::string::npos) - { + if (slash != std::string::npos) { path.resize(slash + 1); } return path + "steam_input_actions.vdf"; @@ -31,25 +29,21 @@ int main() { coop::SteamInputSource src; const bool ok = src.init(manifest_path()); - std::printf("init=%d steam_active=%d backend=\"%s\"\n", ok ? 1 : 0, src.steam_active() ? 1 : 0, - src.name()); + std::printf("init=%d steam_active=%d backend=\"%s\"\n", ok ? 1 : 0, src.steam_active() ? 1 : 0, src.name()); - for (int frame = 0; frame < 20; ++frame) - { + for (int frame = 0; frame < 20; ++frame) { src.poll(); Sleep(50); } std::printf("steam_controllers=%d\n", src.steam_controllers()); const auto& pads = src.pads(); - for (std::uint32_t i = 0; i < coop::kMaxPads; ++i) - { + for (std::uint32_t i = 0; i < coop::kMaxPads; ++i) { const coop::PadInfo& p = pads[i]; - if (p.connected) - { - std::printf(" slot %u: source=\"%s\" buttons=0x%04X LX=%d LY=%d LT=%u RT=%u\n", i, - p.source.c_str(), p.state.buttons, p.state.thumb_lx, p.state.thumb_ly, - p.state.left_trigger, p.state.right_trigger); + if (p.connected) { + std::printf(" slot %u: source=\"%s\" buttons=0x%04X LX=%d LY=%d LT=%u RT=%u\n", i, p.source.c_str(), + p.state.buttons, p.state.thumb_lx, p.state.thumb_ly, p.state.left_trigger, + p.state.right_trigger); } } diff --git a/tools/vk_validate/main.cpp b/tools/vk_validate/main.cpp index c7d2e3c..e3ac235 100644 --- a/tools/vk_validate/main.cpp +++ b/tools/vk_validate/main.cpp @@ -31,26 +31,21 @@ using namespace coop; -namespace -{ -constexpr const wchar_t* kDefaultExe = - L"G:\\SteamLibrary\\steamapps\\common\\Sphere Spectacle\\sphere.exe"; +namespace { +constexpr const wchar_t* kDefaultExe = L"G:\\SteamLibrary\\steamapps\\common\\Sphere Spectacle\\sphere.exe"; constexpr const wchar_t* kSteamUrl = L"steam://rungameid/1123040"; unsigned long find_pid(const wchar_t* image) { unsigned long pid = 0; HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snap == INVALID_HANDLE_VALUE) - { + if (snap == INVALID_HANDLE_VALUE) { return 0; } PROCESSENTRY32W pe{}; pe.dwSize = sizeof(pe); - for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) - { - if (_wcsicmp(pe.szExeFile, image) == 0) - { + for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) { + if (_wcsicmp(pe.szExeFile, image) == 0) { pid = pe.th32ProcessID; break; } @@ -61,8 +56,7 @@ unsigned long find_pid(const wchar_t* image) void kill_pid(unsigned long pid) { - if (HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid)) - { + if (HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid)) { TerminateProcess(h, 0); CloseHandle(h); } @@ -73,24 +67,21 @@ void kill_pid(unsigned long pid) void layer_register(const std::wstring& image_basename) { wchar_t tmp[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, tmp) != 0) - { + if (GetTempPathW(MAX_PATH, tmp) != 0) { const std::wstring sf = std::wstring(tmp) + L"coop_vk_target.txt"; char utf8[260] = {}; - const int n = WideCharToMultiByte(CP_UTF8, 0, image_basename.c_str(), -1, utf8, sizeof(utf8), nullptr, - nullptr); + const int n = WideCharToMultiByte(CP_UTF8, 0, image_basename.c_str(), -1, utf8, sizeof(utf8), nullptr, nullptr); HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); - if (f != INVALID_HANDLE_VALUE) - { + if (f != INVALID_HANDLE_VALUE) { DWORD wr = 0; WriteFile(f, utf8, n > 0 ? static_cast(n - 1) : 0, &wr, nullptr); // drop the NUL CloseHandle(f); } } HKEY key = nullptr; - if (RegCreateKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers", 0, nullptr, 0, - KEY_SET_VALUE, nullptr, &key, nullptr) == ERROR_SUCCESS) - { + if (RegCreateKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers", 0, nullptr, 0, KEY_SET_VALUE, + nullptr, &key, nullptr) + == ERROR_SUCCESS) { const std::wstring mp = deployed_artifact_path(L"coop_vk_layer.json"); DWORD enabled = 0; RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast(&enabled), sizeof(enabled)); @@ -101,15 +92,13 @@ void layer_register(const std::wstring& image_basename) void layer_unregister() { HKEY key = nullptr; - if (RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers", 0, KEY_SET_VALUE, &key) == - ERROR_SUCCESS) - { + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers", 0, KEY_SET_VALUE, &key) + == ERROR_SUCCESS) { RegDeleteValueW(key, deployed_artifact_path(L"coop_vk_layer.json").c_str()); RegCloseKey(key); } wchar_t tmp[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, tmp) != 0) - { + if (GetTempPathW(MAX_PATH, tmp) != 0) { DeleteFileW((std::wstring(tmp) + L"coop_vk_target.txt").c_str()); } } @@ -117,22 +106,19 @@ void layer_unregister() bool inject(unsigned long pid) { const std::wstring dll = deployed_artifact_path(L"coop_hook.dll"); - HANDLE process = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | - PROCESS_VM_WRITE | PROCESS_VM_READ, + HANDLE process = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION + | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid); - if (process == nullptr) - { + if (process == nullptr) { return false; } const SIZE_T bytes = (dll.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; - if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) - { - auto load = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); - if (HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr)) - { + if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) { + auto load = + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); + if (HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr)) { WaitForSingleObject(th, INFINITE); DWORD code = 0; GetExitCodeThread(th, &code); @@ -140,8 +126,7 @@ bool inject(unsigned long pid) ok = code != 0; } } - if (remote != nullptr) - { + if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); @@ -150,16 +135,14 @@ bool inject(unsigned long pid) SharedBlock* make_ipc(SharedMemory& shm, unsigned long pid) { - if (!shm.create(shared_memory_name(pid), sizeof(SharedBlock))) - { + if (!shm.create(shared_memory_name(pid), sizeof(SharedBlock))) { return nullptr; } auto* b = shm.as(); b->version = kProtocolVersion; b->pad_count = 0; b->sequence.store(0, std::memory_order_relaxed); - for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) - { + for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { b->control.subsystem_disabled[s].store(0, std::memory_order_release); // all on (video included) } b->magic = kProtocolMagic; @@ -170,9 +153,8 @@ ID3D11Device* make_device() { ID3D11Device* dev = nullptr; const D3D_FEATURE_LEVEL fl[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; - if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, - static_cast(std::size(fl)), D3D11_SDK_VERSION, &dev, nullptr, nullptr))) - { + if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, static_cast(std::size(fl)), + D3D11_SDK_VERSION, &dev, nullptr, nullptr))) { return nullptr; } return dev; @@ -195,8 +177,7 @@ bool write_bmp(const std::wstring& path, const std::vector& rgba, ih.biCompression = BI_RGB; ih.biSizeImage = imgsize; HANDLE f = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); - if (f == INVALID_HANDLE_VALUE) - { + if (f == INVALID_HANDLE_VALUE) { return false; } DWORD wr = 0; @@ -205,8 +186,7 @@ bool write_bmp(const std::wstring& path, const std::vector& rgba, std::vector line(row, 0); for (int y = static_cast(h) - 1; y >= 0; --y) // BMP is bottom-up { - for (std::uint32_t x = 0; x < w; ++x) - { + for (std::uint32_t x = 0; x < w; ++x) { const std::uint8_t* p = &rgba[(static_cast(y) * w + x) * 4]; line[x * 3 + 0] = p[2]; // B line[x * 3 + 1] = p[1]; // G @@ -235,46 +215,38 @@ int main(int argc, char** argv) const std::string mode = argc > 1 ? argv[1] : "layer"; const int seconds = argc > 2 ? std::atoi(argv[2]) : 12; std::wstring exe = kDefaultExe; - if (argc > 3) - { + if (argc > 3) { const std::string a = argv[3]; exe.assign(a.begin(), a.end()); } const bool layer_mode = mode != "inject"; - std::printf("== Vulkan backend validation: method=%s game=%ls ==\n", layer_mode ? "layer" : "inject", - exe.c_str()); + std::printf("== Vulkan backend validation: method=%s game=%ls ==\n", layer_mode ? "layer" : "inject", exe.c_str()); int failures = 0; auto check = [&](bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); - if (!ok) - { + if (!ok) { ++failures; } }; // Clean slate. - if (unsigned long old = find_pid(L"sphere.exe")) - { + if (unsigned long old = find_pid(L"sphere.exe")) { kill_pid(old); Sleep(1000); } PROCESS_INFORMATION pi{}; unsigned long pid = 0; - if (layer_mode) - { + if (layer_mode) { layer_register(L"sphere.exe"); ShellExecuteW(nullptr, L"open", kSteamUrl, nullptr, nullptr, SW_SHOWNORMAL); std::printf(" launched via Steam; waiting for sphere.exe...\n"); - for (int i = 0; i < 40 && pid == 0; ++i) - { + for (int i = 0; i < 40 && pid == 0; ++i) { Sleep(500); pid = find_pid(L"sphere.exe"); } - } - else - { + } else { STARTUPINFOW si{}; si.cb = sizeof(si); std::wstring cmd = exe; @@ -284,16 +256,14 @@ int main(int argc, char** argv) const std::size_t slash = exe.find_last_of(L"\\/"); const std::wstring workdir = slash != std::wstring::npos ? exe.substr(0, slash) : std::wstring(); if (!CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, - workdir.empty() ? nullptr : workdir.c_str(), &si, &pi)) - { + workdir.empty() ? nullptr : workdir.c_str(), &si, &pi)) { check(false, "suspended-launch the game exe directly"); return 1; } pid = pi.dwProcessId; } - if (pid == 0) - { + if (pid == 0) { check(false, "game process appeared"); layer_unregister(); return 1; @@ -303,23 +273,19 @@ int main(int argc, char** argv) // Create the IPC block right away so the layer/hook can connect + publish present counts. SharedMemory shm; SharedBlock* block = make_ipc(shm, pid); - if (block == nullptr) - { + if (block == nullptr) { check(false, "create IPC block"); kill_pid(pid); layer_unregister(); return 1; } - if (!layer_mode) - { + if (!layer_mode) { const bool injected = inject(pid); ResumeThread(pi.hThread); check(injected, "inject coop_hook.dll early (pre-vkCreateInstance)"); CloseHandle(pi.hThread); - } - else - { + } else { // The layer does video; also inject coop_hook.dll (late) so its FOCUS subsystem keeps the // game rendering at full rate while unfocused -- the realistic co-injected setup, and // required for a meaningful present-rate measurement (an unfocused game throttles itself, @@ -332,8 +298,7 @@ int main(int argc, char** argv) } ID3D11Device* device = make_device(); - if (device == nullptr) - { + if (device == nullptr) { check(false, "create a D3D11 device to read the shared texture"); kill_pid(pid); layer_unregister(); @@ -350,46 +315,39 @@ int main(int argc, char** argv) std::uint32_t shot_w = 0, shot_h = 0; const DWORD end = GetTickCount() + static_cast(seconds) * 1000; bool alive = true; - while (GetTickCount() < end && (alive = find_pid(L"sphere.exe") == pid)) - { + while (GetTickCount() < end && (alive = find_pid(L"sphere.exe") == pid)) { Sleep(50); const VideoShareView sv = read_share(block); - if (!src.update(sv, pid)) - { + if (!src.update(sv, pid)) { continue; } cap_w = src.width(); cap_h = src.height(); - if (first_gen == 0) - { + if (first_gen == 0) { first_gen = sv.generation; } last_gen = sv.generation; // Sample a few pixels for non-black; grab a full screenshot mid-run. std::uint8_t px[4] = {}; - if (src.read_pixel(cap_w / 2, cap_h / 2, px) && (px[0] | px[1] | px[2]) != 0) - { + if (src.read_pixel(cap_w / 2, cap_h / 2, px) && (px[0] | px[1] | px[2]) != 0) { ++nonblack_frames; } - if (shot.empty() && src.frames_copied() > 10) - { + if (shot.empty() && src.frames_copied() > 10) { src.read_frame(shot, shot_w, shot_h); } } const std::uint64_t presents = block->video.present_calls; std::printf(" captured %ux%u copied=%llu present_calls=%llu gen %u..%u nonblack=%llu\n", cap_w, cap_h, - static_cast(src.frames_copied()), - static_cast(presents), first_gen, last_gen, - static_cast(nonblack_frames)); + static_cast(src.frames_copied()), static_cast(presents), + first_gen, last_gen, static_cast(nonblack_frames)); // Not-applicable skip: some titles produce no Vulkan presents when their exe is suspended-launched // directly (e.g. they refuse to run without a real Steam launch). For those the inject method // can't reach the game and the layer method should be used. (Sphere Spectacle does NOT need this: // it runs fine launched directly with its own folder as the working directory -- see the inject // launch above -- so this branch should not trigger for it.) - if (!layer_mode && presents == 0 && src.frames_copied() == 0) - { + if (!layer_mode && presents == 0 && src.frames_copied() == 0) { std::printf(" the directly-launched exe produced no Vulkan presents -- it may require a real Steam\n" " launch (or failed to initialize), so the suspended-inject method can't reach it here;\n" " use the layer method. (The early-inject mechanism itself is covered by mock_game_test.)\n"); @@ -407,16 +365,12 @@ int main(int argc, char** argv) check(nonblack_frames >= 5, "captured frames are non-black (real image content)"); // Screenshot for visual confirmation of correctness (colors / brightness / no swizzle). - if (!shot.empty()) - { + if (!shot.empty()) { const std::wstring out = exe_directory() + (layer_mode ? L"vk_validate_layer.bmp" : L"vk_validate_inject.bmp"); - if (write_bmp(out, shot, shot_w, shot_h)) - { + if (write_bmp(out, shot, shot_w, shot_h)) { std::printf(" screenshot: %ls (%ux%u)\n", out.c_str(), shot_w, shot_h); } - } - else - { + } else { check(false, "grabbed a screenshot frame"); } @@ -425,8 +379,7 @@ int main(int argc, char** argv) // so the game must keep a healthy present rate while we mirror. The capture is NOT throttled -- // it follows the present rate, which vsync paces -- so the mirror rate tracks the present rate. A // present rate that collapses (the bug was ~3/s) fails here. - if (alive && find_pid(L"sphere.exe") == pid) - { + if (alive && find_pid(L"sphere.exe") == pid) { const std::uint64_t p0 = block->video.present_calls; const std::uint32_t g0 = block->video.generation.load(std::memory_order_acquire); Sleep(3000); @@ -444,8 +397,7 @@ int main(int argc, char** argv) kill_pid(pid); device->Release(); - if (layer_mode) - { + if (layer_mode) { layer_unregister(); } diff --git a/vk_layer/coop_vk_layer.cpp b/vk_layer/coop_vk_layer.cpp index 92efd15..e7d9a19 100644 --- a/vk_layer/coop_vk_layer.cpp +++ b/vk_layer/coop_vk_layer.cpp @@ -38,67 +38,59 @@ #include "vk_capture.hpp" // --- Loader/layer interface (interface version 2) --------------------------- -extern "C" -{ - typedef enum VkLayerFunction_ - { - COOP_VK_LAYER_LINK_INFO = 0, - COOP_VK_LOADER_DATA_CALLBACK = 1, - COOP_VK_LOADER_LAYER_CREATE_DEVICE_CALLBACK = 2, - COOP_VK_LOADER_FEATURES = 3, - } CoopVkLayerFunction; +extern "C" { +typedef enum VkLayerFunction_ { + COOP_VK_LAYER_LINK_INFO = 0, + COOP_VK_LOADER_DATA_CALLBACK = 1, + COOP_VK_LOADER_LAYER_CREATE_DEVICE_CALLBACK = 2, + COOP_VK_LOADER_FEATURES = 3, +} CoopVkLayerFunction; - typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_GetPhysicalDeviceProcAddr)(VkInstance, const char*); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_GetPhysicalDeviceProcAddr)(VkInstance, const char*); - typedef struct VkLayerInstanceLink_ - { - struct VkLayerInstanceLink_* pNext; - PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; - PFN_GetPhysicalDeviceProcAddr pfnNextGetPhysicalDeviceProcAddr; - } VkLayerInstanceLink; +typedef struct VkLayerInstanceLink_ { + struct VkLayerInstanceLink_* pNext; + PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; + PFN_GetPhysicalDeviceProcAddr pfnNextGetPhysicalDeviceProcAddr; +} VkLayerInstanceLink; - typedef struct VkLayerInstanceCreateInfo - { - VkStructureType sType; // 1000000000 = VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO - const void* pNext; - CoopVkLayerFunction function; - union { - VkLayerInstanceLink* pLayerInfo; - void* pfnCallback; // other callbacks (unused here); keeps the union pointer-sized - } u; - } VkLayerInstanceCreateInfo; +typedef struct VkLayerInstanceCreateInfo { + VkStructureType sType; // 1000000000 = VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO + const void* pNext; + CoopVkLayerFunction function; + union { + VkLayerInstanceLink* pLayerInfo; + void* pfnCallback; // other callbacks (unused here); keeps the union pointer-sized + } u; +} VkLayerInstanceCreateInfo; - typedef struct VkLayerDeviceLink_ - { - struct VkLayerDeviceLink_* pNext; - PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; - PFN_vkGetDeviceProcAddr pfnNextGetDeviceProcAddr; - } VkLayerDeviceLink; +typedef struct VkLayerDeviceLink_ { + struct VkLayerDeviceLink_* pNext; + PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr pfnNextGetDeviceProcAddr; +} VkLayerDeviceLink; - typedef struct VkLayerDeviceCreateInfo - { - VkStructureType sType; // 1000000001 = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO - const void* pNext; - CoopVkLayerFunction function; - union { - VkLayerDeviceLink* pLayerInfo; - void* pfnCallback; - } u; - } VkLayerDeviceCreateInfo; +typedef struct VkLayerDeviceCreateInfo { + VkStructureType sType; // 1000000001 = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO + const void* pNext; + CoopVkLayerFunction function; + union { + VkLayerDeviceLink* pLayerInfo; + void* pfnCallback; + } u; +} VkLayerDeviceCreateInfo; - typedef struct VkNegotiateLayerInterface - { - uint32_t sType; // 1 = LAYER_NEGOTIATE_INTERFACE_STRUCT - void* pNext; - uint32_t loaderLayerInterfaceVersion; - PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr; - PFN_vkGetDeviceProcAddr pfnGetDeviceProcAddr; - PFN_GetPhysicalDeviceProcAddr pfnGetPhysicalDeviceProcAddr; - } VkNegotiateLayerInterface; +typedef struct VkNegotiateLayerInterface { + uint32_t sType; // 1 = LAYER_NEGOTIATE_INTERFACE_STRUCT + void* pNext; + uint32_t loaderLayerInterfaceVersion; + PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr pfnGetDeviceProcAddr; + PFN_GetPhysicalDeviceProcAddr pfnGetPhysicalDeviceProcAddr; +} VkNegotiateLayerInterface; } -namespace -{ +namespace { // The loader tags its chain-link structs with small, loader-internal sType values (not the // 1000000000-range): VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO = 47, _DEVICE = 48. These are // from the (unvendored) vk_layer.h and are stable across loader versions. @@ -126,8 +118,7 @@ std::uint32_t g_qfam = 0; VkCapture g_cap; // the shared, off-present-thread read-back -struct SwapInfo -{ +struct SwapInfo { VkSwapchainKHR sc; VkFormat fmt; std::uint32_t w, h; @@ -150,22 +141,18 @@ bool eq(const char* a, const char* b) void logvk(const char* fmt, ...) { static int enabled = -1; - if (enabled < 0) - { + if (enabled < 0) { enabled = GetEnvironmentVariableW(L"COOP_VK_LAYER_LOG", nullptr, 0) != 0 ? 1 : 0; } - if (enabled == 0) - { + if (enabled == 0) { return; } wchar_t dir[MAX_PATH] = {}; - if (GetTempPathW(MAX_PATH, dir) == 0) - { + if (GetTempPathW(MAX_PATH, dir) == 0) { return; } FILE* f = _wfopen((std::wstring(dir) + L"coop_vk_layer.log").c_str(), L"a"); - if (f == nullptr) - { + if (f == nullptr) { return; } va_list ap; @@ -179,20 +166,17 @@ void logvk(const char* fmt, ...) // Decide whether this process is the host's capture target (see file header). bool decide_active() { - if (GetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", nullptr, 0) != 0) - { + if (GetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", nullptr, 0) != 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) { return false; } - HANDLE f = CreateFileW((std::wstring(dir) + L"coop_vk_target.txt").c_str(), GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (f == INVALID_HANDLE_VALUE) - { + HANDLE f = CreateFileW((std::wstring(dir) + L"coop_vk_target.txt").c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (f == INVALID_HANDLE_VALUE) { return false; } char want[MAX_PATH] = {}; @@ -200,12 +184,10 @@ bool decide_active() ReadFile(f, want, sizeof(want) - 1, &got, nullptr); CloseHandle(f); // Trim trailing whitespace/newline. - while (got > 0 && (want[got - 1] == '\n' || want[got - 1] == '\r' || want[got - 1] == ' ')) - { + while (got > 0 && (want[got - 1] == '\n' || want[got - 1] == '\r' || want[got - 1] == ' ')) { want[--got] = '\0'; } - if (got == 0) - { + if (got == 0) { return false; } wchar_t self[MAX_PATH] = {}; @@ -221,10 +203,8 @@ bool decide_active() // can reallocate and dangle the returned pointer). 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; } } @@ -237,20 +217,17 @@ const SwapInfo* find_swap(VkSwapchainKHR sc) void trace_present_rate() { static int enabled = -1; - if (enabled < 0) - { + if (enabled < 0) { enabled = GetEnvironmentVariableW(L"COOP_VK_LAYER_LOG", nullptr, 0) != 0 ? 1 : 0; } - if (enabled == 0) - { + if (enabled == 0) { return; } static LARGE_INTEGER freq{}; static LARGE_INTEGER last{}; static int count = 0; static std::uint64_t last_published = 0; - if (freq.QuadPart == 0) - { + if (freq.QuadPart == 0) { QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&last); } @@ -258,8 +235,7 @@ void trace_present_rate() LARGE_INTEGER now; QueryPerformanceCounter(&now); const double sec = static_cast(now.QuadPart - last.QuadPart) / static_cast(freq.QuadPart); - if (sec >= 1.0) - { + if (sec >= 1.0) { const std::uint64_t pub = g_cap.frames_published(); logvk("present rate %.1f/s captured %.1f/s (game keeps its rate; capture is off the present thread)", count / sec, (pub - last_published) / sec); @@ -272,12 +248,10 @@ void trace_present_rate() VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* pi) { trace_present_rate(); // unconditional so an inactive (pass-through) run gives a baseline to compare - if (g_active && g_ipc.connected()) - { + if (g_active && g_ipc.connected()) { g_ipc.note_present(); } - if (g_active && pi != nullptr && pi->swapchainCount == 1) - { + if (g_active && pi != nullptr && pi->swapchainCount == 1) { // Copy the matched swapchain out under the lock, then capture without holding it. VkImage image = VK_NULL_HANDLE; VkFormat fmt = VK_FORMAT_UNDEFINED; @@ -287,8 +261,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres std::scoped_lock lock(g_swaps_mutex); const SwapInfo* s = find_swap(pi->pSwapchains[0]); const std::uint32_t idx = pi->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; @@ -296,11 +269,9 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres matched = true; } } - if (matched) - { + if (matched) { VkSemaphore chained = VK_NULL_HANDLE; - if (g_cap.present(image, fmt, w, h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained)) - { + if (g_cap.present(image, fmt, w, h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained)) { VkPresentInfoKHR p = *pi; p.waitSemaphoreCount = 1; p.pWaitSemaphores = &chained; @@ -312,7 +283,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres } VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR* ci, - const VkAllocationCallbacks* a, VkSwapchainKHR* out) + const VkAllocationCallbacks* a, VkSwapchainKHR* out) { // Log the game's chosen present mode (= its sync mode) -- 0 IMMEDIATE, 1 MAILBOX, 2 FIFO (vsync), // 3 FIFO_RELAXED. We pass `ci` straight through, so whatever the game asked for is what it gets; @@ -320,8 +291,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V logvk("CreateSwapchain: presentMode=%d (0=IMMEDIATE 1=MAILBOX 2=FIFO 3=FIFO_RELAXED) %ux%u minImageCount=%u", static_cast(ci->presentMode), ci->imageExtent.width, ci->imageExtent.height, ci->minImageCount); const VkResult r = g_real_create_swapchain(device, ci, a, out); - if (g_active && r == VK_SUCCESS && out && g_get_swapchain_images) - { + if (g_active && r == VK_SUCCESS && out && g_get_swapchain_images) { SwapInfo info{}; info.sc = *out; info.fmt = ci->imageFormat; @@ -333,12 +303,11 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V g_get_swapchain_images(device, *out, &n, info.images.data()); { std::scoped_lock lock(g_swaps_mutex); - 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()); } } @@ -381,19 +350,16 @@ void start_capture(VkDevice dev) #undef LOAD f.GetPhysicalDeviceMemoryProperties = reinterpret_cast( g_next_gipa(g_instance, "vkGetPhysicalDeviceMemoryProperties")); - g_get_swapchain_images = - reinterpret_cast(g_next_gdpa(dev, "vkGetSwapchainImagesKHR")); + g_get_swapchain_images = reinterpret_cast(g_next_gdpa(dev, "vkGetSwapchainImagesKHR")); g_cap.init(g_phys, dev, g_qfam, f, GetCurrentProcessId(), [](std::uint32_t w, std::uint32_t h) { // Runs on the reaper thread after each frame is published to the shared texture. Connect the // IPC channel lazily here (the host may not have created it yet at device-create time), then // publish the frame so the host's generation counter advances. - if (!g_ipc_tried.exchange(true)) - { + if (!g_ipc_tried.exchange(true)) { g_ipc.connect(/*attempts=*/40, /*delay_ms=*/25); } - if (g_ipc.connected()) - { + if (g_ipc.connected()) { g_ipc.publish_video_frame(w, h, static_cast(DXGI_FORMAT_R8G8B8A8_UNORM)); } }); @@ -405,13 +371,10 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateDevice(VkPhysicalDevice phys, const V const VkAllocationCallbacks* a, VkDevice* out) { auto* link = reinterpret_cast(const_cast(ci->pNext)); - while (link != nullptr && - !(link->sType == kLoaderDeviceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) - { + while (link != nullptr && !(link->sType == kLoaderDeviceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) { link = reinterpret_cast(const_cast(link->pNext)); } - if (link == nullptr) - { + if (link == nullptr) { logvk("CreateDevice: LINK_INFO not found"); return VK_ERROR_INITIALIZATION_FAILED; } @@ -421,33 +384,29 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateDevice(VkPhysicalDevice phys, const V auto create = reinterpret_cast(next_gipa(g_instance, "vkCreateDevice")); const VkResult r = create(phys, ci, a, out); logvk("CreateDevice: result=%d active=%d", (int)r, g_active ? 1 : 0); - if (r == VK_SUCCESS && out != nullptr && g_device == VK_NULL_HANDLE) - { + if (r == VK_SUCCESS && out != nullptr && g_device == VK_NULL_HANDLE) { g_phys = phys; g_device = *out; g_next_gdpa = next_gdpa; g_qfam = ci->queueCreateInfoCount > 0 ? ci->pQueueCreateInfos[0].queueFamilyIndex : 0; g_real_present = reinterpret_cast(next_gdpa(*out, "vkQueuePresentKHR")); g_real_create_swapchain = reinterpret_cast(next_gdpa(*out, "vkCreateSwapchainKHR")); - if (g_active) - { + if (g_active) { start_capture(*out); } } return r; } -VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo* ci, - const VkAllocationCallbacks* a, VkInstance* out) +VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo* ci, const VkAllocationCallbacks* a, + VkInstance* out) { auto* link = reinterpret_cast(const_cast(ci->pNext)); - while (link != nullptr && - !(link->sType == kLoaderInstanceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) - { + while (link != nullptr + && !(link->sType == kLoaderInstanceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) { link = reinterpret_cast(const_cast(link->pNext)); } - if (link == nullptr) - { + if (link == nullptr) { logvk("CreateInstance: LINK_INFO not found"); return VK_ERROR_INITIALIZATION_FAILED; } @@ -455,8 +414,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo* link->u.pLayerInfo = link->u.pLayerInfo->pNext; // advance the chain auto create = reinterpret_cast(next_gipa(nullptr, "vkCreateInstance")); const VkResult r = create(ci, a, out); - if (r == VK_SUCCESS && out != nullptr) - { + if (r == VK_SUCCESS && out != nullptr) { g_instance = *out; g_next_gipa = next_gipa; g_active = decide_active(); @@ -468,8 +426,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo* VKAPI_ATTR void VKAPI_CALL layer_DestroyDevice(VkDevice device, const VkAllocationCallbacks* a) { auto destroy = reinterpret_cast(g_next_gdpa(device, "vkDestroyDevice")); - if (g_active && device == g_device) - { + if (g_active && device == g_device) { g_cap.shutdown(); // joins the reaper, drains the device, frees the read-back resources { std::scoped_lock lock(g_swaps_mutex); @@ -489,8 +446,7 @@ VKAPI_ATTR void VKAPI_CALL layer_DestroyInstance(VkInstance instance, const VkAl VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gdpa(VkDevice device, const char* name) { - if (name == nullptr) - { + if (name == nullptr) { return nullptr; } if (eq(name, "vkGetDeviceProcAddr")) @@ -506,8 +462,7 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gdpa(VkDevice device, const char* VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gipa(VkInstance instance, const char* name) { - if (name == nullptr) - { + if (name == nullptr) { return nullptr; } if (eq(name, "vkGetInstanceProcAddr")) @@ -526,11 +481,10 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gipa(VkInstance instance, const c } // namespace extern "C" __declspec(dllexport) VkResult VKAPI_CALL - vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVersionStruct) +vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVersionStruct) { logvk("negotiate: requestedVersion=%u", pVersionStruct->loaderLayerInterfaceVersion); - if (pVersionStruct->loaderLayerInterfaceVersion > 2) - { + if (pVersionStruct->loaderLayerInterfaceVersion > 2) { pVersionStruct->loaderLayerInterfaceVersion = 2; } pVersionStruct->pfnGetInstanceProcAddr = layer_gipa; @@ -540,8 +494,7 @@ extern "C" __declspec(dllexport) VkResult VKAPI_CALL } // Also export the entry points directly, for loaders that probe them by name. -extern "C" __declspec(dllexport) PFN_vkVoidFunction VKAPI_CALL coop_vkGetInstanceProcAddr(VkInstance i, - const char* n) +extern "C" __declspec(dllexport) PFN_vkVoidFunction VKAPI_CALL coop_vkGetInstanceProcAddr(VkInstance i, const char* n) { return layer_gipa(i, n); }