From 8b95b7e883a4e919d6ea3bb027e3b7d2f3cbcb63 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:27:10 +0000 Subject: [PATCH] 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 -- 2.54.0