diff --git a/host/src/audio/audio_format_verifier.cpp b/host/src/audio/audio_format_verifier.cpp index 5085ead..a26b6d8 100644 --- a/host/src/audio/audio_format_verifier.cpp +++ b/host/src/audio/audio_format_verifier.cpp @@ -47,48 +47,12 @@ ScalarFormat resolve(const WAVEFORMATEX* wfx) return f; } -// Decode interleaved PCM (`channels`/`bits`/`tag`) into per-channel float samples, then average to -// mono. Handles float32 and 16/32-bit PCM (the formats WASAPI shared-mode streams use). +// Decode interleaved PCM at the resolved scalar format down to mono float, via the shared +// de-interleaver the correlation core uses (WAVE_FORMAT_* values match its layout tags). std::vector to_mono(const std::vector& bytes, const ScalarFormat& fmt) { std::vector mono; - const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels; - const unsigned bps = fmt.bits / 8; - if (bps == 0) - { - return mono; - } - const std::size_t frame = static_cast(ch) * bps; - const std::size_t frames = bytes.size() / frame; - mono.resize(frames); - const bool is_float = fmt.tag == WAVE_FORMAT_IEEE_FLOAT; - for (std::size_t i = 0; i < frames; ++i) - { - double sum = 0.0; - for (unsigned c = 0; c < ch; ++c) - { - const BYTE* p = bytes.data() + i * frame + static_cast(c) * bps; - float s = 0.0f; - if (is_float && fmt.bits == 32) - { - std::memcpy(&s, p, 4); - } - else if (fmt.bits == 16) - { - std::int16_t v; - std::memcpy(&v, p, 2); - s = v / 32768.0f; - } - else if (fmt.bits == 32) - { - std::int32_t v; - std::memcpy(&v, p, 4); - s = static_cast(v / 2147483648.0); - } - sum += s; - } - mono[i] = static_cast(sum / ch); - } + correlate_detail::decode_layout(bytes.data(), bytes.size(), {fmt.channels, fmt.bits, fmt.tag}, mono); return mono; } @@ -201,7 +165,8 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign const std::size_t hook_frames = dev_block != 0 ? cap.bytes.size() / dev_block : 0; CoTaskMemFree(dev_wfx); - if (const char* dbg = std::getenv("COOP_VERIFY_DEBUG"); dbg != nullptr && dbg[0] == '1') + char dbg[2] = {}; + 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(), @@ -214,7 +179,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign if (recover_layout) { - // Step (b): recover channels + bit depth too, by trying candidate de-interleavings of the + // 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 = correlate_format(cap, loop_mono, dev.rate, standard_audio_rates(), standard_audio_layouts()); @@ -228,7 +193,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign } else { - // Step (a): rate only, assuming the hook layout matches the device (common stereo case), so + // 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); const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates()); diff --git a/host/src/audio/audio_loopback.cpp b/host/src/audio/audio_loopback.cpp index 7171ee7..9261b1b 100644 --- a/host/src/audio/audio_loopback.cpp +++ b/host/src/audio/audio_loopback.cpp @@ -100,6 +100,112 @@ 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 +{ + IMMDeviceEnumerator* enumerator = nullptr; + IMMDevice* endpoint = nullptr; + IAudioClient* client = nullptr; + IAudioRenderClient* render = nullptr; + HANDLE event = nullptr; + UINT32 buffer_frames = 0; + + RenderEndpoint() = default; + RenderEndpoint(const RenderEndpoint&) = delete; + RenderEndpoint& operator=(const RenderEndpoint&) = delete; + + ~RenderEndpoint() + { + if (render) + { + render->Release(); + } + if (client) + { + client->Release(); + } + if (endpoint) + { + endpoint->Release(); + } + if (enumerator) + { + enumerator->Release(); + } + if (event) + { + CloseHandle(event); + } + } + + // Bring up the default endpoint, an (uninitialized) IAudioClient, and the render event. + // On failure reports the failing step through `fail(step, hr)` and returns false. + template + bool activate(Fail&& fail) + { + 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)) + { + fail("GetDefaultAudioEndpoint", hr); + return false; + } + hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&client)); + if (FAILED(hr)) + { + fail("Activate render client", hr); + return false; + } + event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!event) + { + fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError())); + return false; + } + return true; + } + + // Initialize the client shared-mode at `fmt`. Returned (not reported) so the hooked path + // can treat an unrenderable game format as a quiet fall-back rather than an error. + HRESULT initialize(const WAVEFORMATEX* fmt, DWORD flags) + { + constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms, in 100-ns units + return client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kRenderBuffer, 0, fmt, nullptr); + } + + // Wire the render event and fetch the render service + buffer size (post-Initialize). + template + bool wire(Fail&& fail) + { + HRESULT hr = client->SetEventHandle(event); + if (FAILED(hr)) + { + fail("Render SetEventHandle", hr); + return false; + } + hr = client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render)); + if (FAILED(hr)) + { + fail("GetService(RenderClient)", hr); + return false; + } + hr = client->GetBufferSize(&buffer_frames); + if (FAILED(hr)) + { + fail("GetBufferSize", hr); + return false; + } + return true; + } +}; + } // namespace AudioMirror::~AudioMirror() @@ -125,6 +231,13 @@ void AudioMirror::set_status(std::string s) status_ = std::move(s); } +void AudioMirror::set_error(const char* step, HRESULT hr) +{ + char buf[160]; + std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", step, static_cast(hr)); + set_status(buf); +} + void AudioMirror::set_fallback_reason(std::string s) { std::lock_guard lock(status_mutex_); @@ -429,77 +542,32 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) } auto* fmt = reinterpret_cast(&wfx); - IMMDeviceEnumerator* enumerator = nullptr; - IMMDevice* endpoint = nullptr; - IAudioClient* render_client = nullptr; - IAudioRenderClient* render = nullptr; - HANDLE render_event = nullptr; + RenderEndpoint ep; bool started = false; HookedResult result = HookedResult::Stopped; - - auto fail = [&](const char* msg, HRESULT hr) { - char buf[160]; - std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", msg, static_cast(hr)); - set_status(buf); - }; + auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); }; do { - HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); - if (FAILED(hr)) + if (!ep.activate(fail)) { - fail("CoCreateInstance(MMDeviceEnumerator)", hr); break; } - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint); - if (FAILED(hr)) - { - fail("GetDefaultAudioEndpoint", hr); - break; - } - hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(&render_client)); - if (FAILED(hr)) - { - fail("Activate render client", hr); - break; - } - - render_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); - if (!render_event) - { - fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError())); - break; - } - - constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; - hr = render_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, kRenderBuffer, 0, fmt, nullptr); - if (FAILED(hr)) + 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 (FAILED(hr = render_client->SetEventHandle(render_event))) + if (!ep.wire(fail)) { - fail("Render SetEventHandle", hr); - break; - } - if (FAILED(hr = render_client->GetService(__uuidof(IAudioRenderClient), - reinterpret_cast(&render)))) - { - fail("GetService(RenderClient)", hr); - break; - } - UINT32 render_frames = 0; - if (FAILED(hr = render_client->GetBufferSize(&render_frames))) - { - fail("GetBufferSize", hr); break; } + IAudioClient* render_client = ep.client; + IAudioRenderClient* render = ep.render; + const UINT32 render_frames = ep.buffer_frames; sample_rate_.store(rate, std::memory_order_relaxed); channels_.store(channels, std::memory_order_relaxed); @@ -514,9 +582,9 @@ 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 (FAILED(hr = render_client->Start())) + if (const HRESULT hr = render_client->Start(); FAILED(hr)) { - fail("Render Start", hr); + set_error("Render Start", hr); break; } @@ -526,7 +594,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) source_.store(Source::Hooked, std::memory_order_relaxed); running_.store(true, std::memory_order_release); - HANDLE waits[2] = {stop_event_, render_event}; + HANDLE waits[2] = {stop_event_, ep.event}; for (;;) { const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200); @@ -614,27 +682,6 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings) enable_capture(rings, false); } - if (render) - { - render->Release(); - } - if (render_client) - { - render_client->Release(); - } - if (endpoint) - { - endpoint->Release(); - } - if (enumerator) - { - enumerator->Release(); - } - if (render_event) - { - CloseHandle(render_event); - } - if (!started) { // Never got a working render client; let the caller try loopback. Capture is @@ -649,45 +696,20 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) source_.store(Source::Loopback, std::memory_order_relaxed); bool promote = false; - IMMDeviceEnumerator* enumerator = nullptr; - IMMDevice* endpoint = nullptr; - IAudioClient* render_client = nullptr; - IAudioRenderClient* render = nullptr; + RenderEndpoint ep; WAVEFORMATEX* fmt = nullptr; - HANDLE render_event = nullptr; ProcessLoopbackCapture capture; - - auto fail = [&](const char* msg, HRESULT hr) { - char buf[160]; - std::snprintf(buf, sizeof(buf), "%s (0x%08lX)", msg, static_cast(hr)); - set_status(buf); - }; + auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); }; do { - HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); - if (FAILED(hr)) + if (!ep.activate(fail)) { - fail("CoCreateInstance(MMDeviceEnumerator)", hr); - break; - } - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint); - if (FAILED(hr)) - { - fail("GetDefaultAudioEndpoint", hr); - break; - } - hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(&render_client)); - if (FAILED(hr)) - { - fail("Activate render client", hr); break; } // Capture and render share one format (the output endpoint's mix format); // WASAPI converts the captured process audio into it. - hr = render_client->GetMixFormat(&fmt); + HRESULT hr = ep.client->GetMixFormat(&fmt); if (FAILED(hr)) { fail("GetMixFormat", hr); @@ -696,40 +718,19 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) sample_rate_.store(fmt->nSamplesPerSec, std::memory_order_relaxed); channels_.store(fmt->nChannels, std::memory_order_relaxed); - render_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); - if (!render_event) - { - fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError())); - break; - } - - constexpr REFERENCE_TIME kRenderBuffer = 30 * 10000; // 30 ms, in 100-ns units - hr = render_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - kRenderBuffer, 0, fmt, nullptr); + hr = ep.initialize(fmt, AUDCLNT_STREAMFLAGS_EVENTCALLBACK); if (FAILED(hr)) { fail("Render Initialize", hr); break; } - hr = render_client->SetEventHandle(render_event); - if (FAILED(hr)) + if (!ep.wire(fail)) { - fail("Render SetEventHandle", hr); - break; - } - hr = render_client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&render)); - if (FAILED(hr)) - { - fail("GetService(RenderClient)", hr); - break; - } - UINT32 render_frames = 0; - hr = render_client->GetBufferSize(&render_frames); - if (FAILED(hr)) - { - fail("GetBufferSize", hr); break; } + IAudioClient* render_client = ep.client; + IAudioRenderClient* render = ep.render; + const UINT32 render_frames = ep.buffer_frames; const size_t frame_bytes = fmt->nBlockAlign; ByteRing ring; @@ -760,7 +761,7 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) set_status(st); running_.store(true, std::memory_order_release); - HANDLE waits[2] = {stop_event_, render_event}; + HANDLE waits[2] = {stop_event_, ep.event}; for (;;) { const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200); @@ -809,31 +810,10 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring) } while (false); capture.stop(); - - if (render) - { - render->Release(); - } - if (render_client) - { - render_client->Release(); - } - if (endpoint) - { - endpoint->Release(); - } - if (enumerator) - { - enumerator->Release(); - } if (fmt) { CoTaskMemFree(fmt); } - if (render_event) - { - CloseHandle(render_event); - } 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 64d3a27..e62e909 100644 --- a/host/src/audio/audio_loopback.hpp +++ b/host/src/audio/audio_loopback.hpp @@ -2,7 +2,7 @@ // THIS host's audio) carries the real game's sound, re-rendering it on the // default output endpoint. // -// Two source paths (see docs/audio-render-hook-plan.md): +// Two source paths: // - Hooked: the injected render-hook copies the game's frames into a shared // audio ring AND silences the game locally, so there is no echo. Preferred. // - Loopback: WASAPI process-loopback capture of the game (the game still @@ -128,6 +128,7 @@ private: void drain_ops(); // audio thread: post queued operator ops to the session rings bool stop_requested() const; void set_status(std::string s); + void set_error(const char* step, HRESULT hr); // status = " (0x
)" void set_fallback_reason(std::string s); std::thread thread_; diff --git a/host/src/audio/audio_overrides.cpp b/host/src/audio/audio_overrides.cpp index ba4e240..2ededf2 100644 --- a/host/src/audio/audio_overrides.cpp +++ b/host/src/audio/audio_overrides.cpp @@ -23,10 +23,8 @@ std::wstring to_lower(std::wstring s) return s; } -// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and can't -// collide with another name in its high bits. The old `c & 0x7F` mask was lossy and not a true -// inverse of widen; for ASCII names (the common case) UTF-8 is byte-identical, so existing override -// files stay compatible. +// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and +// can't collide with another name; for ASCII names (the common case) UTF-8 is byte-identical. std::string narrow(const std::wstring& w) { if (w.empty()) diff --git a/host/src/audio/audio_overrides.hpp b/host/src/audio/audio_overrides.hpp index 2da8dca..f965a9f 100644 --- a/host/src/audio/audio_overrides.hpp +++ b/host/src/audio/audio_overrides.hpp @@ -5,8 +5,8 @@ // truth). A human-readable one-line-per-game text file: // // # CoopAllTheThings per-game audio overrides -// brotato.exe = 44100 2 32 float -// snb.exe = 48000 2 16 pcm +// = 44100 2 32 float +// = 48000 2 16 pcm #pragma once #include diff --git a/host/src/audio/render_pacer.hpp b/host/src/audio/render_pacer.hpp index 2f3b410..c192d1c 100644 --- a/host/src/audio/render_pacer.hpp +++ b/host/src/audio/render_pacer.hpp @@ -12,11 +12,11 @@ // 3. Re-prime (rebuild the cushion) ONLY on a genuine starvation: the device buffer fully // drained AND the ring is empty. Crucially, do NOT re-prime on a mere partial fill. // -// Rule 3 is the whole point. The original code re-primed whenever it couldn't completely fill -// the free buffer space that tick (`to_write < avail`); that withholds the feed until ~30 ms -// has rebuffered, which DRAINS the device and manufactures the very ~30 ms silence gap it meant -// to avoid -- turning a one-frame ring dip into a full drop-out. On a jittery game that fired -// constantly, producing the choppy / "metallic" mirror audio. coop_audio_validate quantifies it. +// Rule 3 is the whole point. Re-priming whenever a tick can't completely fill the free buffer +// space (`to_write < avail`) withholds the feed until ~30 ms has rebuffered, which DRAINS the +// device and manufactures the very ~30 ms silence gap it means to avoid -- turning a one-frame +// ring dip into a full drop-out. On a jittery game that fires constantly, producing choppy / +// "metallic" mirror audio. coop_audio_validate quantifies the defect. #pragma once #include diff --git a/host/src/audio_panel.cpp b/host/src/audio_panel.cpp index 9f5e232..f21a1c9 100644 --- a/host/src/audio_panel.cpp +++ b/host/src/audio_panel.cpp @@ -298,8 +298,8 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details) // --- Render-stream view ----------------------------------------------- // The hook counts every render stream the game creates, even with mirroring - // off. v1 captures only the first ("primary"); the count makes a multi-stream - // game obvious, and the per-stream table (debug details) shows why. + // off. The count makes a multi-stream game obvious; the per-stream table + // (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) diff --git a/host/src/capture/shared_texture.cpp b/host/src/capture/shared_texture.cpp index d6dd03b..df1da44 100644 --- a/host/src/capture/shared_texture.cpp +++ b/host/src/capture/shared_texture.cpp @@ -93,27 +93,33 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share) return true; } -bool SharedTextureSource::read_frame(std::vector& out, std::uint32_t& w, std::uint32_t& h) +bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr& staging, + D3D11_MAPPED_SUBRESOURCE& map, D3D11_TEXTURE2D_DESC& desc) { if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr) { return false; } - D3D11_TEXTURE2D_DESC desc{}; private_->GetDesc(&desc); D3D11_TEXTURE2D_DESC staging_desc = desc; staging_desc.Usage = D3D11_USAGE_STAGING; staging_desc.BindFlags = 0; staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; staging_desc.MiscFlags = 0; - Microsoft::WRL::ComPtr staging; if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging))) { return false; } ctx_->CopyResource(staging.Get(), private_.Get()); + return SUCCEEDED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map)); +} + +bool SharedTextureSource::read_frame(std::vector& out, std::uint32_t& w, std::uint32_t& h) +{ + Microsoft::WRL::ComPtr staging; D3D11_MAPPED_SUBRESOURCE map{}; - if (FAILED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map))) + D3D11_TEXTURE2D_DESC desc{}; + if (!map_staging_copy(staging, map, desc)) { return false; } @@ -132,30 +138,16 @@ bool SharedTextureSource::read_frame(std::vector& out, std::uint32 bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4]) { - if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr) + Microsoft::WRL::ComPtr staging; + D3D11_MAPPED_SUBRESOURCE map{}; + D3D11_TEXTURE2D_DESC desc{}; + if (!map_staging_copy(staging, map, desc)) { return false; } - D3D11_TEXTURE2D_DESC desc{}; - private_->GetDesc(&desc); if (x >= desc.Width || y >= desc.Height) { - return false; - } - D3D11_TEXTURE2D_DESC staging_desc = desc; - staging_desc.Usage = D3D11_USAGE_STAGING; - staging_desc.BindFlags = 0; - staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - staging_desc.MiscFlags = 0; - Microsoft::WRL::ComPtr staging; - if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging))) - { - return false; - } - ctx_->CopyResource(staging.Get(), private_.Get()); - D3D11_MAPPED_SUBRESOURCE map{}; - if (FAILED(ctx_->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &map))) - { + ctx_->Unmap(staging.Get(), 0); return false; } const auto* px = static_cast(map.pData) + static_cast(y) * map.RowPitch + diff --git a/host/src/capture/shared_texture.hpp b/host/src/capture/shared_texture.hpp index 273bfa9..6d782e0 100644 --- a/host/src/capture/shared_texture.hpp +++ b/host/src/capture/shared_texture.hpp @@ -68,6 +68,10 @@ public: 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`. + bool map_staging_copy(Microsoft::WRL::ComPtr& staging, D3D11_MAPPED_SUBRESOURCE& map, + D3D11_TEXTURE2D_DESC& desc); Microsoft::WRL::ComPtr device_; Microsoft::WRL::ComPtr ctx_; diff --git a/host/src/inject/dll_probe.cpp b/host/src/inject/dll_probe.cpp index 4c8ea9e..9d02751 100644 --- a/host/src/inject/dll_probe.cpp +++ b/host/src/inject/dll_probe.cpp @@ -15,8 +15,8 @@ bool hook_dll_alive(unsigned long pid, int timeout_ms) // The per-pid section exists only while someone holds it; a connected DLL keeps it alive across // a host restart. Open it (don't create), then confirm the DLL's worker is actually beating -- // a stale section with a dead worker (heartbeat frozen) must read as not-alive so we inject fresh. - // Poll rather than sample once: the worker only beats ~every 250 ms, so a single short read can - // straddle a gap and miss it; return the instant a beat lands, and give up after the timeout. + // 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))) { diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 277031c..272d8f7 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -9,6 +9,7 @@ #include "inject/dll_probe.hpp" #include "inject/injector.hpp" #include "ui/app_chrome.hpp" +#include "ui/text_match.hpp" namespace coop { @@ -32,41 +33,15 @@ std::string narrow(const std::wstring& w) return out; } -// Case-insensitive equality of two image names (e.g. "snb.exe"). +// Case-insensitive equality of two image names (e.g. "game.exe"). bool iequals_name(const std::wstring& a, const std::wstring& b) { - const std::string x = narrow(a), y = narrow(b); - if (x.size() != y.size()) - { - return false; - } - for (std::size_t i = 0; i < x.size(); ++i) - { - if (::tolower(static_cast(x[i])) != ::tolower(static_cast(y[i]))) - { - return false; - } - } - return true; + return _wcsicmp(a.c_str(), b.c_str()) == 0; } -bool contains_ci(const std::wstring& haystack, const char* needle_utf8) +bool contains_ci_w(const std::wstring& haystack, const char* needle_utf8) { - if (needle_utf8 == nullptr || needle_utf8[0] == '\0') - { - return true; - } - const std::string hay = narrow(haystack); - std::string h = hay, n = needle_utf8; - for (char& c : h) - { - c = static_cast(::tolower(static_cast(c))); - } - for (char& c : n) - { - c = static_cast(::tolower(static_cast(c))); - } - return h.find(n) != std::string::npos; + return contains_ci(narrow(haystack), needle_utf8); } // Absolute path to coop_hook.dll, assumed to sit next to the host executable. @@ -517,8 +492,8 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status) ImGui::PopID(); } - // Cursor release is a Focus sub-option for games that clip/recenter the mouse - // (e.g. Trails through Daybreak), which would otherwise trap the operator. + // 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_)) { server_.set_cursor_clip_allowed(!release_cursor_); @@ -690,7 +665,7 @@ void InjectionPanel::draw(bool debug_details) { for (const WindowEntry& w : windows_) { - if (!contains_ci(w.title, window_filter_) && !contains_ci(w.exe_name, window_filter_)) + if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_)) { continue; } @@ -718,7 +693,7 @@ void InjectionPanel::draw(bool debug_details) { for (const ProcessEntry& entry : processes_) { - if (!contains_ci(entry.exe_name, filter_)) + if (!contains_ci_w(entry.exe_name, filter_)) { continue; } diff --git a/host/src/log_panel.cpp b/host/src/log_panel.cpp index 62d1ac2..3667ae7 100644 --- a/host/src/log_panel.cpp +++ b/host/src/log_panel.cpp @@ -1,37 +1,16 @@ #include "log_panel.hpp" -#include -#include #include -#include -#include #include "imgui.h" #include "injection_panel.hpp" #include "ui/app_chrome.hpp" +#include "ui/text_match.hpp" namespace coop { -namespace -{ -// Case-insensitive substring test, so the log filter matches regardless of case (consistent with the -// rest of the app, where "error" should find "ERROR"). -bool icontains(const std::string& hay, const char* needle) -{ - if (needle == nullptr || needle[0] == '\0') - { - return true; - } - auto lower = [](std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - return s; - }; - return lower(hay).find(lower(needle)) != std::string::npos; -} -} // namespace - void LogPanel::add_line(const LogRecord& rec) { if (first_millis_ == 0) @@ -76,7 +55,7 @@ void LogPanel::draw() const bool has_filter = filter_[0] != '\0'; for (const Line& line : lines_) { - if (has_filter && !icontains(line.text, filter_)) + if (has_filter && !contains_ci(line.text, filter_)) { continue; } diff --git a/host/src/ui/text_match.hpp b/host/src/ui/text_match.hpp new file mode 100644 index 0000000..2284562 --- /dev/null +++ b/host/src/ui/text_match.hpp @@ -0,0 +1,31 @@ +// Small case-insensitive text helpers shared by the host panels' filter boxes, so +// "error" matches "ERROR" everywhere consistently. ASCII-fold only (the UI filters +// are typed ASCII); non-ASCII bytes compare as-is. +#pragma once + +#include +#include + +namespace coop +{ + +inline std::string ascii_lower(std::string s) +{ + for (char& c : s) + { + c = static_cast(std::tolower(static_cast(c))); + } + return 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') + { + return true; + } + return ascii_lower(haystack).find(ascii_lower(needle)) != std::string::npos; +} + +} // namespace coop