A tiny test "game" for exercising the capture/audio/hook paths. Opens a normal window and renders an animated pattern -- moving bar + per-frame background so motion (and dropped frames) are obvious -- with a top-left block whose RGB encodes the exact frame number, so a capture test can decode it and detect dropped / duplicated / stale frames. Selectable backend (dx11 / dx12 today), behind a RenderBackend interface so OpenGL/Vulkan can be added. With audio args it also plays a configurable WASAPI tone (shared ToneSource), so it's a full A/V source with a window (unlike coop_tone). coop_mock_game.exe [dx11|dx12] [seconds] [rate] [channels] [bits] [pcm|float] Milestone 1 of the mock-game roadmap item (the stress-test suite follows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
199 lines
6.1 KiB
C++
199 lines
6.1 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_4.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;
|
|
}
|
|
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;
|
|
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(1, 0);
|
|
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;
|
|
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
|