The Vulkan backend was the odd one out: it cleared the whole swapchain image to the frame-counter colour (vkCmdClearColorImage), while every other backend draws an animated background + a moving vertical bar + a top-left frame-counter block. Bring it in line. Use a render pass (loadOp CLEAR paints the animated background, final layout PRESENT_SRC so no manual barriers) plus vkCmdClearAttachments to clear the bar and block rects -- so it renders the full FramePattern while still needing no pipeline / shaders / SPIR-V, matching the clear-based character of the dx11/dx12/gl backends. Swapchain images gain COLOR_ATTACHMENT usage (keep TRANSFER_SRC for the capture hook). No new dependency: render pass / framebuffer / image view / ClearAttachments are core Vulkan, already available through the vendored volk submodule. mock_game_test decodes the vk frame-counter block correctly through the capture layer, same as before.
385 lines
14 KiB
C++
385 lines
14 KiB
C++
// Vulkan backend for the mock game. Brings up a real Vulkan instance/device/swap chain via volk
|
|
// (which dlopens vulkan-1.dll -- the loader-bypass case the capture hook must handle) and renders
|
|
// the same pattern as the other backends -- an animated background, a moving vertical bar, and the
|
|
// top-left frame-counter block -- so a dropped/duplicated/stale frame is detectable by the capture
|
|
// test just like the D3D/GL paths. The per-rect draw uses a render pass whose load clears the
|
|
// background and vkCmdClearAttachments for the bar + block, so it still needs no pipeline / shaders.
|
|
#include "render_backend.hpp"
|
|
|
|
#include <vector>
|
|
|
|
#include <volk.h>
|
|
|
|
namespace coop::mock {
|
|
namespace {
|
|
class VkBackend : public RenderBackend {
|
|
public:
|
|
bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override
|
|
{
|
|
width_ = width;
|
|
height_ = height;
|
|
if (volkInitialize() != VK_SUCCESS) {
|
|
return false; // no Vulkan loader on this machine
|
|
}
|
|
|
|
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
|
app.pApplicationName = "coop_mock_game";
|
|
app.apiVersion = VK_API_VERSION_1_1;
|
|
const char* inst_ext[] = {VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME};
|
|
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
|
ici.pApplicationInfo = &app;
|
|
ici.enabledExtensionCount = 2;
|
|
ici.ppEnabledExtensionNames = inst_ext;
|
|
if (vkCreateInstance(&ici, nullptr, &instance_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
volkLoadInstance(instance_);
|
|
|
|
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
|
|
sci.hinstance = GetModuleHandleW(nullptr);
|
|
sci.hwnd = hwnd;
|
|
if (vkCreateWin32SurfaceKHR(instance_, &sci, nullptr, &surface_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
return pick_device() && create_device() && create_swapchain() && create_render_pass() && create_framebuffers()
|
|
&& create_commands();
|
|
}
|
|
|
|
void render_and_present(std::uint32_t frame) override
|
|
{
|
|
if (device_ == VK_NULL_HANDLE || swapchain_ == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
vkWaitForFences(device_, 1, &in_flight_, VK_TRUE, UINT64_MAX);
|
|
|
|
std::uint32_t idx = 0;
|
|
VkResult acq = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, acquire_sem_, VK_NULL_HANDLE, &idx);
|
|
if (acq != VK_SUCCESS) {
|
|
return; // OUT_OF_DATE / SUBOPTIMAL / error: skip (the mock window isn't resized in practice)
|
|
}
|
|
vkResetFences(device_, 1, &in_flight_);
|
|
|
|
const FramePattern pat = frame_pattern(frame, extent_.width);
|
|
|
|
VkCommandBuffer cb = cmd_;
|
|
vkResetCommandBuffer(cb, 0);
|
|
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
vkBeginCommandBuffer(cb, &bi);
|
|
|
|
// The render pass's load op clears the whole attachment to the animated background; the
|
|
// pass's final layout is PRESENT_SRC_KHR, so no manual image barriers are needed.
|
|
VkClearValue clear{};
|
|
clear.color = {{pat.bg_r / 255.0f, pat.bg_g / 255.0f, pat.bg_b / 255.0f, 1.0f}};
|
|
VkRenderPassBeginInfo rpbi{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
rpbi.renderPass = render_pass_;
|
|
rpbi.framebuffer = framebuffers_[idx];
|
|
rpbi.renderArea = {{0, 0}, extent_};
|
|
rpbi.clearValueCount = 1;
|
|
rpbi.pClearValues = &clear;
|
|
vkCmdBeginRenderPass(cb, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
|
|
|
|
// Moving vertical bar (full height) then the top-left frame-counter block, cleared as
|
|
// rectangular regions of the bound attachment (no pipeline needed).
|
|
clear_rect(cb, {{static_cast<std::int32_t>(pat.bar_x), 0}, {kBarWidth, extent_.height}},
|
|
{{1.0f, 1.0f, 1.0f, 1.0f}});
|
|
clear_rect(cb, {{0, 0}, {kFrameBlock, kFrameBlock}},
|
|
{{pat.code_r / 255.0f, pat.code_g / 255.0f, pat.code_b / 255.0f, 1.0f}});
|
|
|
|
vkCmdEndRenderPass(cb);
|
|
vkEndCommandBuffer(cb);
|
|
|
|
VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
si.waitSemaphoreCount = 1;
|
|
si.pWaitSemaphores = &acquire_sem_;
|
|
si.pWaitDstStageMask = &wait_stage;
|
|
si.commandBufferCount = 1;
|
|
si.pCommandBuffers = &cb;
|
|
si.signalSemaphoreCount = 1;
|
|
si.pSignalSemaphores = &submit_sem_;
|
|
vkQueueSubmit(queue_, 1, &si, in_flight_);
|
|
|
|
VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
|
pi.waitSemaphoreCount = 1;
|
|
pi.pWaitSemaphores = &submit_sem_;
|
|
pi.swapchainCount = 1;
|
|
pi.pSwapchains = &swapchain_;
|
|
pi.pImageIndices = &idx;
|
|
vkQueuePresentKHR(queue_, &pi);
|
|
}
|
|
|
|
[[nodiscard]] const char* name() const override { return "vk"; }
|
|
|
|
~VkBackend() override
|
|
{
|
|
if (device_ != VK_NULL_HANDLE) {
|
|
vkDeviceWaitIdle(device_);
|
|
for (VkFramebuffer fb : framebuffers_)
|
|
if (fb != VK_NULL_HANDLE)
|
|
vkDestroyFramebuffer(device_, fb, nullptr);
|
|
for (VkImageView v : views_)
|
|
if (v != VK_NULL_HANDLE)
|
|
vkDestroyImageView(device_, v, nullptr);
|
|
if (render_pass_ != VK_NULL_HANDLE)
|
|
vkDestroyRenderPass(device_, render_pass_, nullptr);
|
|
if (in_flight_ != VK_NULL_HANDLE)
|
|
vkDestroyFence(device_, in_flight_, nullptr);
|
|
if (acquire_sem_ != VK_NULL_HANDLE)
|
|
vkDestroySemaphore(device_, acquire_sem_, nullptr);
|
|
if (submit_sem_ != VK_NULL_HANDLE)
|
|
vkDestroySemaphore(device_, submit_sem_, nullptr);
|
|
if (pool_ != VK_NULL_HANDLE)
|
|
vkDestroyCommandPool(device_, pool_, nullptr);
|
|
if (swapchain_ != VK_NULL_HANDLE)
|
|
vkDestroySwapchainKHR(device_, swapchain_, nullptr);
|
|
vkDestroyDevice(device_, nullptr);
|
|
}
|
|
if (surface_ != VK_NULL_HANDLE)
|
|
vkDestroySurfaceKHR(instance_, surface_, nullptr);
|
|
if (instance_ != VK_NULL_HANDLE)
|
|
vkDestroyInstance(instance_, nullptr);
|
|
}
|
|
|
|
private:
|
|
// Clear one rectangular region of the bound color attachment to `color`.
|
|
void clear_rect(VkCommandBuffer cb, VkRect2D rect, VkClearColorValue color)
|
|
{
|
|
VkClearAttachment att{};
|
|
att.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
att.colorAttachment = 0;
|
|
att.clearValue.color = color;
|
|
VkClearRect cr{};
|
|
cr.rect = rect;
|
|
cr.baseArrayLayer = 0;
|
|
cr.layerCount = 1;
|
|
vkCmdClearAttachments(cb, 1, &att, 1, &cr);
|
|
}
|
|
|
|
bool pick_device()
|
|
{
|
|
std::uint32_t n = 0;
|
|
vkEnumeratePhysicalDevices(instance_, &n, nullptr);
|
|
std::vector<VkPhysicalDevice> devs(n);
|
|
vkEnumeratePhysicalDevices(instance_, &n, devs.data());
|
|
for (VkPhysicalDevice pd : devs) {
|
|
std::uint32_t qn = 0;
|
|
vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, nullptr);
|
|
std::vector<VkQueueFamilyProperties> qf(qn);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, qf.data());
|
|
for (std::uint32_t i = 0; i < qn; ++i) {
|
|
VkBool32 present = VK_FALSE;
|
|
vkGetPhysicalDeviceSurfaceSupportKHR(pd, i, surface_, &present);
|
|
if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) {
|
|
phys_ = pd;
|
|
qfam_ = i;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool create_device()
|
|
{
|
|
float prio = 1.0f;
|
|
VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
|
qci.queueFamilyIndex = qfam_;
|
|
qci.queueCount = 1;
|
|
qci.pQueuePriorities = &prio;
|
|
const char* dev_ext[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
|
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
|
dci.queueCreateInfoCount = 1;
|
|
dci.pQueueCreateInfos = &qci;
|
|
dci.enabledExtensionCount = 1;
|
|
dci.ppEnabledExtensionNames = dev_ext;
|
|
if (vkCreateDevice(phys_, &dci, nullptr, &device_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
volkLoadDevice(device_);
|
|
vkGetDeviceQueue(device_, qfam_, 0, &queue_);
|
|
return true;
|
|
}
|
|
|
|
bool create_swapchain()
|
|
{
|
|
VkSurfaceCapabilitiesKHR caps{};
|
|
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys_, surface_, &caps);
|
|
|
|
std::uint32_t fn = 0;
|
|
vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, nullptr);
|
|
std::vector<VkSurfaceFormatKHR> formats(fn);
|
|
vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, formats.data());
|
|
VkSurfaceFormatKHR chosen =
|
|
formats.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}
|
|
: formats[0];
|
|
for (const VkSurfaceFormatKHR& f : formats) {
|
|
if (f.format == VK_FORMAT_B8G8R8A8_UNORM || f.format == VK_FORMAT_R8G8B8A8_UNORM) {
|
|
chosen = f;
|
|
break;
|
|
}
|
|
}
|
|
format_ = chosen.format;
|
|
extent_ = caps.currentExtent.width != 0xFFFFFFFFu ? caps.currentExtent : VkExtent2D{width_, height_};
|
|
|
|
std::uint32_t want = caps.minImageCount + 1;
|
|
if (caps.maxImageCount > 0 && want > caps.maxImageCount) {
|
|
want = caps.maxImageCount;
|
|
}
|
|
VkSwapchainCreateInfoKHR sc{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
|
|
sc.surface = surface_;
|
|
sc.minImageCount = want;
|
|
sc.imageFormat = chosen.format;
|
|
sc.imageColorSpace = chosen.colorSpace;
|
|
sc.imageExtent = extent_;
|
|
sc.imageArrayLayers = 1;
|
|
// COLOR_ATTACHMENT so the render pass can draw into it; TRANSFER_SRC so the capture hook can
|
|
// copy it out.
|
|
sc.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
sc.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
sc.preTransform = caps.currentTransform;
|
|
sc.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
|
// The mock is a perf fixture and must run UNCAPPED: prefer IMMEDIATE (no vsync) > MAILBOX >
|
|
// FIFO. FIFO (vsync) would cap it at the refresh, hiding capture-induced slowdowns.
|
|
std::uint32_t pmn = 0;
|
|
vkGetPhysicalDeviceSurfacePresentModesKHR(phys_, surface_, &pmn, nullptr);
|
|
std::vector<VkPresentModeKHR> pmodes(pmn);
|
|
vkGetPhysicalDeviceSurfacePresentModesKHR(phys_, surface_, &pmn, pmodes.data());
|
|
auto has_mode = [&](VkPresentModeKHR m) {
|
|
for (VkPresentModeKHR p : pmodes)
|
|
if (p == m)
|
|
return true;
|
|
return false;
|
|
};
|
|
sc.presentMode = has_mode(VK_PRESENT_MODE_IMMEDIATE_KHR) ? VK_PRESENT_MODE_IMMEDIATE_KHR
|
|
: has_mode(VK_PRESENT_MODE_MAILBOX_KHR) ? VK_PRESENT_MODE_MAILBOX_KHR
|
|
: VK_PRESENT_MODE_FIFO_KHR;
|
|
sc.clipped = VK_TRUE;
|
|
if (vkCreateSwapchainKHR(device_, &sc, nullptr, &swapchain_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
std::uint32_t in = 0;
|
|
vkGetSwapchainImagesKHR(device_, swapchain_, &in, nullptr);
|
|
images_.resize(in);
|
|
vkGetSwapchainImagesKHR(device_, swapchain_, &in, images_.data());
|
|
return true;
|
|
}
|
|
|
|
bool create_render_pass()
|
|
{
|
|
VkAttachmentDescription color{};
|
|
color.format = format_;
|
|
color.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // clears to the animated background each frame
|
|
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // ready to present, no manual barrier
|
|
|
|
VkAttachmentReference ref{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
VkSubpassDescription sub{};
|
|
sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
sub.colorAttachmentCount = 1;
|
|
sub.pColorAttachments = &ref;
|
|
|
|
// Order the acquire against the attachment's first write.
|
|
VkSubpassDependency dep{};
|
|
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
dep.dstSubpass = 0;
|
|
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dep.srcAccessMask = 0;
|
|
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
|
|
VkRenderPassCreateInfo rpci{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
|
rpci.attachmentCount = 1;
|
|
rpci.pAttachments = &color;
|
|
rpci.subpassCount = 1;
|
|
rpci.pSubpasses = ⊂
|
|
rpci.dependencyCount = 1;
|
|
rpci.pDependencies = &dep;
|
|
return vkCreateRenderPass(device_, &rpci, nullptr, &render_pass_) == VK_SUCCESS;
|
|
}
|
|
|
|
bool create_framebuffers()
|
|
{
|
|
views_.resize(images_.size(), VK_NULL_HANDLE);
|
|
framebuffers_.resize(images_.size(), VK_NULL_HANDLE);
|
|
for (std::size_t i = 0; i < images_.size(); ++i) {
|
|
VkImageViewCreateInfo ivci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
ivci.image = images_[i];
|
|
ivci.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
ivci.format = format_;
|
|
ivci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
if (vkCreateImageView(device_, &ivci, nullptr, &views_[i]) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
VkFramebufferCreateInfo fbci{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
fbci.renderPass = render_pass_;
|
|
fbci.attachmentCount = 1;
|
|
fbci.pAttachments = &views_[i];
|
|
fbci.width = extent_.width;
|
|
fbci.height = extent_.height;
|
|
fbci.layers = 1;
|
|
if (vkCreateFramebuffer(device_, &fbci, nullptr, &framebuffers_[i]) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool create_commands()
|
|
{
|
|
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
|
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
|
pci.queueFamilyIndex = qfam_;
|
|
if (vkCreateCommandPool(device_, &pci, nullptr, &pool_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
|
ai.commandPool = pool_;
|
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
ai.commandBufferCount = 1;
|
|
if (vkAllocateCommandBuffers(device_, &ai, &cmd_) != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
|
VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
|
fi.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
|
return vkCreateSemaphore(device_, &si, nullptr, &acquire_sem_) == VK_SUCCESS
|
|
&& vkCreateSemaphore(device_, &si, nullptr, &submit_sem_) == VK_SUCCESS
|
|
&& vkCreateFence(device_, &fi, nullptr, &in_flight_) == VK_SUCCESS;
|
|
}
|
|
|
|
std::uint32_t width_ = 0;
|
|
std::uint32_t height_ = 0;
|
|
VkInstance instance_ = VK_NULL_HANDLE;
|
|
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
|
|
VkPhysicalDevice phys_ = VK_NULL_HANDLE;
|
|
std::uint32_t qfam_ = 0;
|
|
VkDevice device_ = VK_NULL_HANDLE;
|
|
VkQueue queue_ = VK_NULL_HANDLE;
|
|
VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
|
|
VkFormat format_ = VK_FORMAT_UNDEFINED;
|
|
VkExtent2D extent_ = {0, 0};
|
|
std::vector<VkImage> images_;
|
|
std::vector<VkImageView> views_;
|
|
std::vector<VkFramebuffer> framebuffers_;
|
|
VkRenderPass render_pass_ = VK_NULL_HANDLE;
|
|
VkCommandPool pool_ = VK_NULL_HANDLE;
|
|
VkCommandBuffer cmd_ = VK_NULL_HANDLE;
|
|
VkSemaphore acquire_sem_ = VK_NULL_HANDLE;
|
|
VkSemaphore submit_sem_ = VK_NULL_HANDLE;
|
|
VkFence in_flight_ = VK_NULL_HANDLE;
|
|
};
|
|
} // namespace
|
|
|
|
std::unique_ptr<RenderBackend> create_vk_backend()
|
|
{
|
|
return std::make_unique<VkBackend>();
|
|
}
|
|
|
|
} // namespace coop::mock
|