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>
This commit is contained in:
2026-06-22 12:03:43 +02:00
parent 67c1940d52
commit e996188aec
8 changed files with 154 additions and 17 deletions

View File

@@ -137,21 +137,10 @@ Conventions for every milestone below:
panel so it fits (content can be moved between columns / rows — the most detailed case
should still fit). Land an automated harness check (drive-to-max → screenshot → assert no
overflow) so later milestones that add UI keep it green. *Independent of the backend work;
every milestone below must preserve this test* (M3's red banner and Vulkan-layer checkbox
every milestone below must preserve this test* (M2's red banner and Vulkan-layer checkbox
in particular).
- **M2 — OpenGL (mock → capture coverage).**
1. **Mock backend** (`render_gl.cpp`). Raw WGL context (`wglCreateContextAttribsARB`) with the
**glad** loader (new submodule, `Dav1dde/glad`). Background via `glClearColor`/`glClear`;
bar + block via `glScissor` + clear (shader-free GL 1.x). GL's framebuffer is
**bottom-left** origin, so the counter block is placed flipped so the captured (top-left)
pixel still decodes — matching the existing `glReadPixels` capture flip. Adds the
`.gitmodules` entry + the `coop_require_submodule()` check.
2. **Capture coverage.** The GL `SwapBuffers`/`wglSwapBuffers` + `glReadPixels` path already
ships; add a mock-backed regression that decodes the mock's frames through it (upgrading the
synthetic `opengl_hook_test` to a real animated game). Small.
- **M3 — Vulkan (mock → capture).** The largest.
- **M2 — Vulkan (mock → capture).** The largest.
1. **Mock backend** (`render_vk.cpp`). New submodules **Vulkan-Headers**
(`KhronosGroup/Vulkan-Headers`, official) + **volk** (`zeux/volk`); raw
`vkCreateWin32SurfaceKHR`, swap chain, per-frame acquire → clear → present. Background via
@@ -296,7 +285,7 @@ ctest --test-dir build -C Debug --output-on-failure
endpoint format). Skips cleanly if the machine has no audio endpoint.
- **`mock_game_test`** — comprehensive capture/audio/hook stress test against
**`coop_mock_game`** (an animated, frame-numbered A/V test game under
[`tools/mock_game`](tools/mock_game) with selectable **DX9 / DX9Ex / DX10 / DX11 / DX12** backends and a
[`tools/mock_game`](tools/mock_game) with selectable **DX9 / DX9Ex / DX10 / DX11 / DX12 / OpenGL** backends and a
configurable WASAPI tone). It launches the game, injects `coop_hook.dll`, opens the
hook's shared video texture, and **decodes the frame number out of the captured pixels**
to assert the mirror sees a *monotonic, advancing* sequence for each backend (the bar
@@ -457,6 +446,13 @@ Non-obvious things that cost time and constrain the design:
so **one path serves both** — the GPU shared-surface fast path the plan sketched for D3D9Ex
wasn't worth it (D3D9 games are light enough that the read-back cost is fine, and it dodges the
cross-device keyed-mutex-less sync of a legacy shared surface).
- **The OpenGL mock needs no GL loader.** Drawing the animated pattern with scissored clears
(`glClear` + `glScissor`) only touches **GL 1.1**, which `opengl32` exports directly — so the
planned **glad** submodule wasn't needed (a legacy `wglCreateContext` + `<GL/gl.h>` suffices).
GL's framebuffer is **bottom-left** origin and the capture flips it top-down, so the mock draws
the frame-counter block at the GL *top* (`y = h - block`) to land at the captured image's
top-left. The GL `SwapBuffers` hook now also bumps the shared present counter (it's the GL
present), so the Video panel's present rate works for GL games too.
- **A render client that predates our injection has no knowable format — measure it.**
We inject into already-running games, so we usually never see the game's
`IAudioClient::Initialize`; the render-hook then assumes the device mix format for that