// In-process self-test for the Present-hook video path (hook/src/present_hook.cpp). // This process plays both "game" and "host": it installs the Present hook, then // creates its own D3D11 swapchain, clears the backbuffer to a known color, and // calls Present -- exactly as a game would. With the hook live, that Present must // (1) fire the detour, (2) copy the backbuffer into the shared keyed-mutex // texture, and (3) publish the descriptor to the IPC video channel. A second // device then opens the shared texture by name and reads it back, proving the // CreateSharedHandle / OpenSharedResourceByName / keyed-mutex / CopyResource // contract both the hook and the host rely on -- with no game and no Steam. // // Requires a D3D11 device; on a headless / no-GPU machine it reports SKIP and // exits 0 (mirrors audio_hook_test). #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; } } constexpr UINT kW = 256; constexpr UINT kH = 256; // Distinctive clear color; UNORM bytes are ~ {51, 102, 153, 255}. constexpr float kClear[4] = {0.20f, 0.40f, 0.60f, 1.0f}; bool near_byte(std::uint8_t got, int expected) { return std::abs(static_cast(got) - expected) <= 2; } } // namespace int main() { // --- Host side: SharedBlock (named by our pid) so the hook's IpcClient connects. SharedMemory shm; if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock))) { std::printf("FAIL: create shared memory\n"); return 1; } auto* block = shm.as(); // OS zero-fills the mapping block->version = kProtocolVersion; block->sequence.store(0, std::memory_order_relaxed); block->magic = kProtocolMagic; hook::IpcClient ipc; check(ipc.connect(10, 5), "IPC client connect"); // --- Install the Present hook (inline-hooks IDXGISwapChain::Present). --- if (!hook::install_present_hooks(ipc)) { std::printf("SKIP: could not install the Present hook (no D3D11 device?)\n"); return 0; } // --- Game side: our own swapchain on a hidden window; render + Present. --- WNDCLASSEXW wc{}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = DefWindowProcW; wc.hInstance = GetModuleHandleW(nullptr); wc.lpszClassName = L"coop_present_test"; RegisterClassExW(&wc); HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, kW, kH, nullptr, nullptr, wc.hInstance, nullptr); DXGI_SWAP_CHAIN_DESC scd{}; scd.BufferCount = 2; scd.BufferDesc.Width = kW; scd.BufferDesc.Height = kH; scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; scd.OutputWindow = hwnd; scd.SampleDesc.Count = 1; scd.Windowed = TRUE; scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; IDXGISwapChain* swapchain = nullptr; ID3D11Device* device = nullptr; ID3D11DeviceContext* ctx = nullptr; HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &scd, &swapchain, &device, nullptr, &ctx); if (FAILED(hr) || swapchain == nullptr) { std::printf("SKIP: could not create a D3D11 swapchain (hr=0x%08lX)\n", static_cast(hr)); hook::remove_present_hooks(); return 0; } // Clear the backbuffer to the known color, then Present (fires the detour). for (int frame = 0; frame < 3; ++frame) { ID3D11Texture2D* back = nullptr; if (SUCCEEDED(swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&back)))) { ID3D11RenderTargetView* rtv = nullptr; if (SUCCEEDED(device->CreateRenderTargetView(back, nullptr, &rtv))) { ctx->ClearRenderTargetView(rtv, kClear); ctx->Flush(); rtv->Release(); } back->Release(); } swapchain->Present(0, 0); } 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); check(hook::present_calls() >= 3, "Present detour fired"); check(hook::present_frames_shared() > 0, "backbuffer copied into the shared texture"); check(block->video.generation.load() > 0, "video generation published to IPC"); check(block->video.present_qpc > 0, "present QPC stamped for the latency metric"); check(block->video.width == kW && block->video.height == kH, "shared dimensions published"); check(block->video.format == DXGI_FORMAT_R8G8B8A8_UNORM, "shared format published"); // --- Consumer side: open the shared texture by name (second device), copy it // into a staging texture, map it, and verify the clear color survived. --- { 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 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); } release(swapchain); release(ctx); release(device); DestroyWindow(hwnd); UnregisterClassW(wc.lpszClassName, wc.hInstance); hook::remove_present_hooks(); std::printf(g_failures == 0 ? "PRESENT HOOK TEST PASS\n" : "PRESENT HOOK TEST FAILED (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }