// Shared helper for the per-backend present-thread overhead guards. // // The capture path (Present / SwapBuffers hook, or the Vulkan read-back) runs on the game's present // thread. If it stalls there, it caps the game's frame rate -- the Vulkan layer did exactly this, // dropping a 144 FPS game to ~3 FPS by spending ~370 ms per present reading write-combined memory. // Each GPU backend's in-process hook test measures the wall time its present spends with the hook // live vs. removed and asserts the added cost stays under one display frame, so a future regression // that puts a synchronous read-back / stall back on the present thread fails the test. #pragma once #include namespace cooptest { inline double now_ms() { LARGE_INTEGER f, c; QueryPerformanceFrequency(&f); QueryPerformanceCounter(&c); return 1000.0 * static_cast(c.QuadPart) / static_cast(f.QuadPart); } // Average wall time of `present()` over n frames, calling `render()` (untimed) before each so a // fresh frame is produced. Returns milliseconds per present. template double avg_present_ms(int n, RenderFn render, PresentFn present) { render(); present(); // warm (first present/resource setup) double total = 0; for (int i = 0; i < n; ++i) { render(); const double a = now_ms(); present(); total += now_ms() - a; } return n > 0 ? total / n : 0.0; } // One 60 Hz display frame. The capture's added present-thread cost must stay well under this or it // throttles the game; the Vulkan bug added ~370 ms (22x over budget). Generous on purpose -- the // guard targets the catastrophic-stall class, not micro-overhead, so it never flakes on jitter. inline constexpr double kPresentOverheadBudgetMs = 16.7; } // namespace cooptest