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:
@@ -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); });
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user