Phase 2: Present-hook video path (shared-texture mirror)

Add an injected IDXGISwapChain::Present / Present1 hook as a lower-latency,
border-free alternative to WGC. The hook copies the swapchain backbuffer into a
shared keyed-mutex texture (coop_video_<pid>); the host opens it by name and
samples it. New opt-in HookSubsys_Video (protocol v6 -> v7); the Video mirror
panel gains a WGC vs Hooked source toggle that installs/removes the subsystem.

Verified by present_hook_test (drives a real D3D11 swapchain end-to-end and reads
the rendered pixels back through the shared texture) and against Phantom Brave
(D3D9: hook installs cleanly and stays idle, WGC fallback). All 5 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:34:12 +02:00
parent 9557b9ca69
commit 36b861d167
21 changed files with 1160 additions and 36 deletions

View File

@@ -23,6 +23,7 @@ XInput game becomes Remote-Play-Together-able.
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` | done |
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` | done |
| Mirror video | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` | done |
| Mirror video (alt) | Injected Present-hook copies the DXGI backbuffer into a shared keyed-mutex texture the host samples (lower latency, no capture border) | `coop_hook.dll` + `coop_host.exe` | done |
| Mirror audio | Injected render-hook copies the game's WASAPI frames into a shared ring and silences the game locally (no echo); WASAPI process loopback is the automatic fallback | `coop_hook.dll` + `coop_host.exe` | done |
| Host ↔ hook IPC | Named shared memory (seqlock for input, status back-channel) | `common/` | done |
@@ -131,11 +132,19 @@ 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).
Future work, roughly in priority order:
- **Present-hook video path:** capture the game's frames by hooking
`IDXGISwapChain::Present` in the injected DLL and sharing the backbuffer via a
shared D3D11 texture, as a lower-latency / more stable alternative to WGC.
- **Steam Input:** consume guest input through the Steam Input API directly
rather than XInput.
- **x86 support:** add an x86 build of `coop_hook.dll` plus an x86 injector
@@ -185,6 +194,12 @@ 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.
- **`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,
the backbuffer reached the shared keyed-mutex texture, and a second device can
open it by name and read the exact pixels back. Skips cleanly if the machine has
no D3D11 device.
- **`audio_loopback_test`** — spawns `coop_tone.exe` (a standalone WASAPI
sine-wave source under [`tools/audio_tone`](tools/audio_tone)) and verifies the
shipping process-loopback capture (the fallback path) receives its audio by
@@ -223,7 +238,11 @@ person/account to receive the stream.
**Attached**, a non-zero **XInput polled: N/s**, and **Focus spoof: active**.
3. **Mirror video:** in the **Video mirror** panel, tick **Mirror game window** —
the host window now shows a live, letterboxed copy of the game.
the host window now shows a live, letterboxed copy of the game. **Source**
picks how the frames are grabbed: **WGC** (default, Windows Graphics Capture —
works for any window) or **Hooked (Present)** (the injected Present-hook's
shared texture — lower latency and no capture border, but only for DXGI /
D3D11 games; selecting it installs the video subsystem in the game).
4. **Mirror audio:** in the **Audio mirror** panel, tick **Mirror game audio**.
With the hook injected, **Source** shows **Hooked (no echo)** and the game's