Render the same pattern in the Vulkan mock backend as the others

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.
This commit is contained in:
2026-07-12 12:01:11 +02:00
parent 30eccf749d
commit ce996763d5

View File

@@ -1,9 +1,9 @@
// 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
// each frame clears the swap-chain image to the frame-counter colour with vkCmdClearColorImage
// (no pipeline, no shaders, no SPIR-V) and presents. The whole image encodes the frame number,
// so it animates and a dropped/stale frame is detectable by the capture test. Clear-only keeps
// this to one image-clear per frame; richer per-rect drawing would need a render pass.
// 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>
@@ -42,10 +42,8 @@ class VkBackend : public RenderBackend {
return false;
}
if (!pick_device() || !create_device() || !create_swapchain() || !create_commands()) {
return false;
}
return true;
return pick_device() && create_device() && create_swapchain() && create_render_pass() && create_framebuffers()
&& create_commands();
}
void render_and_present(std::uint32_t frame) override
@@ -57,38 +55,42 @@ class VkBackend : public RenderBackend {
std::uint32_t idx = 0;
VkResult acq = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, acquire_sem_, VK_NULL_HANDLE, &idx);
if (acq == VK_ERROR_OUT_OF_DATE_KHR || acq == VK_SUBOPTIMAL_KHR) {
return; // skip this frame (the mock window isn't resized in practice)
}
if (acq != VK_SUCCESS) {
return;
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);
barrier(cb, images_[idx], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
// 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);
std::uint8_t r = 0, g = 0, b = 0;
frame_to_rgb(frame, r, g, b); // whole image encodes the frame -> animates + decodable
VkClearColorValue cc{};
cc.float32[0] = r / 255.0f;
cc.float32[1] = g / 255.0f;
cc.float32[2] = b / 255.0f;
cc.float32[3] = 1.0f;
VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cb, images_[idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &cc, 1, &range);
// 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}});
barrier(cb, images_[idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
vkCmdEndRenderPass(cb);
vkEndCommandBuffer(cb);
VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
si.waitSemaphoreCount = 1;
si.pWaitSemaphores = &acquire_sem_;
@@ -114,6 +116,14 @@ class VkBackend : public RenderBackend {
{
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)
@@ -133,6 +143,20 @@ class VkBackend : public RenderBackend {
}
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;
@@ -197,6 +221,7 @@ class VkBackend : public RenderBackend {
}
}
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) {
@@ -207,10 +232,11 @@ class VkBackend : public RenderBackend {
sc.minImageCount = want;
sc.imageFormat = chosen.format;
sc.imageColorSpace = chosen.colorSpace;
sc.imageExtent = caps.currentExtent.width != 0xFFFFFFFFu ? caps.currentExtent : VkExtent2D{width_, height_};
sc.imageExtent = extent_;
sc.imageArrayLayers = 1;
// TRANSFER_DST so we can clear it; TRANSFER_SRC so the capture hook can copy it out.
sc.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
// 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;
@@ -240,6 +266,70 @@ class VkBackend : public RenderBackend {
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 = &sub;
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};
@@ -263,21 +353,6 @@ class VkBackend : public RenderBackend {
&& vkCreateFence(device_, &fi, nullptr, &in_flight_) == VK_SUCCESS;
}
static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkAccessFlags src_access,
VkAccessFlags dst_access, VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage)
{
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
b.srcAccessMask = src_access;
b.dstAccessMask = dst_access;
b.oldLayout = from;
b.newLayout = to;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = img;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, 0, nullptr, 0, nullptr, 1, &b);
}
std::uint32_t width_ = 0;
std::uint32_t height_ = 0;
VkInstance instance_ = VK_NULL_HANDLE;
@@ -288,7 +363,11 @@ class VkBackend : public RenderBackend {
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;