Files
CoopAllTheThings/tools/mock_game/render_dx12.cpp
BlackMark 9a736cf477 Give the mock-game backends one source of truth for the test pattern
The animated pattern math (background sweep, moving 24px bar, frame-
counter block) was copy-pasted across all five clear-based backends
(dx9/dx10/dx11/dx12/gl). The backends MUST render an identical image --
the capture test decodes the same frame-counter block from each -- so a
drift between copies would be a silent test hazard.

Factor it into one frame_pattern() helper (plus kBarWidth) in
render_backend.hpp; each backend now consumes the computed bg/bar_x/code
values with its own API's clear/fill. mock_game_test still decodes every
backend correctly.
2026-07-12 09:35:45 +02:00

205 lines
6.3 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);
const FramePattern p = frame_pattern(frame, width_);
const float bg[4] = {p.bg_r / 255.0f, p.bg_g / 255.0f, p.bg_b / 255.0f, 1.0f};
list_->ClearRenderTargetView(rtv_handles_[idx], bg, 0, nullptr);
const float white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
const LONG bx = static_cast<LONG>(p.bar_x);
const D3D12_RECT bar = {bx, 0, bx + static_cast<LONG>(kBarWidth), static_cast<LONG>(height_)};
list_->ClearRenderTargetView(rtv_handles_[idx], white, 1, &bar);
const float code[4] = {p.code_r / 255.0f, p.code_g / 255.0f, p.code_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