Deduplicate the host audio/UI code and scrub history from comments
Extract a RAII RenderEndpoint that owns the enumerator/endpoint/client/ render-event/buffer for both AudioMirror::run_hooked and run_loopback, replacing two hand-rolled setup+teardown blocks and their two identical fail lambdas with one set_error helper; every COM object now frees on each exit path automatically. Route audio_format_verifier's mono decode through the shared correlate_detail::decode_layout instead of a second copy, and read the debug env var via GetEnvironmentVariableA (drops the getenv C4996). Fold the near-identical read_frame/read_pixel staging-copy setup in SharedTextureSource into one map_staging_copy, and the three separate case-insensitive filter helpers (log/injection panels) into ui/text_match.hpp. Comments: drop game-name anecdotes and "the old code"/version phrasing; genericize the override-file example; fix a stale heartbeat-interval note. Behavior unchanged (host tests + mock_game_test pass).
This commit is contained in:
@@ -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<float> to_mono(const std::vector<BYTE>& bytes, const ScalarFormat& fmt)
|
||||
{
|
||||
std::vector<float> 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<std::size_t>(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<std::size_t>(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<float>(v / 2147483648.0);
|
||||
}
|
||||
sum += s;
|
||||
}
|
||||
mono[i] = static_cast<float>(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<float> hook_mono = to_mono(cap.bytes, dev);
|
||||
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, dev.rate, standard_audio_rates());
|
||||
|
||||
@@ -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 <typename Fail>
|
||||
bool activate(Fail&& fail)
|
||||
{
|
||||
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&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<void**>(&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 <typename Fail>
|
||||
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<void**>(&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<unsigned long>(hr));
|
||||
set_status(buf);
|
||||
}
|
||||
|
||||
void AudioMirror::set_fallback_reason(std::string s)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(status_mutex_);
|
||||
@@ -429,77 +542,32 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
||||
}
|
||||
auto* fmt = reinterpret_cast<WAVEFORMATEX*>(&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<unsigned long>(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<void**>(&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<void**>(&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<void**>(&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<BYTE> temp(static_cast<size_t>(render_frames) * frame_bytes);
|
||||
std::vector<float> acc(static_cast<size_t>(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<unsigned long>(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<void**>(&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<void**>(&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<void**>(&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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = "<step> (0x<hr>)"
|
||||
void set_fallback_reason(std::string s);
|
||||
|
||||
std::thread thread_;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
// <image.exe> = 44100 2 32 float
|
||||
// <image.exe> = 48000 2 16 pcm
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -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 <algorithm>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -93,27 +93,33 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SharedTextureSource::read_frame(std::vector<std::uint8_t>& out, std::uint32_t& w, std::uint32_t& h)
|
||||
bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2D>& 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<ID3D11Texture2D> 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<std::uint8_t>& out, std::uint32_t& w, std::uint32_t& h)
|
||||
{
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> 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<std::uint8_t>& 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<ID3D11Texture2D> 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<ID3D11Texture2D> 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<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch +
|
||||
|
||||
@@ -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<ID3D11Texture2D>& staging, D3D11_MAPPED_SUBRESOURCE& map,
|
||||
D3D11_TEXTURE2D_DESC& desc);
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Device1> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> ctx_;
|
||||
|
||||
@@ -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)))
|
||||
{
|
||||
|
||||
@@ -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<unsigned char>(x[i])) != ::tolower(static_cast<unsigned char>(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<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
for (char& c : n)
|
||||
{
|
||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(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;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
#include "log_panel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#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<char>(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;
|
||||
}
|
||||
|
||||
31
host/src/ui/text_match.hpp
Normal file
31
host/src/ui/text_match.hpp
Normal file
@@ -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 <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
inline std::string ascii_lower(std::string s)
|
||||
{
|
||||
for (char& c : s)
|
||||
{
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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
|
||||
Reference in New Issue
Block a user