From 38616d81ef821efc9a7710653450d21e770389e6 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:27:04 +0000 Subject: [PATCH] perf(window): hoist invariant per-frame Vulkan info structs to members (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Window::Render() rebuilt cmdBufInfo, the subresource range, both VkImageMemoryBarrier structs, and presentInfo on the stack every frame even though only the barriers' image (and the acquire barrier's oldLayout) ever change. Hoist them to members initialised once in the constructor; Render() now patches only the two varying fields. presentInfo.pImageIndices/pSwapchains point at the currentBuffer and swapChain members, so they track value changes (including swapchain recreation) automatically. The render-complete wait semaphore is fixed for the window's lifetime, so the per-frame `renderComplete != NULL` check — set once, never cleared — was dead and is removed; the semaphore is wired into presentInfo in the constructor instead. Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Window.cpp | 105 +++++++++++--------- interfaces/Crafter.Graphics-Window.cppm | 10 ++ 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 1388a03..9eccdca 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -430,6 +430,58 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height submitInfo.pSignalSemaphores = &semaphores.renderComplete; submitInfo.pNext = VK_NULL_HANDLE; + // Per-frame info structs: everything that never varies between frames is + // set here once. Render() only patches the barriers' image/oldLayout. + cmdBufInfo = {}; + cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + const VkImageSubresourceRange range { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; + + // Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image + // and oldLayout are patched per frame (oldLayout depends on first use). + acquireBarrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = VK_NULL_HANDLE, + .subresourceRange = range, + }; + + // Transition back to PRESENT_SRC_KHR after the passes. Only image varies. + presentBarrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = 0, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = VK_NULL_HANDLE, + .subresourceRange = range, + }; + + // pImageIndices/pSwapchains point at members, so they follow value changes + // (currentBuffer each frame, swapChain across recreation) on their own. + // The render-complete wait semaphore is fixed for the window's lifetime. + presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.pNext = NULL; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = &swapChain; + presentInfo.pImageIndices = ¤tBuffer; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &semaphores.renderComplete; + lastMousePos = {0,0}; mouseDelta = {0,0}; currentMousePos = {0,0}; @@ -688,18 +740,8 @@ void Window::Render() { submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; - VkCommandBufferBeginInfo cmdBufInfo {}; - cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo)); - VkImageSubresourceRange range{}; - range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - range.baseMipLevel = 0; - range.levelCount = VK_REMAINING_MIP_LEVELS; - range.baseArrayLayer = 0; - range.layerCount = VK_REMAINING_ARRAY_LAYERS; - // On an image's first use after (re)creating the swapchain it is still in // VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in // PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation @@ -708,20 +750,11 @@ void Window::Render() { const bool firstUse = !imageInitialised[currentBuffer]; imageInitialised[currentBuffer] = true; - VkImageMemoryBarrier image_memory_barrier { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, - .oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED - : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = images[currentBuffer], - .subresourceRange = range - }; + acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + acquireBarrier.image = images[currentBuffer]; - vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier); + vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier); // Synthesise key-repeat events before listeners run, so the focused // widget's OnTextInput / OnKeyDown sees them in the same frame. @@ -786,35 +819,13 @@ void Window::Render() { } } - VkImageMemoryBarrier image_memory_barrier2 { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, - .dstAccessMask = 0, - .oldLayout = VK_IMAGE_LAYOUT_GENERAL, - .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = images[currentBuffer], - .subresourceRange = range - }; + presentBarrier.image = images[currentBuffer]; - vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier2); + vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier); Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer])); Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); - VkPresentInfoKHR presentInfo = {}; - presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - presentInfo.pNext = NULL; - presentInfo.swapchainCount = 1; - presentInfo.pSwapchains = &swapChain; - presentInfo.pImageIndices = ¤tBuffer; - // Check if a wait semaphore has been specified to wait for before presenting the image - if (semaphores.renderComplete != VK_NULL_HANDLE) - { - presentInfo.pWaitSemaphores = &semaphores.renderComplete; - presentInfo.waitSemaphoreCount = 1; - } VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo); if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) { diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index b957882..4bd121a 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -259,6 +259,16 @@ export namespace Crafter { std::thread thread; VkCommandBuffer drawCmdBuffers[numFrames]; VkSubmitInfo submitInfo; + // Per-frame Vulkan info structs whose contents are almost entirely + // invariant. Initialised once in the constructor; Render() patches only + // the few fields that change each frame (the two barriers' image and + // the acquire barrier's oldLayout). presentInfo.pImageIndices and + // pSwapchains point at the currentBuffer/swapChain members, so they + // track value changes (including swapchain recreation) automatically. + VkCommandBufferBeginInfo cmdBufInfo; + VkImageMemoryBarrier acquireBarrier; + VkImageMemoryBarrier presentBarrier; + VkPresentInfoKHR presentInfo; Semaphores semaphores; std::uint32_t currentBuffer = 0; VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; -- 2.54.0