Add coop_mock_game: animated, frame-numbered A/V test game (DX11 + DX12)
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>
This commit is contained in:
@@ -120,6 +120,7 @@ if(COOP_BUILD_HOOK)
|
||||
add_subdirectory(hook)
|
||||
enable_testing()
|
||||
add_subdirectory(tools/audio_tone) # coop_tone: audio source for the loopback test
|
||||
add_subdirectory(tools/mock_game) # coop_mock_game: A/V test game for the capture/hook tests
|
||||
add_subdirectory(tools/audio_probe) # coop_audio_probe: inject + diagnose the render-hook
|
||||
add_subdirectory(tools/input_probe) # coop_input_probe: inject + forward synthetic input
|
||||
add_subdirectory(tests)
|
||||
|
||||
16
tools/mock_game/CMakeLists.txt
Normal file
16
tools/mock_game/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# CoopMockGame -- a tiny test "game" with an animated, frame-numbered render (DX11/DX12)
|
||||
# and a configurable WASAPI tone, used by the capture/audio/hook stress tests. Not shipped.
|
||||
add_executable(coop_mock_game
|
||||
main.cpp
|
||||
render_backend.cpp
|
||||
render_dx11.cpp
|
||||
render_dx12.cpp)
|
||||
|
||||
# Reuses the shared ToneSource (also used by coop_tone + the audio hook self-test).
|
||||
target_include_directories(coop_mock_game PRIVATE ${CMAKE_SOURCE_DIR}/tools/audio_tone)
|
||||
|
||||
target_link_libraries(coop_mock_game PRIVATE d3d11 d3d12 dxgi ole32)
|
||||
set_target_properties(coop_mock_game PROPERTIES OUTPUT_NAME "coop_mock_game")
|
||||
|
||||
# Test fixture -> stage next to the tests (alongside coop_tone), not in the deployable root.
|
||||
coop_output_subdir(tests coop_mock_game)
|
||||
147
tools/mock_game/main.cpp
Normal file
147
tools/mock_game/main.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
// CoopMockGame -- a tiny test "game" used to exercise the capture + audio + hook paths.
|
||||
//
|
||||
// coop_mock_game.exe [dx11|dx12] [seconds] [rate] [channels] [bits] [pcm|float]
|
||||
//
|
||||
// It opens a normal visible window and renders an animated, frame-numbered pattern (see
|
||||
// render_backend.hpp): a moving bar + per-frame background colour make motion obvious, and
|
||||
// a top-left block encodes the exact frame number so a capture test can decode it and
|
||||
// detect dropped / duplicated / stale frames. With audio args it also plays a configurable
|
||||
// WASAPI tone (the same ToneSource coop_tone uses), so it doubles as an A/V source. seconds
|
||||
// = 0 runs until the window is closed. Console app so it can print its pid + status.
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "render_backend.hpp"
|
||||
#include "tone_source.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic<bool> g_running{true};
|
||||
|
||||
LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
if (msg == WM_DESTROY)
|
||||
{
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
// Spawns the audio render loop on its own thread (WASAPI wants its own COM apartment).
|
||||
void audio_thread(coop::tone::ToneFormat want)
|
||||
{
|
||||
if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
coop::tone::ToneSource tone;
|
||||
if (tone.open(want, 440.0))
|
||||
{
|
||||
std::printf("MOCK_GAME audio: %u Hz %u ch %u-bit %s\n", tone.format().rate, tone.format().channels,
|
||||
tone.format().bits, tone.format().is_float ? "float" : "pcm");
|
||||
std::fflush(stdout);
|
||||
while (g_running.load(std::memory_order_relaxed))
|
||||
{
|
||||
tone.render_step(200);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("MOCK_GAME audio: failed to open requested format\n");
|
||||
}
|
||||
tone.close();
|
||||
CoUninitialize();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const std::string backend_name = argc > 1 ? argv[1] : "dx11";
|
||||
const double seconds = argc > 2 ? std::strtod(argv[2], nullptr) : 0.0;
|
||||
const bool want_audio = argc > 3;
|
||||
coop::tone::ToneFormat audio_fmt;
|
||||
if (want_audio)
|
||||
{
|
||||
audio_fmt.rate = static_cast<unsigned>(std::strtoul(argv[3], nullptr, 10));
|
||||
audio_fmt.channels = argc > 4 ? static_cast<unsigned>(std::strtoul(argv[4], nullptr, 10)) : 2;
|
||||
audio_fmt.bits = argc > 5 ? static_cast<unsigned>(std::strtoul(argv[5], nullptr, 10)) : 32;
|
||||
audio_fmt.is_float = argc > 6 ? (std::string(argv[6]) == "float") : (audio_fmt.bits == 32);
|
||||
}
|
||||
|
||||
constexpr std::uint32_t kW = 640, kH = 480;
|
||||
const HINSTANCE inst = GetModuleHandleW(nullptr);
|
||||
WNDCLASSEXW wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = wnd_proc;
|
||||
wc.hInstance = inst;
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = L"CoopMockGameWindow";
|
||||
RegisterClassExW(&wc);
|
||||
|
||||
RECT r = {0, 0, static_cast<LONG>(kW), static_cast<LONG>(kH)};
|
||||
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"CoopMockGame", WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, r.right - r.left, r.bottom - r.top, nullptr,
|
||||
nullptr, inst, nullptr);
|
||||
if (hwnd == nullptr)
|
||||
{
|
||||
std::printf("MOCK_GAME error: CreateWindow failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto backend = coop::mock::RenderBackend::create(backend_name);
|
||||
if (!backend || !backend->init(hwnd, kW, kH))
|
||||
{
|
||||
std::printf("MOCK_GAME error: backend '%s' unavailable\n", backend_name.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::printf("MOCK_GAME pid=%lu backend=%s w=%u h=%u audio=%s\n", GetCurrentProcessId(), backend->name(),
|
||||
kW, kH, want_audio ? "yes" : "no");
|
||||
std::fflush(stdout);
|
||||
|
||||
std::thread audio;
|
||||
if (want_audio)
|
||||
{
|
||||
audio = std::thread(audio_thread, audio_fmt);
|
||||
}
|
||||
|
||||
const ULONGLONG start = GetTickCount64();
|
||||
std::uint32_t frame = 0;
|
||||
for (;;)
|
||||
{
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (msg.message == WM_QUIT)
|
||||
{
|
||||
g_running.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
if (!g_running.load(std::memory_order_relaxed))
|
||||
{
|
||||
break;
|
||||
}
|
||||
backend->render_and_present(frame++);
|
||||
if (seconds > 0.0 && (GetTickCount64() - start) >= static_cast<ULONGLONG>(seconds * 1000.0))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_running.store(false, std::memory_order_relaxed);
|
||||
if (audio.joinable())
|
||||
{
|
||||
audio.join();
|
||||
}
|
||||
std::printf("MOCK_GAME done: %u frames\n", frame);
|
||||
return 0;
|
||||
}
|
||||
19
tools/mock_game/render_backend.cpp
Normal file
19
tools/mock_game/render_backend.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "render_backend.hpp"
|
||||
|
||||
namespace coop::mock
|
||||
{
|
||||
|
||||
std::unique_ptr<RenderBackend> RenderBackend::create(const std::string& name)
|
||||
{
|
||||
if (name == "dx11")
|
||||
{
|
||||
return create_dx11_backend();
|
||||
}
|
||||
if (name == "dx12")
|
||||
{
|
||||
return create_dx12_backend();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace coop::mock
|
||||
64
tools/mock_game/render_backend.hpp
Normal file
64
tools/mock_game/render_backend.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Pluggable render backend for the mock game (a test fixture, not a shipped component).
|
||||
//
|
||||
// The mock game renders an *animated* (never static) pattern so dropped / duplicated /
|
||||
// torn / stale frames are obvious both to the eye and to an automated capture test: a
|
||||
// moving bar + a per-frame background colour give visible motion, and a small top-left
|
||||
// "frame-counter" block encodes the exact frame number in its RGB so a test can decode it
|
||||
// from a captured pixel and assert the sequence advances (the DX12 rotating-backbuffer bug
|
||||
// showed up as a stuck/repeating counter). The backend is selectable (DX11 / DX12 today)
|
||||
// and structured so OpenGL / Vulkan can be added by implementing this interface.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace coop::mock
|
||||
{
|
||||
|
||||
// Encode a frame counter into an RGB triple (and back). R/G/B are the low 24 bits, so it
|
||||
// is unambiguous for ~16M frames. The swap chain is UNORM (not sRGB), so the bytes survive
|
||||
// a capture copy exactly. The capture test samples the frame-counter block and decodes it.
|
||||
inline void frame_to_rgb(std::uint32_t frame, std::uint8_t& r, std::uint8_t& g, std::uint8_t& b)
|
||||
{
|
||||
r = static_cast<std::uint8_t>(frame & 0xFF);
|
||||
g = static_cast<std::uint8_t>((frame >> 8) & 0xFF);
|
||||
b = static_cast<std::uint8_t>((frame >> 16) & 0xFF);
|
||||
}
|
||||
|
||||
inline std::uint32_t rgb_to_frame(std::uint8_t r, std::uint8_t g, std::uint8_t b)
|
||||
{
|
||||
return static_cast<std::uint32_t>(r) | (static_cast<std::uint32_t>(g) << 8) |
|
||||
(static_cast<std::uint32_t>(b) << 16);
|
||||
}
|
||||
|
||||
// Size (px) of the top-left frame-counter block the test samples.
|
||||
inline constexpr std::uint32_t kFrameBlock = 64;
|
||||
|
||||
class RenderBackend
|
||||
{
|
||||
public:
|
||||
virtual ~RenderBackend() = default;
|
||||
|
||||
// Bring up the device + swap chain on `hwnd` at the given client size. False on failure.
|
||||
virtual bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) = 0;
|
||||
|
||||
// Draw frame `frame` (animated background + moving bar + the frame-counter block) and
|
||||
// present it. One call per displayed frame.
|
||||
virtual void render_and_present(std::uint32_t frame) = 0;
|
||||
|
||||
// Human-readable backend name (e.g. "dx11"), for logging.
|
||||
[[nodiscard]] virtual const char* name() const = 0;
|
||||
|
||||
// Factory: returns a backend for `name` ("dx11" / "dx12"), or nullptr if unknown /
|
||||
// unavailable on this machine.
|
||||
static std::unique_ptr<RenderBackend> create(const std::string& name);
|
||||
};
|
||||
|
||||
// Per-backend factories (defined in their own TUs; create() dispatches to them).
|
||||
std::unique_ptr<RenderBackend> create_dx11_backend();
|
||||
std::unique_ptr<RenderBackend> create_dx12_backend();
|
||||
|
||||
} // namespace coop::mock
|
||||
116
tools/mock_game/render_dx11.cpp
Normal file
116
tools/mock_game/render_dx11.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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_2.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;
|
||||
}
|
||||
|
||||
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;
|
||||
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(1, 0); // vsync -> a game-like cadence
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* name() const override
|
||||
{
|
||||
return "dx11";
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint32_t width_ = 0;
|
||||
std::uint32_t height_ = 0;
|
||||
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
|
||||
198
tools/mock_game/render_dx12.cpp
Normal file
198
tools/mock_game/render_dx12.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user