Mock game: render every backend UNCAPPED + assert a healthy present rate

The mock backends presented with vsync ("a game-like cadence") -- wrong for a
perf/stress fixture: it does trivial work on an RTX 4090, so it must run as fast
as it can. Vsync capped them to tens of fps (dx9 30, dx10 23, dx11 63, dx12 126),
which hid both capture-induced slowdowns and the hook-removal race. Uncapped now:

  dx9/dx10 INTERVAL_IMMEDIATE / Present(0,0) (BLT), dx11/dx12 ALLOW_TEARING +
  Present(0, ALLOW_TEARING) (flip), gl wglSwapIntervalEXT(0), vk IMMEDIATE/MAILBOX.

Measured no-hook: dx9 ~21000, dx10 ~2800, dx11 ~17000, dx12 ~12000, gl ~26000, vk
~24000 fps.

mock_game_test now adds a present-rate floor per backend (>= 300/s while
capturing): with the hook live every backend stays in the hundreds-thousands
(vk 13500, dx11 9000+, gl 1800, dx9/10 ~1000-1600, dx12 2500). This is the
dimension the frame-advance checks missed -- the Vulkan 144->3 FPS stall still
advanced frames -- so it catches a present-thread stall OR an accidental vsync.

The faster storm exposed the hook-removal UAF fixed in the previous commit.
README roadmap + lessons-learned updated (incl. correcting the old "reset makes
in-flight trampoline calls safe" claim). Full suite 21/21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:45:00 +02:00
parent 958c355126
commit 8a43d2f568
8 changed files with 111 additions and 37 deletions

View File

@@ -39,7 +39,7 @@ public:
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
pp.hDeviceWindow = hwnd;
pp.Windowed = TRUE;
pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; // vsync -> a game-like cadence
pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // uncapped: the mock must be fast
if (ex_)
{

View File

@@ -103,7 +103,7 @@ public:
{
device_->CopyResource(back.Get(), scratch_.Get());
}
swap_->Present(1, 0); // vsync -> a game-like cadence
swap_->Present(0, 0); // uncapped (BLT model): the mock does nothing -> it must be fast
}
[[nodiscard]] const char* name() const override

View File

@@ -4,7 +4,7 @@
#include "render_backend.hpp"
#include <d3d11_1.h>
#include <dxgi1_2.h>
#include <dxgi1_6.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
@@ -43,6 +43,16 @@ public:
return false;
}
// The mock does trivial work, so it must render UNCAPPED (it's a perf fixture, not a real
// game) -- a flip-model swapchain only tears free of vsync with ALLOW_TEARING, so require it.
ComPtr<IDXGIFactory5> factory5;
BOOL tearing = FALSE;
if (SUCCEEDED(factory.As(&factory5)))
{
factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &tearing, sizeof(tearing));
}
tearing_ = tearing != 0;
DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
@@ -51,6 +61,7 @@ public:
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 2;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.Flags = tearing_ ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0u;
if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd, &desc, nullptr, nullptr,
swap_.GetAddressOf())))
{
@@ -90,7 +101,7 @@ public:
const D3D11_RECT block = {0, 0, static_cast<LONG>(kFrameBlock), static_cast<LONG>(kFrameBlock)};
ctx_->ClearView(rtv_.Get(), code, &block, 1);
swap_->Present(1, 0); // vsync -> a game-like cadence
swap_->Present(0, tearing_ ? DXGI_PRESENT_ALLOW_TEARING : 0u); // uncapped: the mock must be fast
}
[[nodiscard]] const char* name() const override
@@ -101,6 +112,7 @@ public:
private:
std::uint32_t width_ = 0;
std::uint32_t height_ = 0;
bool tearing_ = false;
ComPtr<ID3D11Device> device_;
ComPtr<ID3D11DeviceContext1> ctx_;
ComPtr<IDXGISwapChain1> swap_;

View File

