M2: DX10 capture via D3D10 read-back; mock_game_test covers DX10
A pure-D3D10 game's backbuffer QIs to ID3D11Texture2D but that view reads back empty (content lives on the game's D3D10 device), and a D3D11 backbuffer also QIs to ID3D10Texture2D -- so GetBuffer can't discriminate. The reliable signal is that a feature-level-10 device rejects CreateTexture2D with the NT-handle keyed-mutex share flags (E_INVALIDARG). The present hook now tries the D3D11 fast path and, on that failure, switches (sticky) to reading the backbuffer through the game's own D3D10 device into a staging texture and uploading it into the shared texture on a hook-owned D3D11 device (Map blocks until the GPU copy completes -> no cross-device race). The host reader is unchanged. mock_game_test now decodes DX10 frames through the hook (monotonic/advancing) alongside DX11/DX12; 15/15 ctest. Roadmap: DX10 milestone done and removed (remaining renumbered); architecture + lessons updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
41
README.md
41
README.md
@@ -34,9 +34,12 @@ and forwards guest controllers back into it.
|
||||
|
||||
The hooked video path has two producers: **Direct3D (DXGI)** hooks
|
||||
`IDXGISwapChain::Present` / `Present1` and copies the backbuffer — directly for
|
||||
D3D10/11 games (the backbuffer is an `ID3D11Texture2D`), and via a **D3D11On12
|
||||
D3D11 games (the backbuffer is an `ID3D11Texture2D`), via a **D3D11On12
|
||||
bridge** for D3D12 games (wrap the `ID3D12Resource` backbuffer, `CopyResource` into
|
||||
the shared texture); **OpenGL** hooks `SwapBuffers` / `wglSwapBuffers` and reads the
|
||||
the shared texture), and via a **D3D10 read-back** for D3D10 games (their backbuffer's
|
||||
D3D11 view is empty and a feature-level-10 device can't host the shared texture, so
|
||||
read it through the game's own D3D10 device and upload it via a hook-owned D3D11
|
||||
device); **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**
|
||||
@@ -131,22 +134,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* (M5's red banner and Vulkan-layer checkbox
|
||||
every milestone below must preserve this test* (M4's red banner and Vulkan-layer checkbox
|
||||
in particular).
|
||||
|
||||
- **M2 — DX10 (mock → capture).**
|
||||
1. **Mock backend** (`render_dx10.cpp`). `ID3D10Device` + DXGI swap chain; Windows SDK only
|
||||
(`d3d10`, `dxgi`), **no new deps**. Sub-region fills (bar, counter block) via
|
||||
`CopySubresourceRegion` of small solid-colour textures (DX10 has no clear-rect). Top-left
|
||||
origin, so the counter block maps straight to the capture's sample point.
|
||||
2. **Capture.** The DXGI `Present`/`Present1` hook already catches D3D10 swap chains, but the
|
||||
copy QIs the backbuffer to `ID3D11Texture2D`, which a *pure* D3D10 device fails. Share the
|
||||
D3D10 backbuffer into the hook's **own D3D11 device** (legacy shared handle) and copy it
|
||||
into the standard keyed-mutex texture; add a `dx10_present_hook_test` that decodes the
|
||||
mock's frames. Validates the "D3D10/11" claim the Architecture section currently makes
|
||||
untested.
|
||||
|
||||
- **M3 — DX9 (mock → D3D9Ex capture → plain-D3D9 capture).**
|
||||
- **M2 — DX9 (mock → D3D9Ex capture → plain-D3D9 capture).**
|
||||
1. **Mock backend** (`render_dx09.cpp`). `IDirect3DDevice9` / `IDirect3DDevice9Ex` + present;
|
||||
Windows SDK only (`d3d9`), **no new deps**. `Clear` for the background and `ColorFill` for
|
||||
the bar + block (D3D9's built-in rect fill — exactly the primitive DX10/11 lack). Top-left
|
||||
@@ -170,7 +161,7 @@ Conventions for every milestone below:
|
||||
texture* differs (CPU copy, not GPU). `GetRenderTargetData` is a GPU→sysmem stall, so
|
||||
**drop / throttle** mirror frames rather than back-pressure the game. Same test, plain mode.
|
||||
|
||||
- **M4 — OpenGL (mock → capture coverage).**
|
||||
- **M3 — 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
|
||||
@@ -181,7 +172,7 @@ Conventions for every milestone below:
|
||||
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.
|
||||
|
||||
- **M5 — Vulkan (mock → capture).** The largest.
|
||||
- **M4 — 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
|
||||
@@ -326,10 +317,10 @@ 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 **DX11 / DX12** backends and a
|
||||
[`tools/mock_game`](tools/mock_game) with selectable **DX10 / DX11 / DX12** 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 both backends (the bar
|
||||
to assert the mirror sees a *monotonic, advancing* sequence for each backend (the bar
|
||||
for no dropped / stale / out-of-order frames — what the DX12 rotating-backbuffer bug
|
||||
broke). It launches the game at several **audio formats** (44100/48000/96000, PCM +
|
||||
float) and asserts the hook measures each one's rate through the full inject path, then
|
||||
@@ -467,6 +458,16 @@ Non-obvious things that cost time and constrain the design:
|
||||
the producer-side `AcquireSync` non-blocking (`timeout 0`) so a busy mutex drops a
|
||||
*mirror* frame instead of stalling the game; the Video panel's "Frames lost" line
|
||||
surfaces both capture- and display-stage drops.
|
||||
- **A D3D10 game's backbuffer lies about being D3D11.** A pure-D3D10 swapchain's backbuffer
|
||||
QIs to `ID3D11Texture2D` *successfully*, but that D3D11 view reads back **empty** — the
|
||||
rendered content only exists on the game's own D3D10 device. (And a D3D11 backbuffer QIs to
|
||||
`ID3D10Texture2D` too, so `GetBuffer` alone can't tell them apart.) The reliable signal is
|
||||
that a feature-level-10 device **rejects** `CreateTexture2D` with the NT-handle keyed-mutex
|
||||
share flags (`E_INVALIDARG`): so try the D3D11 fast path, and on that failure switch (sticky)
|
||||
to reading the backbuffer through the game's **D3D10** device into a staging texture and
|
||||
`UpdateSubresource`-ing it into the shared texture on a **hook-owned D3D11 device** (the game
|
||||
has no usable D3D11 device of its own). The staging `Map` blocks until the GPU copy completes,
|
||||
so there's no cross-device race.
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user