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:
47
README.md
47
README.md
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user