Files
CoopAllTheThings/tools/mock_game/render_dx12.cpp
BlackMark 8a43d2f568 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>
2026-06-23 09:45:00 +02:00

211 lines
6.6 KiB
C++

// DX12 backend for the mock game. Renders the same animated pattern as the DX11 backend
// (animated background + moving bar + top-left frame-counter block) so the capture tests
// can verify the DX12 capture path against an identical, frame-numbered source.
#include "render_backend.hpp"
#include <d3d12.h>
#include <dxgi1_6.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
namespace coop::mock
{
namespace
{
constexpr UINT kBackBuffers = 3; // DX12 rotates these explicitly -- the case the capture must get right
class Dx12Backend : public RenderBackend
{
public:
bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override
{
width_ = width;
height_ = height;
if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device_.GetAddressOf()))))
{
return false;
}
D3D12_COMMAND_QUEUE_DESC qd = {};
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
if (FAILED(device_->CreateCommandQueue(&qd, IID_PPV_ARGS(queue_.GetAddressOf()))))
{
return false;
}
ComPtr<IDXGIFactory4> factory;
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf()))))
{
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;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
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())) ||
FAILED(sc1.As(&swap_)))
{
return false;
}
factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
D3D12_DESCRIPTOR_HEAP_DESC hd = {};
hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
hd.NumDescriptors = kBackBuffers;
if (FAILED(device_->CreateDescriptorHeap(&hd, IID_PPV_ARGS(rtv_heap_.GetAddressOf()))))
{
return false;
}
rtv_stride_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
D3D12_CPU_DESCRIPTOR_HANDLE h = rtv_heap_->GetCPUDescriptorHandleForHeapStart();
for (UINT i = 0; i < kBackBuffers; ++i)
{
if (FAILED(swap_->GetBuffer(i, IID_PPV_ARGS(targets_[i].GetAddressOf()))))
{
return false;
}
device_->CreateRenderTargetView(targets_[i].Get(), nullptr, h);
rtv_handles_[i] = h;
h.ptr += rtv_stride_;
if (FAILED(device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(allocs_[i].GetAddressOf()))))
{
return false;
}
}
if (FAILED(device_->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocs_[0].Get(), nullptr,
IID_PPV_ARGS(list_.GetAddressOf()))))
{
return false;
}
list_->Close();
if (FAILED(device_->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence_.GetAddressOf()))))
{
return false;
}
fence_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
return fence_event_ != nullptr;
}
void render_and_present(std::uint32_t frame) override
{
const UINT idx = swap_->GetCurrentBackBufferIndex();
allocs_[idx]->Reset();
list_->Reset(allocs_[idx].Get(), nullptr);
transition(targets_[idx].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
// Animated background (full RT clear).
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};
list_->ClearRenderTargetView(rtv_handles_[idx], bg, 0, nullptr);
// Moving vertical bar.
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 D3D12_RECT bar = {bx, 0, bx + 24, static_cast<LONG>(height_)};
list_->ClearRenderTargetView(rtv_handles_[idx], white, 1, &bar);
// Frame-counter block (top-left).
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 D3D12_RECT block = {0, 0, static_cast<LONG>(kFrameBlock), static_cast<LONG>(kFrameBlock)};
list_->ClearRenderTargetView(rtv_handles_[idx], code, 1, &block);
transition(targets_[idx].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
list_->Close();
ID3D12CommandList* lists[] = {list_.Get()};
queue_->ExecuteCommandLists(1, lists);
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)
}
[[nodiscard]] const char* name() const override
{
return "dx12";
}
~Dx12Backend() override
{
if (fence_ != nullptr)
{
wait_for_gpu();
}
if (fence_event_ != nullptr)
{
CloseHandle(fence_event_);
}
}
private:
void transition(ID3D12Resource* res, D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to)
{
D3D12_RESOURCE_BARRIER b = {};
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
b.Transition.pResource = res;
b.Transition.StateBefore = from;
b.Transition.StateAfter = to;
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
list_->ResourceBarrier(1, &b);
}
void wait_for_gpu()
{
const UINT64 v = ++fence_value_;
queue_->Signal(fence_.Get(), v);
if (fence_->GetCompletedValue() < v)
{
fence_->SetEventOnCompletion(v, fence_event_);
WaitForSingleObject(fence_event_, INFINITE);
}
}
std::uint32_t width_ = 0;
std::uint32_t height_ = 0;
bool tearing_ = false;
ComPtr<ID3D12Device> device_;
ComPtr<ID3D12CommandQueue> queue_;
ComPtr<IDXGISwapChain3> swap_;
ComPtr<ID3D12DescriptorHeap> rtv_heap_;
UINT rtv_stride_ = 0;
ComPtr<ID3D12Resource> targets_[kBackBuffers];
D3D12_CPU_DESCRIPTOR_HANDLE rtv_handles_[kBackBuffers] = {};
ComPtr<ID3D12CommandAllocator> allocs_[kBackBuffers];
ComPtr<ID3D12GraphicsCommandList> list_;
ComPtr<ID3D12Fence> fence_;
UINT64 fence_value_ = 0;
HANDLE fence_event_ = nullptr;
};
} // namespace
std::unique_ptr<RenderBackend> create_dx12_backend()
{
return std::make_unique<Dx12Backend>();
}
} // namespace coop::mock