perf(ui): flush only the written descriptor range, not the whole heap (#61)

WriteBufferDescriptor / WriteSampledImageDescriptor / RegisterSampler /
WriteSwapchainDescriptors each FlushDevice()'d the entire multi-KB per-frame
descriptor heap after writing one few-byte descriptor. Add a ranged
VulkanBuffer::FlushDevice(offset, bytes) that flushes only the touched bytes,
rounding the range outward to nonCoherentAtomSize as vkFlushMappedMemoryRanges
requires (the WHOLE_SIZE path sidestepped that). The coherent-memory gate from
issue #60 is preserved — coherent memory still skips the flush entirely.

The descriptor writers now self-flush their written range, so the redundant
whole-heap flushes in UIRenderer::Initialize and the resize callback are gone.

Rounding lives in AlignMappedFlushRange (pure math) and is unit-tested without
a device in the new VulkanBufferRangedFlush test; the change was also verified
end-to-end by running HelloUI on a real GPU with validation layers (font-atlas
glyphs, sampler and storage-image descriptors all flush correctly, no VUIDs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 18:29:24 +00:00
commit 6d7ad87e38
6 changed files with 236 additions and 7 deletions

View file

@ -67,16 +67,16 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
WriteFontAtlasDescriptor();
}
// Flush the host-mapped descriptor heaps so the GPU sees what we wrote.
for (auto& h : heap_->resourceHeap) h.FlushDevice();
for (auto& h : heap_->samplerHeap) h.FlushDevice();
// No whole-heap flush here: WriteSwapchainDescriptors / WriteFontAtlasDescriptor
// / RegisterSampler each flush their own written byte range above, so the GPU
// already sees everything written during setup.
// Re-bind the swapchain image descriptors after every resize. Window
// already drained the queue and rebuilt the imageViews[] before
// firing the event, so vkWriteResourceDescriptorsEXT is safe here.
// WriteSwapchainDescriptors flushes its own range, so no extra flush needed.
resizeSub_.SetEvent(&window.onResize, [this]() {
WriteSwapchainDescriptors();
for (auto& h : heap_->resourceHeap) h.FlushDevice();
});
(void)initCmd; // reserved for future image-layout tweaks
@ -315,6 +315,12 @@ void UIRenderer::WriteSwapchainDescriptors() {
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
// Only the one image descriptor at outImageSlot_ changed in each per-frame
// heap (same slot offset in every frame), so flush just that byte range
// rather than the whole multi-KB heap.
const std::uint32_t off = heap_->ImageByteOffset(outImageSlot_);
const std::uint32_t sz = Device::descriptorHeapProperties.imageDescriptorSize;
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
}
void UIRenderer::WriteFontAtlasDescriptor() {
@ -366,7 +372,11 @@ void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
// One image descriptor written per heap, at the same slot offset — flush
// just that range rather than the whole heap.
const std::uint32_t off = heap_->ImageByteOffset(slot);
const std::uint32_t sz = Device::descriptorHeapProperties.imageDescriptorSize;
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
}
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
@ -389,7 +399,11 @@ void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress addre
Device::vkWriteResourceDescriptorsEXT(
Device::device, Window::numFrames, resources.data(), destinations.data()
);
for (auto& h : heap_->resourceHeap) h.FlushDevice();
// One buffer descriptor written per heap, at the same slot offset — flush
// just that range rather than the whole heap.
const std::uint32_t off = heap_->BufferByteOffset(slot);
const std::uint32_t sz = Device::descriptorHeapProperties.bufferDescriptorSize;
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
}
SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
@ -406,7 +420,11 @@ SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
Device::vkWriteSamplerDescriptorsEXT(
Device::device, Window::numFrames, infos.data(), destinations.data()
);
for (auto& h : heap_->samplerHeap) h.FlushDevice();
// One sampler descriptor written per heap, at the same slot offset — flush
// just that range rather than the whole sampler heap.
const std::uint32_t off = heap_->SamplerByteOffset(range.firstElement);
const std::uint32_t sz = Device::descriptorHeapProperties.samplerDescriptorSize;
for (auto& h : heap_->samplerHeap) h.FlushDevice(off, sz);
return SamplerSlot{heap_, range.firstElement};
}