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/` | | 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 The hooked video path has two producers: **Direct3D (DXGI)** hooks
`IDXGISwapChain::Present` / `Present1` and copies the backbuffer (D3D10/11 games `IDXGISwapChain::Present` / `Present1` and copies the backbuffer — directly for
whose backbuffer is an `ID3D11Texture2D`); **OpenGL** hooks D3D10/11 games (the backbuffer is an `ID3D11Texture2D`), and via a **D3D11On12
`SwapBuffers` / `wglSwapBuffers` and reads the backbuffer with `glReadPixels` (for bridge** for D3D12 games (wrap the `ID3D12Resource` backbuffer, `CopyResource` into
games that never touch DXGI, e.g. Phantom Brave). The host samples the copy as the shared texture); **OpenGL** hooks `SwapBuffers` / `wglSwapBuffers` and reads the
plain UNORM (`srgb_to_unorm`) so `*_SRGB`-backbuffer games mirror at correct backbuffer with `glReadPixels` (for games that never touch DXGI, e.g. Phantom
brightness. **WGC remains the default** and covers anything the hooked path Brave). The host samples the copy as plain UNORM (`srgb_to_unorm`) so
doesn't (Vulkan, D3D9, DX12 — see Roadmap). `*_SRGB`-backbuffer games mirror at correct brightness. **WGC remains the default**
and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap).
## Limitations ## 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 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. 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 - **Multi-stream audio capture + mixing, with per-stream format detection.** Games
with several concurrent WASAPI render streams (e.g. Miles Morales) only get their 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 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 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 `glReadPixels`'d into the shared texture, and a second device reads the exact
pixels back by name. Skips cleanly without an OpenGL / D3D11 device. 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: - **`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 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, backbuffer to a known color and calls `Present`), and asserts the detour fired,

View File

