// In-process self-test for the OpenGL capture path (hook/src/opengl_hook.cpp). // Plays both "game" and "host": installs the swap hooks, creates a real OpenGL // context, clears the backbuffer to a known color, and calls SwapBuffers. With // the hook live that must (1) fire the SwapBuffers detour, (2) glReadPixels the // backbuffer and upload it into the shared keyed-mutex texture, and (3) publish // the descriptor over IPC. A second D3D device then opens the shared texture by // name and reads it back, proving the OpenGL->shared-texture path end to end -- // no game, no Steam. // // Needs an OpenGL context + a D3D11 device; reports SKIP and exits 0 if either // is unavailable (headless CI), mirroring present_hook_test. #include #include #include #include #include #include #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" #include "ipc_client.hpp" #include "opengl_hook.hpp" #include "present_overhead.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 int kW = 64; constexpr int kH = 64; bool near_byte(std::uint8_t got, int expected) { return std::abs(static_cast(got) - expected) <= 3; } } // namespace int main() { // --- Host side: SharedBlock 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(); 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"); if (!hook::install_opengl_hooks(ipc)) { std::printf("SKIP: could not install the OpenGL swap hooks\n"); return 0; } // --- Game side: a real OpenGL context on a hidden window. --- WNDCLASSEXW wc{}; wc.cbSize = sizeof(wc); wc.lpfnWndProc = DefWindowProcW; wc.hInstance = GetModuleHandleW(nullptr); wc.lpszClassName = L"coop_gl_test"; RegisterClassExW(&wc); // WS_POPUP so the client area is exactly kW x kH (an overlapped window can't // shrink below its minimum caption size, which would skew the captured dims). HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_POPUP, 0, 0, kW, kH, nullptr, nullptr, wc.hInstance, nullptr); ShowWindow(hwnd, SW_SHOWNOACTIVATE); // a mapped window makes the backbuffer reliable HDC hdc = GetDC(hwnd); PIXELFORMATDESCRIPTOR pfd{}; pfd.nSize = sizeof(pfd); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 32; const int pf = ChoosePixelFormat(hdc, &pfd); HGLRC glrc = nullptr; if (pf == 0 || !SetPixelFormat(hdc, pf, &pfd) || (glrc = wglCreateContext(hdc)) == nullptr || !wglMakeCurrent(hdc, glrc)) { std::printf("SKIP: could not create an OpenGL context on this machine\n"); hook::remove_opengl_hooks(); return 0; } // Clear the backbuffer to a known color, then SwapBuffers (fires the detour). for (int frame = 0; frame < 3; ++frame) { glViewport(0, 0, kW, kH); glClearColor(0.20f, 0.40f, 0.60f, 1.0f); // -> ~{51,102,153,255} glClear(GL_COLOR_BUFFER_BIT); glFinish(); SwapBuffers(hdc); } std::printf("swaps=%llu frames_shared=%llu video{gen=%u %ux%u fmt=%u}\n", static_cast(hook::opengl_swaps()), static_cast(hook::opengl_frames_shared()), block->video.generation.load(), block->video.width, block->video.height, block->video.format); check(hook::opengl_swaps() >= 3, "SwapBuffers detour fired"); check(hook::opengl_frames_shared() > 0, "backbuffer uploaded 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: open the shared texture by name, copy to staging, verify color. if (hook::opengl_frames_shared() > 0) { 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; devB->CreateTexture2D(&sd, nullptr, &staging); if (km != nullptr && staging != nullptr && km->AcquireSync(kVideoMutexKey, 1000) == S_OK) { ctxB->CopyResource(staging, sharedB); km->ReleaseSync(kVideoMutexKey); D3D11_MAPPED_SUBRESOURCE m{}; if (SUCCEEDED(ctxB->Map(staging, 0, D3D11_MAP_READ, 0, &m))) { const auto* px = static_cast(m.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); } // --- Performance regression guard: the SwapBuffers hook's glReadPixels read-back must not stall // the present thread (measure SwapBuffers with the hook live vs. removed). --- { auto render = [&] { glViewport(0, 0, kW, kH); glClearColor(0.20f, 0.40f, 0.60f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glFinish(); }; auto present = [&] { SwapBuffers(hdc); }; const double hooked = cooptest::avg_present_ms(120, render, present); hook::remove_opengl_hooks(); // baseline: same context, hook removed const double base = cooptest::avg_present_ms(120, render, present); std::printf("present-thread: hooked %.3f ms, unhooked %.3f ms, capture overhead %.3f ms\n", hooked, base, hooked - base); check(hooked - base < cooptest::kPresentOverheadBudgetMs, "OpenGL capture stays off the present thread (overhead < one 60 Hz frame)"); } wglMakeCurrent(nullptr, nullptr); wglDeleteContext(glrc); ReleaseDC(hwnd, hdc); DestroyWindow(hwnd); UnregisterClassW(wc.lpszClassName, wc.hInstance); std::printf(g_failures == 0 ? "OPENGL HOOK TEST PASS\n" : "OPENGL HOOK TEST FAILED (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }