From ce996763d57d85359fdba160c89a54ea43186f07 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 12 Jul 2026 12:01:11 +0200 Subject: [PATCH] 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. --- tools/mock_game/render_vk.cpp | 171 +++++++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 46 deletions(-) diff --git a/tools/mock_game/render_vk.cpp b/tools/mock_game/render_vk.cpp index c3f0d76..94b803a 100644 --- a/tools/mock_game/render_vk.cpp +++ b/tools/mock_game/render_vk.cpp @@ -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 @@ -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(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 = ⊂ + 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 images_; + std::vector views_; + std::vector framebuffers_; + VkRenderPass render_pass_ = VK_NULL_HANDLE; VkCommandPool pool_ = VK_NULL_HANDLE; VkCommandBuffer cmd_ = VK_NULL_HANDLE; VkSemaphore acquire_sem_ = VK_NULL_HANDLE;