@@ -6,6 +6,8 @@
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <d3d11on12.h>
#include <d3d12.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include <safetyhook.hpp> #include <safetyhook.hpp>
@@ -50,6 +52,16 @@ UINT g_share_h = 0;
DXGI_FORMAT g_share_fmt = DXGI_FORMAT_UNKNOWN; DXGI_FORMAT g_share_fmt = DXGI_FORMAT_UNKNOWN;
bool g_unsupported_logged = false; bool g_unsupported_logged = false;
// D3D11On12 bridge for D3D12 games: we build our own D3D11 device on top of the
// game's D3D12 device (with a queue we create on it) so we can wrap the D3D12
// backbuffer as a D3D11 resource and CopyResource it into the shared texture.
// Created lazily on the first D3D12 Present; guarded by g_tex_mutex.
ID3D11On12Device* g_on12 = nullptr;
ID3D11Device* g_on12_d3d11 = nullptr;
ID3D11DeviceContext* g_on12_ctx = nullptr;
ID3D12CommandQueue* g_on12_queue = nullptr;
ID3D12Device* g_on12_d3d12 = nullptr;
void* vtable_method(void* obj, unsigned index) void* vtable_method(void* obj, unsigned index)
{ {
return (*reinterpret_cast<void***>(obj))[index]; return (*reinterpret_cast<void***>(obj))[index];
@@ -147,6 +159,174 @@ bool ensure_shared_texture_locked(ID3D11Device* device, UINT w, UINT h, DXGI_FOR
return true; return true;
} }
// Drop the D3D11On12 bridge. Caller holds g_tex_mutex.
void release_on12_locked()
{
if (g_on12_ctx != nullptr)
{
g_on12_ctx->Release();
g_on12_ctx = nullptr;
}
if (g_on12 != nullptr)
{
g_on12->Release();
g_on12 = nullptr;
}
if (g_on12_d3d11 != nullptr)
{
g_on12_d3d11->Release();
g_on12_d3d11 = nullptr;
}
if (g_on12_queue != nullptr)
{
g_on12_queue->Release();
g_on12_queue = nullptr;
}
if (g_on12_d3d12 != nullptr)
{
g_on12_d3d12->Release();
g_on12_d3d12 = nullptr;
}
}
// Build the D3D11On12 device for the game's D3D12 `dev` (creating our own DIRECT
// queue on it). Caller holds g_tex_mutex. Returns true when the bridge is ready.
bool ensure_on12_locked(ID3D12Device* dev)
{
if (g_on12 != nullptr && g_on12_d3d12 == dev)
{
return true;
}
release_on12_locked();
D3D12_COMMAND_QUEUE_DESC qd{};
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ID3D12CommandQueue* queue = nullptr;
HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue));
if (FAILED(hr) || queue == nullptr)
{
logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast<unsigned long>(hr));
return false;
}
IUnknown* queues[] = {queue};
ID3D11Device* d11 = nullptr;
ID3D11DeviceContext* ctx = nullptr;
hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr);
if (FAILED(hr) || d11 == nullptr)
{
logf("present(d3d12): D3D11On12CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
queue->Release();
return false;
}
ID3D11On12Device* on12 = nullptr;
hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast<void**>(&on12));
if (FAILED(hr) || on12 == nullptr)
{
logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast<unsigned long>(hr));
if (ctx != nullptr)
{
ctx->Release();
}
d11->Release();
queue->Release();
return false;
}
g_on12 = on12;
g_on12_d3d11 = d11;
g_on12_ctx = ctx;
g_on12_queue = queue;
g_on12_d3d12 = dev;
dev->AddRef(); // we hold a reference for the lifetime of the bridge
logf("present(d3d12): D3D11On12 bridge ready");
return true;
}
// D3D12 backbuffer path: wrap the game's D3D12 swapchain buffer as a D3D11 resource
// via the On12 bridge and CopyResource it into the shared texture.
void capture_backbuffer_d3d12(IDXGISwapChain* sc)
{
ID3D12Resource* bb = nullptr;
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr)
{
if (!g_unsupported_logged)
{
logf("present: backbuffer is neither ID3D11Texture2D nor ID3D12Resource (D3D9/Vulkan?); idle");
g_unsupported_logged = true;
}
return;
}
ID3D12Device* dev = nullptr;
bb->GetDevice(__uuidof(ID3D12Device), reinterpret_cast<void**>(&dev));
bool shared = false;
UINT w = 0, h = 0;
DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
if (dev != nullptr)
{
std::scoped_lock lock(g_tex_mutex);
if (ensure_on12_locked(dev))
{
D3D11_RESOURCE_FLAGS rf{};
rf.BindFlags = D3D11_BIND_RENDER_TARGET;
ID3D11Resource* wrapped = nullptr;
HRESULT hr = g_on12->CreateWrappedResource(bb, &rf, D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_PRESENT, __uuidof(ID3D11Resource),
reinterpret_cast<void**>(&wrapped));
if (SUCCEEDED(hr) && wrapped != nullptr)
{
g_on12->AcquireWrappedResources(&wrapped, 1);
ID3D11Texture2D* wtex = nullptr;
if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D),
reinterpret_cast<void**>(&wtex))) &&
wtex != nullptr)
{
D3D11_TEXTURE2D_DESC d{};
wtex->GetDesc(&d);
w = d.Width;
h = d.Height;
fmt = d.Format;
if (d.SampleDesc.Count == 1 && ensure_shared_texture_locked(g_on12_d3d11, w, h, fmt) &&
g_shared_mutex != nullptr)
{
if (g_shared_mutex->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
g_on12_ctx->CopyResource(g_shared_tex, wtex);
g_shared_mutex->ReleaseSync(kVideoMutexKey);
shared = true;
}
}
wtex->Release();
}
g_on12->ReleaseWrappedResources(&wrapped, 1);
g_on12_ctx->Flush();
wrapped->Release();
}
else if (!g_unsupported_logged)
{
logf("present(d3d12): CreateWrappedResource failed hr=0x%08lX", static_cast<unsigned long>(hr));
g_unsupported_logged = true;
}
}
}
if (shared)
{
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
if (g_ipc != nullptr)
{
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(fmt));
}
}
if (dev != nullptr)
{
dev->Release();
}
bb->Release();
}
// Copy the swapchain's backbuffer into the shared texture and publish it. // Copy the swapchain's backbuffer into the shared texture and publish it.
void capture_backbuffer(IDXGISwapChain* sc) void capture_backbuffer(IDXGISwapChain* sc)
{ {
@@ -154,11 +334,7 @@ void capture_backbuffer(IDXGISwapChain* sc)
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf))) || if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf))) ||
backbuf == nullptr) backbuf == nullptr)
{ {
if (!g_unsupported_logged) capture_backbuffer_d3d12(sc); // D3D12 game: bridge via D3D11On12 (or idle if neither)
{
logf("present: backbuffer is not an ID3D11Texture2D (D3D12/D3D9?); video hook idle");
g_unsupported_logged = true;
}
return; return;
} }
@@ -370,6 +546,7 @@ void remove_present_hooks()
{ {
std::scoped_lock lock(g_tex_mutex); std::scoped_lock lock(g_tex_mutex);
release_shared_locked(); release_shared_locked();
release_on12_locked();
} }
g_present_calls.store(0, std::memory_order_relaxed); g_present_calls.store(0, std::memory_order_relaxed);
g_frames_shared.store(0, std::memory_order_relaxed); g_frames_shared.store(0, std::memory_order_relaxed);

