diff --git a/README.md b/README.md index 49d234d..e3aba6b 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/hook/src/present_hook.cpp b/hook/src/present_hook.cpp index 94e4d35..d4bbc75 100644 --- a/hook/src/present_hook.cpp +++ b/hook/src/present_hook.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include @@ -50,6 +52,16 @@ UINT g_share_h = 0; DXGI_FORMAT g_share_fmt = DXGI_FORMAT_UNKNOWN; 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) { return (*reinterpret_cast(obj))[index]; @@ -147,6 +159,174 @@ bool ensure_shared_texture_locked(ID3D11Device* device, UINT w, UINT h, DXGI_FOR 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(&queue)); + if (FAILED(hr) || queue == nullptr) + { + logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast(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(hr)); + queue->Release(); + return false; + } + ID3D11On12Device* on12 = nullptr; + hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast(&on12)); + if (FAILED(hr) || on12 == nullptr) + { + logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast(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(&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(&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(&wrapped)); + if (SUCCEEDED(hr) && wrapped != nullptr) + { + g_on12->AcquireWrappedResources(&wrapped, 1); + ID3D11Texture2D* wtex = nullptr; + if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D), + reinterpret_cast(&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(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(fmt)); + } + } + if (dev != nullptr) + { + dev->Release(); + } + bb->Release(); +} + // Copy the swapchain's backbuffer into the shared texture and publish it. void capture_backbuffer(IDXGISwapChain* sc) { @@ -154,11 +334,7 @@ void capture_backbuffer(IDXGISwapChain* sc) if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&backbuf))) || backbuf == nullptr) { - if (!g_unsupported_logged) - { - logf("present: backbuffer is not an ID3D11Texture2D (D3D12/D3D9?); video hook idle"); - g_unsupported_logged = true; - } + capture_backbuffer_d3d12(sc); // D3D12 game: bridge via D3D11On12 (or idle if neither) return; } @@ -370,6 +546,7 @@ void remove_present_hooks() { std::scoped_lock lock(g_tex_mutex); release_shared_locked(); + release_on12_locked(); } g_present_calls.store(0, std::memory_order_relaxed); g_frames_shared.store(0, std::memory_order_relaxed); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a8f0300..99a4ba4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -97,6 +97,27 @@ target_link_libraries(present_hook_test PRIVATE 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 # 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 @@ -131,4 +152,5 @@ coop_output_subdir(tests audio_hook_test srgb_format_test present_hook_test + dx12_present_hook_test opengl_hook_test) diff --git a/tests/dx12_present_hook_test.cpp b/tests/dx12_present_hook_test.cpp new file mode 100644 index 0000000..d02deee --- /dev/null +++ b/tests/dx12_present_hook_test.cpp @@ -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 +#include + +#include + +#include +#include +#include + +#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 +void release(T*& p) +{ + if (p) + { + p->Release(); + p = nullptr; + } +} + +bool near_byte(std::uint8_t got, int expected) +{ + const int d = static_cast(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(); + 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(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(hook::present_calls()), + static_cast(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(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; +}