Apply clang-format across the whole tree

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

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

View File

@@ -25,8 +25,7 @@
#include "coop/shared_memory.hpp"
#include "coop/tool_paths.hpp"
namespace
{
namespace {
// coop_hook.dll ships in the deployable bin/<config>/ root; this probe runs from
// bin/<config>/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<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) {
auto load_library =
reinterpret_cast<LPTHREAD_START_ROUTINE>(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 <pid> [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::LogRing>();
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<DWORD>(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<std::uint8_t> 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<std::uint32_t>(drain.size()))) > 0)
{
if (ring->format_tag == 3 /*IEEE_FLOAT*/ && ring->bits == 32)
{
while ((got = coop::audio_ring_pop(*ring, drain.data(), static_cast<std::uint32_t>(drain.size()))) > 0) {
if (ring->format_tag == 3 /*IEEE_FLOAT*/ && ring->bits == 32) {
const auto* f = reinterpret_cast<const float*>(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<double>(std::abs(f[i])));
}
}
else if (ring->bits == 16)
{
} else if (ring->bits == 16) {
const auto* s = reinterpret_cast<const std::int16_t*>(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<unsigned long long>(produced),
static_cast<unsigned long long>(overruns), static_cast<unsigned long long>(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<unsigned long long>(produced), static_cast<unsigned long long>(overruns),
static_cast<unsigned long long>(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<unsigned long long>(s.frames_rendered),
live ? "<live>" : "");
s.is_primary ? "PRIMARY" : "extra ", s.sample_rate, s.channels, s.bits, s.format_tag,
static_cast<unsigned long long>(s.frames_rendered), live ? "<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<unsigned long long>(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); });

View File

@@ -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<unsigned>(_wtoi(argv[3]));
}
if (argc > 4)
{
if (argc > 4) {
tf.channels = static_cast<unsigned>(_wtoi(argv[4]));
}
if (argc > 5)
{
if (argc > 5) {
tf.bits = static_cast<unsigned>(_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<DWORD>(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");
}

View File

@@ -13,52 +13,42 @@
#include <mmdeviceapi.h>
#include <mmreg.h>
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<void**>(&enum_))))
{
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
reinterpret_cast<void**>(&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<void**>(&client_))))
{
if (FAILED(
endpoint_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&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<void**>(&render_))))
{
if (FAILED(client_->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&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 <typename T> static void rel(T*& p)
private:
template <typename T>
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<const WAVEFORMATEXTENSIBLE*>(mix)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
fmt_.is_float = mix->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (mix->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(mix)->SubFormat
== KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
}
float_ = fmt_.is_float;
}
@@ -184,13 +157,11 @@ private:
wfx.Format.wBitsPerSample = static_cast<WORD>(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<WORD>(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<double>(c));
if (phase_c_[c] > kTwoPi)
{
if (phase_c_[c] > kTwoPi) {
phase_c_[c] -= kTwoPi;
}
}
if (float_)
{
if (float_) {
reinterpret_cast<float*>(data)[i * fmt_.channels + c] = static_cast<float>(sc);
}
else
{
} else {
reinterpret_cast<INT16*>(data)[i * fmt_.channels + c] = static_cast<INT16>(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

View File

@@ -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<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) {
auto load_library =
reinterpret_cast<LPTHREAD_START_ROUTINE>(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<long>(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<long>(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<std::uint8_t>& 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<std::uint8_t>& 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::vector<std::uin
channels = ring->channels;
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<std::uint8_t> scratch(coop::kAudioRingCapacity);
const DWORD cap_end = GetTickCount() + static_cast<DWORD>(seconds) * 1000;
while (GetTickCount() < cap_end)
{
while (GetTickCount() < cap_end) {
std::uint32_t got = coop::audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(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<std::uint32_t>(scratch.size()))) > 0)
{
while ((got = coop::audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(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<unsigned long long>(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<const WAVEFORMATEXTENSIBLE*>(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<std::uint8_t>& 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<std::uint8_t>& 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<std::uint8
const bool ok = cap.start(pid, mix, [&](const BYTE* data, std::uint32_t frames, bool silent) {
const std::size_t bytes = static_cast<std::size_t>(frames) * mix->nBlockAlign;
std::lock_guard<std::mutex> 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<DWORD>(seconds) * 1000);
}
cap.stop();
@@ -429,36 +378,32 @@ bool loopback_capture_pid(unsigned long pid, int seconds, std::vector<std::uint8
int run_listen_mode(const Options& o)
{
const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
std::printf("Listening to pid %lu's render output for %d s (e.g. the live coop_host mirror)...\n",
o.listen, o.seconds);
std::printf("Listening to pid %lu's render output for %d s (e.g. the live coop_host mirror)...\n", o.listen,
o.seconds);
std::vector<std::uint8_t> 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<std::size_t>(pcm.size(), static_cast<std::size_t>(rate) * block * 7 / 10);
const std::size_t skip = std::min<std::size_t>(pcm.size(), static_cast<std::size_t>(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<bool> 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<std::size_t>(pcm.size(), static_cast<std::size_t>(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<std::size_t>(pcm.size(), static_cast<std::size_t>(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<std::uint8_t> 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<std::size_t>(frames) * mix->nBlockAlign;
std::lock_guard<std::mutex> 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<std::size_t>(frames) * mix->nBlockAlign;
std::lock_guard<std::mutex> 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<DWORD>(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<std::size_t>(pcm.size(), static_cast<std::size_t>(rate) *
mix->nBlockAlign * 7 / 10);
const std::size_t skip =
std::min<std::size_t>(pcm.size(), static_cast<std::size_t>(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<unsigned>(_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);
}

View File

@@ -13,21 +13,18 @@
#include <windows.h>
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<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
auto load_library =
reinterpret_cast<LPTHREAD_START_ROUTINE>(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 <pid> <dll_path>\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;
}

View File

@@ -23,8 +23,7 @@
#include "coop/shared_memory.hpp"
#include "coop/tool_paths.hpp"
namespace
{
namespace {
// coop_hook.dll ships in the deployable bin/<config>/ root; this probe runs from
// bin/<config>/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<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) {
auto load_library =
reinterpret_cast<LPTHREAD_START_ROUTINE>(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 <pid> [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::LogRing>();
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<unsigned long long>(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<unsigned long long>(e.calls));

View File

@@ -21,8 +21,7 @@
#include "render_backend.hpp"
#include "tone_source.hpp"
namespace
{
namespace {
std::atomic<bool> 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<bool> 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<LPCSTR>(p), &m) &&
m != nullptr)
{
if (p != nullptr
&& GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(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<unsigned>(std::strtoul(argv[3], nullptr, 10));
audio_fmt.channels = argc > 4 ? static_cast<unsigned>(std::strtoul(argv[4], nullptr, 10)) : 2;
audio_fmt.bits = argc > 5 ? static_cast<unsigned>(std::strtoul(argv[5], nullptr, 10)) : 32;
@@ -139,11 +129,9 @@ int main(int argc, char** argv)
RECT r = {0, 0, static_cast<LONG>(kW), static_cast<LONG>(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<double>(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<ULONGLONG>(seconds * 1000.0))
{
if (seconds > 0.0 && (GetTickCount64() - start) >= static_cast<ULONGLONG>(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);

View File

@@ -1,36 +1,28 @@
#include "render_backend.hpp"
namespace coop::mock
{
namespace coop::mock {
std::unique_ptr<RenderBackend> 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;

View File

@@ -15,8 +15,7 @@
#include <windows.h>
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<std::uint32_t>(r) | (static_cast<std::uint32_t>(g) << 8) |
(static_cast<std::uint32_t>(b) << 16);
return static_cast<std::uint32_t>(r) | (static_cast<std::uint32_t>(g) << 8) | (static_cast<std::uint32_t>(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.

View File

@@ -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<IDirect3D9Ex> d3d;
if (FAILED(Direct3DCreate9Ex(D3D_SDK_VERSION, d3d.GetAddressOf())))
{
if (FAILED(Direct3DCreate9Ex(D3D_SDK_VERSION, d3d.GetAddressOf()))) {
return false;
}
ComPtr<IDirect3DDevice9Ex> 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<IDirect3D9> 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<IDirect3DSurface9> 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<LONG>(p.bar_x);
RECT bar = {bx, 0, bx + static_cast<LONG>(kBarWidth), static_cast<LONG>(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;

View File

@@ -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<std::uint32_t>(r) | (static_cast<std::uint32_t>(g) << 8) |
(static_cast<std::uint32_t>(b) << 16) | (static_cast<std::uint32_t>(a) << 24);
return static_cast<std::uint32_t>(r) | (static_cast<std::uint32_t>(g) << 8) | (static_cast<std::uint32_t>(b) << 16)
| (static_cast<std::uint32_t>(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<std::size_t>(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<UINT>(width_ * 4), 0);
ComPtr<ID3D10Texture2D> 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<std::uint32_t> px_;

View File

@@ -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<ID3D11DeviceContext> ctx0;
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels,
static_cast<UINT>(std::size(levels)), D3D11_SDK_VERSION,
device_.GetAddressOf(), nullptr, ctx0.GetAddressOf())))
{
static_cast<UINT>(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<IDXGIDevice> dxgi_device;
ComPtr<IDXGIAdapter> adapter;
ComPtr<IDXGIFactory2> 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<IDXGIFactory5> 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<ID3D11Texture2D> 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;

View File

@@ -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<IDXGIFactory4> 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<IDXGIFactory5> 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<IDXGISwapChain1> 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);
}

View File

@@ -12,21 +12,17 @@
#include <GL/gl.h>
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<PFN_wglSwapIntervalEXT>(wglGetProcAddress("wglSwapIntervalEXT")))
{
if (auto swap_interval = reinterpret_cast<PFN_wglSwapIntervalEXT>(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;

View File

@@ -10,19 +10,15 @@
#include <volk.h>
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<VkPhysicalDevice> 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<VkQueueFamilyProperties> 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<VkSurfaceFormatKHR> 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;

View File

@@ -24,8 +24,7 @@
#include <safetyhook.hpp>
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<bool> stop{false};
std::atomic<long long> calls{0};
std::thread c1([&] { volatile int s = 0; while (!stop.load(std::memory_order_relaxed)) s = target_fn(static_cast<int>(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<int>(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<int>(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<int>(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;

View File

@@ -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);
}
}

View File

@@ -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<DWORD>(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<const BYTE*>(&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<LPTHREAD_START_ROUTINE>(
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<LPTHREAD_START_ROUTINE>(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<SharedBlock>();
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<UINT>(std::size(fl)), D3D11_SDK_VERSION, &dev, nullptr, nullptr)))
{
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, static_cast<UINT>(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<std::uint8_t>& 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<std::uint8_t>& rgba,
std::vector<std::uint8_t> line(row, 0);
for (int y = static_cast<int>(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<std::size_t>(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<DWORD>(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<unsigned long long>(src.frames_copied()),
static_cast<unsigned long long>(presents), first_gen, last_gen,
static_cast<unsigned long long>(nonblack_frames));
static_cast<unsigned long long>(src.frames_copied()), static_cast<unsigned long long>(presents),
first_gen, last_gen, static_cast<unsigned long long>(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();
}