From 41d99890c1e57d724ace40e09f281267c2613886 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:25:22 +0000 Subject: [PATCH 1/5] perf(window): ring buffer for CRAFTER_TIMING frame times (#44) Replace the std::vector frameTimes plus erase(begin()) with a fixed-size std::array ring buffer (head + count). The previous code memmoved ~99 elements left every frame once the window filled. LogTiming computes order-independent sum/avg/min/max, so the ring's write order is irrelevant to the reported stats. Behind CRAFTER_TIMING only; shipping builds are unaffected. Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Window.cpp | 34 +++++++++++---------- interfaces/Crafter.Graphics-Window.cppm | 9 +++++- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 1388a03..411ffb1 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -657,11 +657,12 @@ void Window::Update() { #ifdef CRAFTER_TIMING frameEnd = std::chrono::high_resolution_clock::now(); - frameTimes.push_back(totalUpdate+totalRender); - - // Keep only the last 100 frame times - if (frameTimes.size() > 100) { - frameTimes.erase(frameTimes.begin()); + // Ring buffer: overwrite the oldest entry once full. Order doesn't matter + // to LogTiming, so we never need to shift elements. + frameTimes[frameTimesHead] = totalUpdate+totalRender; + frameTimesHead = (frameTimesHead + 1) % frameTimeCapacity; + if (frameTimesCount < frameTimeCapacity) { + ++frameTimesCount; } #endif lastFrameBegin = startTime; @@ -843,21 +844,22 @@ void Window::LogTiming() { std::cout << std::format("Total: {}", duration_cast(totalUpdate+totalRender)) << std::endl; std::cout << std::format("Vblank: {}", duration_cast(vblank)) << std::endl; - // Add 100-frame average and min-max timing info - if (!frameTimes.empty()) { + // Add 100-frame average and min-max timing info. Only the first + // frameTimesCount entries of the ring buffer hold valid samples. + if (frameTimesCount != 0) { // Calculate average std::chrono::nanoseconds sum(0); - for (const auto& frameTime : frameTimes) { - sum += frameTime; + for (std::size_t i = 0; i < frameTimesCount; ++i) { + sum += frameTimes[i]; } - auto average = sum / frameTimes.size(); - + auto average = sum / frameTimesCount; + // Find min and max - auto min = frameTimes.front(); - auto max = frameTimes.front(); - for (const auto& frameTime : frameTimes) { - if (frameTime < min) min = frameTime; - if (frameTime > max) max = frameTime; + auto min = frameTimes[0]; + auto max = frameTimes[0]; + for (std::size_t i = 0; i < frameTimesCount; ++i) { + if (frameTimes[i] < min) min = frameTimes[i]; + if (frameTimes[i] > max) max = frameTimes[i]; } std::cout << std::format("Last 100 Frame Times - Avg: {}, Min: {}, Max: {}", diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index b957882..9068267 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -164,7 +164,14 @@ export namespace Crafter { std::chrono::nanoseconds vblank; std::chrono::nanoseconds totalFrame; std::chrono::time_point frameEnd; - std::vector frameTimes; + // Fixed-size ring buffer of the most recent frame times. LogTiming does + // order-independent sum/avg/min/max, so head position is irrelevant to + // the reported stats; this avoids the per-frame memmove a vector::erase + // at the front would incur once full. + static constexpr std::size_t frameTimeCapacity = 100; + std::array frameTimes{}; + std::size_t frameTimesHead = 0; + std::size_t frameTimesCount = 0; void LogTiming(); #endif From 38616d81ef821efc9a7710653450d21e770389e6 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:27:04 +0000 Subject: [PATCH 2/5] 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; From 8b95b7e883a4e919d6ea3bb027e3b7d2f3cbcb63 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:27:10 +0000 Subject: [PATCH 3/5] perf(window): cache per-frame heap-bind structs across frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render() rebuilt two VkBindHeapInfoEXT structs every frame and recomputed reservedRangeOffset via a runtime subtract + bitmask against Device::descriptorHeapProperties. The heap address/size per slot are stable for a given heap; only currentBuffer varies. Precompute numFrames resource/sampler bind structs and cache them, keyed on the descriptorHeap pointer. The cache rebuilds whenever the heap is (re)assigned and otherwise just indexes by currentBuffer. Pointer-keyed invalidation catches any heap reassignment — the same point a setter hook would fire — and is independent of onResize, as the heaps never resize today (per the issue's correctness caveat). Resolves #42 Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Window.cpp | 50 ++++++++++++--------- interfaces/Crafter.Graphics-Window.cppm | 12 +++++ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 1388a03..9ffd6fb 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -734,27 +734,35 @@ void Window::Render() { // Pass-side dispatches still run with the same heaps bound — moving // the bind earlier doesn't change anything for them. if (descriptorHeap) { - VkBindHeapInfoEXT resourceHeapInfo = { - .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, - .heapRange = { - .address = descriptorHeap->resourceHeap[currentBuffer].address, - .size = static_cast(descriptorHeap->resourceHeap[currentBuffer].size) - }, - .reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), - .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange - }; - Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo); - - VkBindHeapInfoEXT samplerHeapInfo = { - .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, - .heapRange = { - .address = descriptorHeap->samplerHeap[currentBuffer].address, - .size = static_cast(descriptorHeap->samplerHeap[currentBuffer].size) - }, - .reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, - .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange - }; - Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo); + // Rebuild the cached per-frame bind structs only when the heap changes. + // The address/size of each frame's heap and the reservedRangeOffset + // bitmask are stable for a given heap, so this runs once per assignment + // rather than every frame. + if (descriptorHeap != cachedDescriptorHeap) { + for (std::uint8_t i = 0; i < numFrames; ++i) { + resourceHeapInfos[i] = { + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange = { + .address = descriptorHeap->resourceHeap[i].address, + .size = static_cast(descriptorHeap->resourceHeap[i].size) + }, + .reservedRangeOffset = (descriptorHeap->resourceHeap[i].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), + .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange + }; + samplerHeapInfos[i] = { + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange = { + .address = descriptorHeap->samplerHeap[i].address, + .size = static_cast(descriptorHeap->samplerHeap[i].size) + }, + .reservedRangeOffset = descriptorHeap->samplerHeap[i].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, + .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange + }; + } + cachedDescriptorHeap = descriptorHeap; + } + Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfos[currentBuffer]); + Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfos[currentBuffer]); } onUpdate.Invoke({startTime, startTime-lastFrameBegin}); diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index b957882..021ba45 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -264,6 +264,18 @@ export namespace Crafter { VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; std::vector passes; DescriptorHeapVulkan* descriptorHeap = nullptr; + // Cached per-frame heap-bind structs. The heap address/size per slot + // are stable once a heap is assigned; only currentBuffer varies frame + // to frame, so the structs (and the reservedRangeOffset arithmetic) + // are computed once per heap rather than every frame in Render(). + // Keyed on the heap pointer: whenever descriptorHeap differs from + // cachedDescriptorHeap the cache is rebuilt. This invalidates on any + // heap (re)assignment — heaps never resize today, so a stable pointer + // means stable ranges. Not tied to onResize (the heap is independent + // of swapchain size). + VkBindHeapInfoEXT resourceHeapInfos[numFrames]; + VkBindHeapInfoEXT samplerHeapInfos[numFrames]; + DescriptorHeapVulkan* cachedDescriptorHeap = nullptr; std::optional> clearColor; #else // DOM mode: the page IS the window. WebGPU device and canvas are From 9ef554b97ed63c99f62f8114d07f5879384089af Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:28:35 +0000 Subject: [PATCH 4/5] perf(window): drop dead present-mode queries from swapchain recreate (#45) CreateSwapchain unconditionally used VK_PRESENT_MODE_FIFO_KHR, but still ran two vkGetPhysicalDeviceSurfacePresentModesKHR calls plus a heap-allocated std::vector to enumerate present modes that were never consulted. Delete them and assign the constant directly. compositeAlpha is a surface property that does not change across swapchain recreation, so select it once at construction instead of re-deriving it (with another heap-allocated vector) on every resize configure. The vkQueueWaitIdle before recreation is left intact as required. Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Window.cpp | 52 +++++++++------------ interfaces/Crafter.Graphics-Window.cppm | 5 ++ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 411ffb1..ae8c029 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -405,6 +405,26 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height colorFormat = selectedFormat.format; colorSpace = selectedFormat.colorSpace; + // Select a supported composite alpha format (not all devices support alpha + // opaque). This is a surface property, so pick it once here rather than on + // every CreateSwapchain. Simply select the first available. + { + VkSurfaceCapabilitiesKHR surfCaps; + Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps)); + const VkCompositeAlphaFlagBitsKHR compositeAlphaFlags[] = { + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, + }; + for (const VkCompositeAlphaFlagBitsKHR compositeAlphaFlag : compositeAlphaFlags) { + if (surfCaps.supportedCompositeAlpha & compositeAlphaFlag) { + compositeAlpha = compositeAlphaFlag; + break; + } + } + } + CreateSwapchain(); VkCommandBufferAllocateInfo cmdBufAllocateInfo {}; @@ -896,18 +916,6 @@ void Window::CreateSwapchain() } - // Select a present mode for the swapchain - uint32_t presentModeCount; - Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, NULL)); - assert(presentModeCount > 0); - - std::vector presentModes(presentModeCount); - Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, presentModes.data())); - - // The VK_PRESENT_MODE_FIFO_KHR mode must always be present as per spec - // This mode waits for the vertical blank ("v-sync") - VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR; - // Find the transformation of the surface VkSurfaceTransformFlagsKHR preTransform; if (surfCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) @@ -920,22 +928,6 @@ void Window::CreateSwapchain() preTransform = surfCaps.currentTransform; } - // Find a supported composite alpha format (not all devices support alpha opaque) - VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - // Simply select the first composite alpha format available - std::vector compositeAlphaFlags = { - VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, - VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, - VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, - VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, - }; - for (auto& compositeAlphaFlag : compositeAlphaFlags) { - if (surfCaps.supportedCompositeAlpha & compositeAlphaFlag) { - compositeAlpha = compositeAlphaFlag; - break; - }; - } - VkSwapchainCreateInfoKHR swapchainCI = {}; swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; swapchainCI.surface = vulkanSurface; @@ -948,7 +940,9 @@ void Window::CreateSwapchain() swapchainCI.imageArrayLayers = 1; swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchainCI.queueFamilyIndexCount = 0; - swapchainCI.presentMode = swapchainPresentMode; + // VK_PRESENT_MODE_FIFO_KHR ("v-sync") is guaranteed available per spec, so + // it is used unconditionally — no present-mode enumeration is needed. + swapchainCI.presentMode = VK_PRESENT_MODE_FIFO_KHR; // Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images swapchainCI.oldSwapchain = oldSwapchain; // Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 9068267..24773a6 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -253,6 +253,11 @@ export namespace Crafter { VkSwapchainKHR swapChain = VK_NULL_HANDLE; VkFormat colorFormat; VkColorSpaceKHR colorSpace; + // Supported composite-alpha mode for vulkanSurface. A surface property + // that does not change across swapchain recreation, so it is selected + // once at construction rather than re-derived on every CreateSwapchain + // (i.e. on every resize configure). + VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; VkImage images[numFrames]; VkImageViewCreateInfo imageViews[numFrames]; // Tracks whether each swapchain image has been rendered (and thus From a8ed369c71ec9e51a1694287c0eae54df55096f6 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:29:29 +0000 Subject: [PATCH 5/5] perf(shaders): drop nonuniformEXT on text font slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fontTextureSlot/fontSamplerSlot are push constants, hence provably dynamically uniform. Wrapping them in nonuniformEXT forced the per-invocation divergent-descriptor path and blocked hoisting the uniform descriptor load out of the per-pixel glyph loop. Removing the decoration is zero correctness risk — it is purely a read-index hint. Not applied to ui-images.comp.glsl, whose slots come from an SSBO load the compiler cannot prove uniform. Co-Authored-By: Claude Opus 4.8 --- shaders/ui-text.comp.glsl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shaders/ui-text.comp.glsl b/shaders/ui-text.comp.glsl index c940035..88a0b9d 100644 --- a/shaders/ui-text.comp.glsl +++ b/shaders/ui-text.comp.glsl @@ -36,9 +36,12 @@ void main() { vec2 t = (sp - it.rect.xy) / it.rect.zw; vec2 uv = mix(it.uv.xy, it.uv.zw, t); + // Font slots are push constants — provably dynamically uniform, so no + // nonuniformEXT: lets the compiler hoist the descriptor load out of the + // per-pixel glyph loop and avoid the divergent-descriptor path. float sdf = texture( - sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)], - uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]), + sampler2D(uiTextures[pc.fontTextureSlot], + uiSamplers[pc.fontSamplerSlot]), uv ).r;