Add OpenGL capture path for the hooked video mirror

The Present hook never fired in Phantom Brave because it's an OpenGL game
(OPENGL32.dll loaded, IDXGISwapChain::Present calls=0), so the hooked video source
showed no image. Add an OpenGL producer under the video subsystem: inline-hook
gdi32!SwapBuffers + opengl32!wglSwapBuffers (with a re-entrancy guard, since
SwapBuffers calls wglSwapBuffers), glReadPixels the backbuffer, flip it, and upload
it into the same shared keyed-mutex texture the host already samples -- so the host
is unchanged. DXGI games still hit the Present hook; both producers are installed
and whichever the game uses fills the texture.

Validated by opengl_hook_test (real GL context, clears to a known color, reads the
exact pixels back through the shared texture) and against Phantom Brave (SwapBuffers
~75/s, present=0, shared texture 1920x1080, generation advancing). Vulkan
(vkQueuePresentKHR) still needs WGC -- documented. All 7 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:33:47 +02:00
parent 914dba4d25
commit 9977ad5d5a
7 changed files with 649 additions and 18 deletions

View File

@@ -136,19 +136,30 @@ Done:
Replaces tailing `%TEMP%\coop_hook.log` (which stays as an opt-in file mirror).
`coop_audio_probe` drains and prints the same stream headless.
- **Present-hook video path. ✅** The injected DLL hooks `IDXGISwapChain::Present`
(and `Present1`) and copies the swapchain backbuffer into a shared keyed-mutex
texture (`coop_video_<pid>`); the host opens it by name and samples it — a
lower-latency, border-free alternative to WGC. It's an opt-in subsystem (the
**Video Present-hook** checkbox in the Injection panel, or just pick **Source:
Hooked (Present)** in the Video mirror panel, which installs it). Only DXGI
swapchains with an `ID3D11Texture2D` backbuffer are caught (the common D3D11
case); D3D9 / pure-D3D12 games keep WGC. Validated by `present_hook_test`
(drives a real D3D11 swapchain end-to-end and reads the pixels back through the
shared texture). The host samples the shared copy as plain UNORM (see
`srgb_to_unorm`) so games with an `*_SRGB` backbuffer (e.g. Life is Strange:
Before the Storm, whose backbuffer is `R8G8B8A8_UNORM_SRGB`) mirror with correct
brightness instead of being darkened by an sRGB→linear decode.
- **Hooked video path (Present + OpenGL). ✅** The injected DLL captures the
game's frames into a shared keyed-mutex texture (`coop_video_<pid>`) that the
host opens by name and samples — a lower-latency, border-free alternative to WGC.
It's an opt-in subsystem (the **Video Present-hook** checkbox in the Injection
panel, or just pick **Source: Hooked (Present)** in the Video mirror panel, which
installs it). Two producers cover the common graphics APIs:
- **Direct3D (DXGI):** hooks `IDXGISwapChain::Present` / `Present1` and copies
the backbuffer. Catches D3D10/11/12 games whose backbuffer is an
`ID3D11Texture2D` (the common D3D11 case). Validated by `present_hook_test`
and against Slaps and Beans (32-bit D3D11) and Life is Strange: Before the
Storm. The host samples the copy as plain UNORM (see `srgb_to_unorm`) so games
with an `*_SRGB` backbuffer (Life is Strange is `R8G8B8A8_UNORM_SRGB`) mirror
with correct brightness instead of being darkened by an sRGB→linear decode.
- **OpenGL:** hooks `SwapBuffers` / `wglSwapBuffers`, reads the backbuffer with
`glReadPixels`, and uploads it into the shared texture. Catches OpenGL games
that never touch DXGI (e.g. **Phantom Brave**, which is OpenGL — that's why
the Present hook alone showed no image). Validated by `opengl_hook_test` and
against Phantom Brave. `glReadPixels` forces a per-frame GPU→CPU readback, so
it's heavier than the D3D copy, but fine for the typically-2D OpenGL titles.
**Vulkan** games (present via `vkQueuePresentKHR`) aren't hooked yet — that needs
a Vulkan layer / device-dispatch hook plus a `vkCmdCopyImage` to a readable
image, a larger effort. **Use WGC** (the default Video source) for Vulkan, D3D9,
or anything the hooked path doesn't capture — WGC works for any window.
- **x86 (32-bit) game support. ✅** A nested Win32 sub-build (CMake
`ExternalProject`, driven from the normal x64 build) produces `coop_hook_x86.dll`
@@ -174,7 +185,8 @@ Done:
All planned phases are now implemented. Possible later work: per-stream audio
mixing for multi-stream games, per-stream format detection for the audio hook, and
hooking D3D9 / pure-D3D12 present paths for the video hook.
hooking the remaining present paths for the video hook (Vulkan `vkQueuePresentKHR`,
D3D9, pure-D3D12) — WGC already covers those today.
## Building
@@ -229,6 +241,13 @@ ctest --test-dir build -C Debug --output-on-failure
COM vtables were discovered, the frames reached the ring (non-silent), the
primary stream was silenced, and exactly one render stream was counted. Skips
cleanly if the machine has no audio endpoint.
- **`srgb_format_test`** — unit test of the `srgb_to_unorm` mapping the hooked
video path uses so `*_SRGB`-backbuffer games aren't darkened. No device.
- **`opengl_hook_test`** — in-process self-test of the OpenGL capture path:
installs the swap hooks, drives a real OpenGL context (clears the backbuffer to a
known color, calls `SwapBuffers`), and asserts the detour fired, the frame was
`glReadPixels`'d into the shared texture, and a second device reads the exact
pixels back by name. Skips cleanly without an OpenGL / D3D11 device.
- **`present_hook_test`** — in-process self-test of the Present-hook video path:
installs the hook, drives a real D3D11 swapchain in the same process (clears the
backbuffer to a known color and calls `Present`), and asserts the detour fired,