Add DX12 hooked capture via a D3D11On12 bridge

D3D12 games (e.g. Spider-Man: Miles Morales) fire the Present hook -- the DXGI
swapchain's Present is the same vtable function for D3D11 and D3D12 -- but
GetBuffer(0) as ID3D11Texture2D fails, so the hook used to idle. Now, when the D3D11
GetBuffer fails, present_hook bridges via D3D11On12: it gets the game's ID3D12Device
from the backbuffer, creates its own DIRECT command queue on it (no need to hook the
game's ExecuteCommandLists), builds an ID3D11On12Device, CreateWrappedResource's the
D3D12 backbuffer, and CopyResource's it into the existing shared keyed-mutex texture
-- so the host side is unchanged. The bridge is created lazily and torn down with the
hook.

Verified with a new in-process dx12_present_hook_test: it drives a real D3D12
swapchain (clears a backbuffer, Presents) and asserts present fired, the backbuffer
was bridged into the shared texture, and a second device reads the exact color back
by name -- {51,102,153,255}. Full build x64 + x86 clean (the x86 hook compiles the
D3D12 path too); ctest x64 10/10, x86 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 05:32:15 +02:00
parent 5e05d38be8
commit dfd2be8c17
4 changed files with 513 additions and 19 deletions

View File

@@ -33,13 +33,14 @@ and forwards guest controllers back into it.
| Host ↔ hook IPC | Named shared memory (seqlock for input, status back-channel, video/audio/log shares) | `common/` |
The hooked video path has two producers: **Direct3D (DXGI)** hooks
`IDXGISwapChain::Present` / `Present1` and copies the backbuffer (D3D10/11 games
whose backbuffer is an `ID3D11Texture2D`); **OpenGL** hooks
`SwapBuffers` / `wglSwapBuffers` and reads the backbuffer with `glReadPixels` (for
games that never touch DXGI, e.g. Phantom Brave). The host samples the copy as
plain UNORM (`srgb_to_unorm`) so `*_SRGB`-backbuffer games mirror at correct
brightness. **WGC remains the default** and covers anything the hooked path
doesn't (Vulkan, D3D9, DX12 — see Roadmap).
`IDXGISwapChain::Present` / `Present1` and copies the backbuffer — directly for
D3D10/11 games (the backbuffer is an `ID3D11Texture2D`), and via a **D3D11On12
bridge** for D3D12 games (wrap the `ID3D12Resource` backbuffer, `CopyResource` into
the shared texture); **OpenGL** hooks `SwapBuffers` / `wglSwapBuffers` and reads the
backbuffer with `glReadPixels` (for games that never touch DXGI, e.g. Phantom
Brave). The host samples the copy as plain UNORM (`srgb_to_unorm`) so
`*_SRGB`-backbuffer games mirror at correct brightness. **WGC remains the default**
and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
## Limitations
@@ -76,13 +77,6 @@ is removed from this list once done — so the top item is always next. The
self-verifiable tooling / UI / input items come first; the game-pipeline items that
need a real game (and Remote Play) to fully validate come last.
- **DX12 hooked capture (Spider-Man: Miles Morales).** Miles Morales is D3D12, so
the Present hook fires but `GetBuffer(0)` as `ID3D11Texture2D` fails (the
backbuffer is an `ID3D12Resource`) and the hook idles; WGC works but stutters. Add
a D3D12 path via a **D3D11On12 bridge**: capture the game's D3D12 command queue
(hook `ID3D12CommandQueue::ExecuteCommandLists`), create an `ID3D11On12Device`,
`CreateWrappedResource` around the backbuffer, and `CopyResource` into the
*existing* D3D11 shared keyed-mutex texture — so the host side is unchanged.
- **Multi-stream audio capture + mixing, with per-stream format detection.** Games
with several concurrent WASAPI render streams (e.g. Miles Morales) only get their
first ("primary") stream mirrored today; the rest keep playing locally and never
@@ -170,6 +164,11 @@ ctest --test-dir build -C Debug --output-on-failure
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.
- **`dx12_present_hook_test`** — in-process self-test of the Present hook's **D3D12**
path: drives a real D3D12 swapchain through the (shared) `IDXGISwapChain::Present`
vtable and asserts the D3D11On12 bridge wraps the `ID3D12Resource` backbuffer and
copies it into the shared texture, then reads the exact rendered color back by
name. Skips cleanly without a D3D12 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,