diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 58340bc..5138839 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 {}; @@ -432,6 +452,70 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i])); } + // Per-frame info structs: everything that never varies between frames is + // set here once. Render() patches the barriers' image/oldLayout and the + // sync handles that follow currentBuffer/frameCounter (submitInfo's + // wait/signal semaphores + command buffer, presentInfo's wait semaphore). + cmdBufInfo = {}; + cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + // Submit: stage mask + the 1/1/1 counts are invariant; the wait/signal + // semaphore handles and the command buffer are patched per frame. + submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.pNext = VK_NULL_HANDLE; + submitInfo.pWaitDstStageMask = &submitPipelineStages; + submitInfo.waitSemaphoreCount = 1; + submitInfo.signalSemaphoreCount = 1; + submitInfo.commandBufferCount = 1; + + 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 + // wait semaphore is the per-image render-finished semaphore, patched per + // frame in Render() (it follows currentBuffer). + presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.pNext = NULL; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = &swapChain; + presentInfo.pImageIndices = ¤tBuffer; + presentInfo.waitSemaphoreCount = 1; + lastMousePos = {0,0}; mouseDelta = {0,0}; currentMousePos = {0,0}; @@ -659,11 +743,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; @@ -675,8 +760,7 @@ void Window::Render() { // between Render calls), recreate and retry once. // Pick this frame's acquire semaphore from a free-running CPU counter: the // image index (currentBuffer) isn't known until acquire returns, so the - // acquire's signal semaphore can't be keyed by the image. Held in a local - // so submitInfo below can point at the same handle. + // acquire's signal semaphore can't be keyed by the image. const std::uint32_t acquireSlot = static_cast(frameCounter % numAcquireSemaphores); VkSemaphore imageAcquired = imageAcquiredSemaphores[acquireSlot]; { @@ -702,18 +786,8 @@ void Window::Render() { Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX)); Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[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 @@ -722,20 +796,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. @@ -748,27 +813,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}); @@ -800,45 +873,25 @@ 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])); - // Submit waits on this frame's acquire semaphore and signals this image's - // render-finished semaphore; the fence (keyed by image index) lets a - // future Render() reuse this image's command buffer + heap slot safely. - VkSubmitInfo submitInfo{}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.pNext = VK_NULL_HANDLE; - submitInfo.pWaitDstStageMask = &submitPipelineStages; - submitInfo.waitSemaphoreCount = 1; - submitInfo.pWaitSemaphores = &imageAcquired; - submitInfo.signalSemaphoreCount = 1; + // Patch the per-frame fields of the cached submitInfo/presentInfo (the + // invariant fields were set once in the constructor): submit waits on this + // frame's acquire semaphore and signals this image's render-finished + // semaphore, present waits on that same render-finished semaphore. The + // fence (keyed by image index) lets a future Render() reuse this image's + // command buffer + heap slot safely. The semaphore handles live in member + // arrays, so taking their addresses here is stable for the call. + submitInfo.pWaitSemaphores = &imageAcquiredSemaphores[acquireSlot]; submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer]; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; - + submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentBuffer])); - VkPresentInfoKHR presentInfo = {}; - presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - presentInfo.pNext = NULL; - presentInfo.swapchainCount = 1; - presentInfo.pSwapchains = &swapChain; - presentInfo.pImageIndices = ¤tBuffer; + presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer]; - presentInfo.waitSemaphoreCount = 1; VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo); if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) { @@ -873,21 +926,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: {}", @@ -924,18 +978,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) @@ -948,22 +990,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; @@ -976,7 +1002,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 6eb00d0..0676b96 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 @@ -246,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 @@ -258,6 +270,20 @@ export namespace Crafter { std::array imageInitialised{}; std::thread thread; VkCommandBuffer drawCmdBuffers[numFrames]; + // 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, and — for the multi-frame-in-flight + // sync below — submitInfo/presentInfo's wait/signal semaphores plus + // submitInfo's command buffer, which follow currentBuffer/frameCounter). + // presentInfo.pImageIndices and pSwapchains point at the + // currentBuffer/swapChain members, so they track value changes + // (including swapchain recreation) automatically. + VkSubmitInfo submitInfo; + VkCommandBufferBeginInfo cmdBufInfo; + VkImageMemoryBarrier acquireBarrier; + VkImageMemoryBarrier presentBarrier; + VkPresentInfoKHR presentInfo; // ── Multi-frame-in-flight synchronisation (issue #40) ────────────── // The renderer keeps up to numFrames frames in flight, so a single @@ -301,6 +327,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 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;