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>
129 lines
4.4 KiB
C++
129 lines
4.4 KiB
C++
// DX11 backend for the mock game. Renders the animated pattern entirely with region
|
|
// clears (ID3D11DeviceContext1::ClearView) so it needs no shaders: an animated full-screen
|
|
// background, a moving vertical bar, and the top-left frame-counter block.
|
|
#include "render_backend.hpp"
|
|
|
|
#include <d3d11_1.h>
|
|
#include <dxgi1_6.h>
|
|
#include <wrl/client.h>
|
|
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
namespace coop::mock
|
|
{
|
|
namespace
|
|
{
|
|
class Dx11Backend : public RenderBackend
|
|
{
|
|
public:
|
|
bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override
|
|
{
|
|
width_ = width;
|
|
height_ = height;
|
|
|
|
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
|
|
ComPtr<ID3D11DeviceContext> ctx0;
|
|
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels,
|
|
static_cast<UINT>(std::size(levels)), D3D11_SDK_VERSION,
|
|
device_.GetAddressOf(), nullptr, ctx0.GetAddressOf())))
|
|
{
|
|
return false;
|
|
}
|
|
if (FAILED(ctx0.As(&ctx_))) // ClearView needs ID3D11DeviceContext1
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ComPtr<IDXGIDevice> dxgi_device;
|
|
ComPtr<IDXGIAdapter> adapter;
|
|
ComPtr<IDXGIFactory2> factory;
|
|
if (FAILED(device_.As(&dxgi_device)) || FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf())) ||
|
|
FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf()))))
|
|
{
|
|
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;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // UNORM (not sRGB) so the frame code is exact
|
|
desc.SampleDesc.Count = 1;
|
|
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())))
|
|
{
|
|
return false;
|
|
}
|
|
factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
|
|
|
|
// D3D11 flip-model: GetBuffer(0) stays the live back buffer, so one RTV is reused.
|
|
ComPtr<ID3D11Texture2D> back;
|
|
if (FAILED(swap_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf()))) ||
|
|
FAILED(device_->CreateRenderTargetView(back.Get(), nullptr, rtv_.GetAddressOf())))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void render_and_present(std::uint32_t frame) override
|
|
{
|
|
// Animated background: each channel sweeps at a different rate -> constant motion.
|
|
const float bg[4] = {static_cast<float>((frame * 2) % 256) / 255.0f,
|
|
static_cast<float>((frame * 3) % 256) / 255.0f,
|
|
static_cast<float>((frame * 5) % 256) / 255.0f, 1.0f};
|
|
ctx_->ClearRenderTargetView(rtv_.Get(), bg);
|
|
|
|
// Moving vertical bar (obvious motion; a dropped frame makes it jump).
|
|
const float white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
const LONG span = width_ > 24 ? static_cast<LONG>(width_ - 24) : 1;
|
|
const LONG bx = static_cast<LONG>((frame * 4) % static_cast<std::uint32_t>(span));
|
|
const D3D11_RECT bar = {bx, 0, bx + 24, static_cast<LONG>(height_)};
|
|
ctx_->ClearView(rtv_.Get(), white, &bar, 1);
|
|
|
|
// Frame-counter block (top-left): RGB encodes the exact frame number for the test.
|
|
std::uint8_t r = 0, g = 0, b = 0;
|
|
frame_to_rgb(frame, r, g, b);
|
|
const float code[4] = {r / 255.0f, g / 255.0f, b / 255.0f, 1.0f};
|
|
const D3D11_RECT block = {0, 0, static_cast<LONG>(kFrameBlock), static_cast<LONG>(kFrameBlock)};
|
|
ctx_->ClearView(rtv_.Get(), code, &block, 1);
|
|
|
|
swap_->Present(0, tearing_ ? DXGI_PRESENT_ALLOW_TEARING : 0u); // uncapped: the mock must be fast
|
|
}
|
|
|
|
[[nodiscard]] const char* name() const override
|
|
{
|
|
return "dx11";
|
|
}
|
|
|
|
private:
|
|
std::uint32_t width_ = 0;
|
|
std::uint32_t height_ = 0;
|
|
bool tearing_ = false;
|
|
ComPtr<ID3D11Device> device_;
|
|
ComPtr<ID3D11DeviceContext1> ctx_;
|
|
ComPtr<IDXGISwapChain1> swap_;
|
|
ComPtr<ID3D11RenderTargetView> rtv_;
|
|
};
|
|
} // namespace
|
|
|
|
std::unique_ptr<RenderBackend> create_dx11_backend()
|
|
{
|
|
return std::make_unique<Dx11Backend>();
|
|
}
|
|
|
|
} // namespace coop::mock
|