@@ -4,7 +4,7 @@
#include "render_backend.hpp"
#include <d3d12.h>
#include <dxgi1_4.h>
#include <dxgi1_6.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
@@ -40,6 +40,16 @@ public:
{
return false;
}
// Uncapped (the mock is a perf fixture): a flip-model swapchain needs ALLOW_TEARING to run
// free of vsync.
ComPtr<IDXGIFactory5> factory5;
BOOL tearing = FALSE;
if (SUCCEEDED(factory.As(&factory5)))
{
factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &tearing, sizeof(tearing));
}
tearing_ = tearing != 0;
DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width = width;
desc.Height = height;
@@ -48,6 +58,7 @@ public:
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = kBackBuffers;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.Flags = tearing_ ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0u;
ComPtr<IDXGISwapChain1> sc1;
if (FAILED(factory->CreateSwapChainForHwnd(queue_.Get(), hwnd, &desc, nullptr, nullptr,
sc1.GetAddressOf())) ||
@@ -129,7 +140,7 @@ public:
ID3D12CommandList* lists[] = {list_.Get()};
queue_->ExecuteCommandLists(1, lists);
swap_->Present(1, 0);
swap_->Present(0, tearing_ ? DXGI_PRESENT_ALLOW_TEARING : 0u); // uncapped: the mock must be fast
wait_for_gpu(); // simple per-frame sync (mock game: correctness over throughput)
}
@@ -175,6 +186,7 @@ private:
std::uint32_t width_ = 0;
std::uint32_t height_ = 0;
bool tearing_ = false;
ComPtr<ID3D12Device> device_;
ComPtr<ID3D12CommandQueue> queue_;
ComPtr<IDXGISwapChain3> swap_;

View File

@@ -47,12 +47,12 @@ public:
{
return false;
}
// vsync if available (a game-like cadence; avoids spinning uncapped). Runtime extension
// lookup -- no loader/submodule needed.
// Uncapped: the mock is a perf fixture and must run as fast as it can (disable vsync), so a
// capture-induced slowdown is visible. Runtime extension lookup -- no loader/submodule needed.
using PFN_wglSwapIntervalEXT = BOOL(WINAPI*)(int);
if (auto swap_interval = reinterpret_cast<PFN_wglSwapIntervalEXT>(wglGetProcAddress("wglSwapIntervalEXT")))
{
swap_interval(1);
swap_interval(0);
}
return true;
}

View File

@@ -237,7 +237,21 @@ private:
sc.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
sc.preTransform = caps.currentTransform;
sc.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
sc.presentMode = VK_PRESENT_MODE_FIFO_KHR; // vsync, universally supported
// The mock is a perf fixture and must run UNCAPPED: prefer IMMEDIATE (no vsync) > MAILBOX >
// FIFO. FIFO (vsync) would cap it at the refresh, hiding capture-induced slowdowns.
std::uint32_t pmn = 0;
vkGetPhysicalDeviceSurfacePresentModesKHR(phys_, surface_, &pmn, nullptr);
std::vector<VkPresentModeKHR> pmodes(pmn);
vkGetPhysicalDeviceSurfacePresentModesKHR(phys_, surface_, &pmn, pmodes.data());
auto has_mode = [&](VkPresentModeKHR m) {
for (VkPresentModeKHR p : pmodes)
if (p == m)
return true;
return false;
};
sc.presentMode = has_mode(VK_PRESENT_MODE_IMMEDIATE_KHR) ? VK_PRESENT_MODE_IMMEDIATE_KHR
: has_mode(VK_PRESENT_MODE_MAILBOX_KHR) ? VK_PRESENT_MODE_MAILBOX_KHR
: VK_PRESENT_MODE_FIFO_KHR;
sc.clipped = VK_TRUE;
if (vkCreateSwapchainKHR(device_, &sc, nullptr, &swapchain_) != VK_SUCCESS)
{