perf(window): hoist invariant per-frame Vulkan info structs to members #78

Merged
catbot merged 1 commit from claude/issue-43 into master 2026-06-16 17:27:49 +02:00
2 changed files with 68 additions and 47 deletions
Showing only changes of commit 38616d81ef - Show all commits

perf(window): hoist invariant per-frame Vulkan info structs to members (#43)

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 <noreply@anthropic.com>
catbot 2026-06-16 15:27:04 +00:00

View file

@ -430,6 +430,58 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
submitInfo.pSignalSemaphores = &semaphores.renderComplete; submitInfo.pSignalSemaphores = &semaphores.renderComplete;
submitInfo.pNext = VK_NULL_HANDLE; 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 = &currentBuffer;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &semaphores.renderComplete;
lastMousePos = {0,0}; lastMousePos = {0,0};
mouseDelta = {0,0}; mouseDelta = {0,0};
currentMousePos = {0,0}; currentMousePos = {0,0};
@ -688,18 +740,8 @@ void Window::Render() {
submitInfo.commandBufferCount = 1; submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
VkCommandBufferBeginInfo cmdBufInfo {};
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo)); 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 // 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 // VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation // PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
@ -708,20 +750,11 @@ void Window::Render() {
const bool firstUse = !imageInitialised[currentBuffer]; const bool firstUse = !imageInitialised[currentBuffer];
imageInitialised[currentBuffer] = true; imageInitialised[currentBuffer] = true;
VkImageMemoryBarrier image_memory_barrier { acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
.srcAccessMask = 0, acquireBarrier.image = images[currentBuffer];
.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
};
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 // Synthesise key-repeat events before listeners run, so the focused
// widget's OnTextInput / OnKeyDown sees them in the same frame. // widget's OnTextInput / OnKeyDown sees them in the same frame.
@ -786,35 +819,13 @@ void Window::Render() {
} }
} }
VkImageMemoryBarrier image_memory_barrier2 { presentBarrier.image = images[currentBuffer];
.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
};
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(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); 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 = &currentBuffer;
// 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); VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) { if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {

View file

@ -259,6 +259,16 @@ export namespace Crafter {
std::thread thread; std::thread thread;
VkCommandBuffer drawCmdBuffers[numFrames]; VkCommandBuffer drawCmdBuffers[numFrames];
VkSubmitInfo submitInfo; 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; Semaphores semaphores;
std::uint32_t currentBuffer = 0; std::uint32_t currentBuffer = 0;
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;