// 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 #include 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(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 devs(n); vkEnumeratePhysicalDevices(instance_, &n, devs.data()); for (VkPhysicalDevice pd : devs) { std::uint32_t qn = 0; vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, nullptr); std::vector 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 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 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 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; VkSemaphore submit_sem_ = VK_NULL_HANDLE; VkFence in_flight_ = VK_NULL_HANDLE; }; } // namespace std::unique_ptr create_vk_backend() { return std::make_unique(); } } // namespace coop::mock