View File

@@ -97,6 +97,27 @@ target_link_libraries(present_hook_test PRIVATE
add_test(NAME present_hook_test COMMAND present_hook_test) add_test(NAME present_hook_test COMMAND present_hook_test)
# In-process self-test for the D3D12 path of the Present hook: drives a real D3D12
# swapchain through the (shared) IDXGISwapChain::Present vtable and verifies the
# D3D11On12 bridge copies the D3D12 backbuffer into the shared texture. Skips on a
# machine without a D3D12 device.
add_executable(dx12_present_hook_test
dx12_present_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/present_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(dx12_present_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(dx12_present_hook_test PRIVATE
coop_common
safetyhook::safetyhook
d3d11
d3d12
dxgi)
add_test(NAME dx12_present_hook_test COMMAND dx12_present_hook_test)
# In-process self-test for the OpenGL capture path. Reuses the shipping # In-process self-test for the OpenGL capture path. Reuses the shipping
# opengl_hook.cpp and drives a real OpenGL context in the same process, so it # opengl_hook.cpp and drives a real OpenGL context in the same process, so it
# exercises the SwapBuffers hook, the glReadPixels readback, and the upload into # exercises the SwapBuffers hook, the glReadPixels readback, and the upload into
@@ -131,4 +152,5 @@ coop_output_subdir(tests
audio_hook_test audio_hook_test
srgb_format_test srgb_format_test
present_hook_test present_hook_test
dx12_present_hook_test
opengl_hook_test) opengl_hook_test)

View File

@@ -0,0 +1,296 @@
// In-process self-test for the D3D12 path of the Present hook
// (hook/src/present_hook.cpp). This process plays both "game" and "host": it
// installs the Present hook, then drives a real D3D12 swapchain -- clears a
// backbuffer to a known color and calls Present. IDXGISwapChain::Present is the
// same DXGI vtable function for D3D11 and D3D12 swapchains, so the inline hook
// fires; the D3D12 backbuffer can't be a GetBuffer'd ID3D11Texture2D, so the hook
// must bridge it via D3D11On12 and CopyResource it into the shared texture. A
// second D3D11 device then opens that texture by name and verifies the color.
//
// Requires a D3D12-capable GPU; on a machine without one it reports SKIP, exit 0.
#include <cstdint>
#include <cstdio>
#include <windows.h>
#include <d3d11_1.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "ipc_client.hpp"
#include "present_hook.hpp"
using namespace coop;
namespace
{
int g_failures = 0;
void check(bool ok, const char* what)
{
if (!ok)
{
std::printf(" FAIL: %s\n", what);
++g_failures;
}
}
template <typename T>
void release(T*& p)
{
if (p)
{
p->Release();
p = nullptr;
}
}
bool near_byte(std::uint8_t got, int expected)
{
const int d = static_cast<int>(got) - expected;
return d >= -4 && d <= 4;
}
constexpr UINT kW = 256;
constexpr UINT kH = 256;
constexpr UINT kBuffers = 2;
constexpr float kClear[4] = {0.20f, 0.40f, 0.60f, 1.0f}; // ~ {51, 102, 153}
} // namespace
int main()
{
// --- Host side: shared block named by our pid (the hook opens the same name). ---
SharedMemory shm;
if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock)))
{
std::printf("FAIL: create shared memory\n");
return 1;
}
auto* block = shm.as<SharedBlock>();
block->version = kProtocolVersion;
block->sequence.store(0, std::memory_order_relaxed);
block->magic = kProtocolMagic;
// --- D3D12 device + direct queue. SKIP if the machine has no D3D12. ---
ID3D12Device* device = nullptr;
if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device))) || device == nullptr)
{
std::printf("SKIP: no D3D12 device on this machine\n");
return 0;
}
ID3D12CommandQueue* queue = nullptr;
D3D12_COMMAND_QUEUE_DESC qd{};
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
check(SUCCEEDED(device->CreateCommandQueue(&qd, IID_PPV_ARGS(&queue))), "create command queue");
IDXGIFactory4* factory = nullptr;
check(SUCCEEDED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))), "create DXGI factory");
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"coop_dx12_test";
RegisterClassExW(&wc);
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, kW, kH, nullptr, nullptr,
wc.hInstance, nullptr);
DXGI_SWAP_CHAIN_DESC1 scd{};
scd.Width = kW;
scd.Height = kH;
scd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
scd.SampleDesc.Count = 1;
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
scd.BufferCount = kBuffers;
scd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
IDXGISwapChain1* sc1 = nullptr;
check(SUCCEEDED(factory->CreateSwapChainForHwnd(queue, hwnd, &scd, nullptr, nullptr, &sc1)),
"create D3D12 swapchain");
IDXGISwapChain* swapchain = nullptr;
IDXGISwapChain3* sc3 = nullptr; // for GetCurrentBackBufferIndex
if (sc1 != nullptr)
{
sc1->QueryInterface(IID_PPV_ARGS(&swapchain));
sc1->QueryInterface(IID_PPV_ARGS(&sc3));
}
// RTV heap + render targets for the swapchain buffers.
ID3D12DescriptorHeap* rtv_heap = nullptr;
D3D12_DESCRIPTOR_HEAP_DESC hd{};
hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
hd.NumDescriptors = kBuffers;
device->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&rtv_heap));
const UINT rtv_size = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
ID3D12Resource* render_targets[kBuffers] = {};
if (swapchain != nullptr && rtv_heap != nullptr)
{
D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_heap->GetCPUDescriptorHandleForHeapStart();
for (UINT i = 0; i < kBuffers; ++i)
{
swapchain->GetBuffer(i, IID_PPV_ARGS(&render_targets[i]));
device->CreateRenderTargetView(render_targets[i], nullptr, rtv);
rtv.ptr += rtv_size;
}
}
ID3D12CommandAllocator* allocator = nullptr;
device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator));
ID3D12GraphicsCommandList* cmdlist = nullptr;
device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator, nullptr, IID_PPV_ARGS(&cmdlist));
if (cmdlist != nullptr)
{
cmdlist->Close();
}
ID3D12Fence* fence = nullptr;
device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
HANDLE fence_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
UINT64 fence_value = 0;
// --- Install the Present hook, then render + present a few frames. ---
hook::IpcClient ipc;
check(ipc.connect(10, 5), "IPC client connect");
check(hook::install_present_hooks(ipc), "install Present hooks");
const bool can_render =
swapchain != nullptr && sc3 != nullptr && allocator != nullptr && cmdlist != nullptr && fence != nullptr;
for (int frame = 0; frame < 4 && can_render; ++frame)
{
const UINT idx = sc3->GetCurrentBackBufferIndex();
allocator->Reset();
cmdlist->Reset(allocator, nullptr);
D3D12_RESOURCE_BARRIER b{};
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
b.Transition.pResource = render_targets[idx];
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
b.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
b.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
cmdlist->ResourceBarrier(1, &b);
D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_heap->GetCPUDescriptorHandleForHeapStart();
rtv.ptr += static_cast<SIZE_T>(idx) * rtv_size;
cmdlist->ClearRenderTargetView(rtv, kClear, 0, nullptr);
std::swap(b.Transition.StateBefore, b.Transition.StateAfter); // RENDER_TARGET -> PRESENT
cmdlist->ResourceBarrier(1, &b);
cmdlist->Close();
ID3D12CommandList* lists[] = {cmdlist};
queue->ExecuteCommandLists(1, lists);
swapchain->Present(0, 0); // -> hooked IDXGISwapChain::Present -> D3D11On12 bridge
// Block until the GPU finished this frame (keeps the test simple + correct).
queue->Signal(fence, ++fence_value);
if (fence->GetCompletedValue() < fence_value)
{
fence->SetEventOnCompletion(fence_value, fence_event);
WaitForSingleObject(fence_event, 1000);
}
}
std::printf("present_calls=%llu frames_shared=%llu video{gen=%u %ux%u fmt=%u}\n",
static_cast<unsigned long long>(hook::present_calls()),
static_cast<unsigned long long>(hook::present_frames_shared()), block->video.generation.load(),
block->video.width, block->video.height, block->video.format);
if (can_render)
{
check(hook::present_calls() >= 3, "Present detour fired for the D3D12 swapchain");
check(hook::present_frames_shared() > 0, "D3D12 backbuffer bridged into the shared texture");
check(block->video.generation.load() > 0, "video generation published to IPC");
check(block->video.width == kW && block->video.height == kH, "shared dimensions published");
// --- Consumer side: open the shared texture by name and verify the color. ---
ID3D11Device* devB = nullptr;
ID3D11DeviceContext* ctxB = nullptr;
if (SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
&devB, nullptr, &ctxB)))
{
ID3D11Device1* dev1 = nullptr;
devB->QueryInterface(IID_PPV_ARGS(&dev1));
const std::wstring name = video_share_name(GetCurrentProcessId());
ID3D11Texture2D* sharedB = nullptr;
IDXGIKeyedMutex* km = nullptr;
if (dev1 != nullptr &&
SUCCEEDED(dev1->OpenSharedResourceByName(name.c_str(),
DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
IID_PPV_ARGS(&sharedB))))
{
sharedB->QueryInterface(IID_PPV_ARGS(&km));
D3D11_TEXTURE2D_DESC sd{};
sharedB->GetDesc(&sd);
sd.Usage = D3D11_USAGE_STAGING;
sd.BindFlags = 0;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
sd.MiscFlags = 0;
ID3D11Texture2D* staging = nullptr;
check(SUCCEEDED(devB->CreateTexture2D(&sd, nullptr, &staging)), "create staging texture");
if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK)
{
ctxB->CopyResource(staging, sharedB);
km->ReleaseSync(kVideoMutexKey);
D3D11_MAPPED_SUBRESOURCE mapped{};
if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &mapped)))
{
const auto* px = static_cast<const std::uint8_t*>(mapped.pData);
std::printf("readback pixel0 = {%u,%u,%u,%u}\n", px[0], px[1], px[2], px[3]);
check(near_byte(px[0], 51) && near_byte(px[1], 102) && near_byte(px[2], 153),
"shared texture carries the D3D12-rendered color");
ctxB->Unmap(staging, 0);
}
else
{
check(false, "map staging texture");
}
}
else
{
check(false, "acquire keyed mutex + copy shared texture");
}
release(staging);
}
else
{
check(false, "open shared texture by name");
}
release(km);
release(sharedB);
release(dev1);
}
release(ctxB);
release(devB);
}
hook::remove_present_hooks();
if (fence_event != nullptr)
{
CloseHandle(fence_event);
}
release(fence);
release(cmdlist);
release(allocator);
for (UINT i = 0; i < kBuffers; ++i)
{
release(render_targets[i]);
}
release(rtv_heap);
release(sc3);
release(swapchain);
release(sc1);
release(factory);
release(queue);
release(device);
DestroyWindow(hwnd);
UnregisterClassW(wc.lpszClassName, wc.hInstance);
std::printf(g_failures == 0 ? "DX12 PRESENT HOOK TEST PASS\n" : "DX12 PRESENT HOOK TEST FAILED (%d)\n",
g_failures);
return g_failures == 0 ? 0 : 1;
}