Files
CoopAllTheThings/tools/mock_game/main.cpp
BlackMark e996188aec M2(OpenGL): GL mock backend (no loader) + mock-backed capture coverage
Add render_gl.cpp (selectable as `gl`): renders the animated pattern with
scissored clears (glClear + glScissor -- GL 1.1, exported straight from
opengl32) and presents with SwapBuffers. No glad/submodule needed: a legacy
wglCreateContext + <GL/gl.h> suffices, so the planned loader dependency was
dropped. GL is bottom-left origin and the capture flips top-down, so the
frame-counter block is drawn at the GL top to land top-left in the captured
image. Window class gains CS_OWNDC for a stable GL DC; best-effort vsync via a
runtime wglSwapIntervalEXT lookup.

The GL SwapBuffers/wglSwapBuffers hook now bumps the shared present counter
(it's the GL present), so present_calls works for GL games too. mock_game_test
decodes GL frames through the existing glReadPixels capture path; 15/15 ctest.
Roadmap: OpenGL milestone done and removed (Vulkan renumbered to M2);
architecture/lessons/test docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:03:43 +02:00

149 lines
4.4 KiB
C++

// CoopMockGame -- a tiny test "game" used to exercise the capture + audio + hook paths.
//
// coop_mock_game.exe [dx9|dx9ex|dx10|dx11|dx12|gl] [seconds] [rate] [channels] [bits] [pcm|float]
//
// It opens a normal visible window and renders an animated, frame-numbered pattern (see
// render_backend.hpp): a moving bar + per-frame background colour make motion obvious, and
// a top-left block encodes the exact frame number so a capture test can decode it and
// detect dropped / duplicated / stale frames. With audio args it also plays a configurable
// WASAPI tone (the same ToneSource coop_tone uses), so it doubles as an A/V source. seconds
// = 0 runs until the window is closed. Console app so it can print its pid + status.
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <string>
#include <thread>
#include <windows.h>
#include "render_backend.hpp"
#include "tone_source.hpp"
namespace
{
std::atomic<bool> g_running{true};
LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
if (msg == WM_DESTROY)
{
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wparam, 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)))
{
return;
}
coop::tone::ToneSource tone;
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))
{
tone.render_step(200);
}
}
else
{
std::printf("MOCK_GAME audio: failed to open requested format\n");
}
tone.close();
CoUninitialize();
}
} // namespace
int main(int argc, char** argv)
{
const std::string backend_name = argc > 1 ? argv[1] : "dx11";
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)
{
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;
audio_fmt.is_float = argc > 6 ? (std::string(argv[6]) == "float") : (audio_fmt.bits == 32);
}
constexpr std::uint32_t kW = 640, kH = 480;
const HINSTANCE inst = GetModuleHandleW(nullptr);
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.style = CS_OWNDC; // a stable private DC, so the OpenGL backend can keep one GL context
wc.lpfnWndProc = wnd_proc;
wc.hInstance = inst;
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.lpszClassName = L"CoopMockGameWindow";
RegisterClassExW(&wc);
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)
{
std::printf("MOCK_GAME error: CreateWindow failed\n");
return 1;
}
auto backend = coop::mock::RenderBackend::create(backend_name);
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::fflush(stdout);
std::thread audio;
if (want_audio)
{
audio = std::thread(audio_thread, audio_fmt);
}
const ULONGLONG start = GetTickCount64();
std::uint32_t frame = 0;
for (;;)
{
MSG msg;
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))
{
break;
}
backend->render_and_present(frame++);
if (seconds > 0.0 && (GetTickCount64() - start) >= static_cast<ULONGLONG>(seconds * 1000.0))
{
break;
}
}
g_running.store(false, std::memory_order_relaxed);
if (audio.joinable())
{
audio.join();
}
std::printf("MOCK_GAME done: %u frames\n", frame);
return 0;
}