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

@ -472,6 +472,33 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
fc.GetInterfacesAndImplementations(ifaces, flushImpls);
cfg.tests.push_back(std::move(flushTest));
// Ranged FlushDevice: UI descriptor registration now flushes only the
// written descriptor byte range instead of the whole heap, rounding the
// range outward to nonCoherentAtomSize via AlignMappedFlushRange. The
// rounding is pure math and the coherent gate is pure logic, so this
// test drives both directly with no GPU device.
Test rangedFlushTest;
Configuration& rfc = rangedFlushTest.config;
rfc.path = cfg.path;
rfc.name = "VulkanBufferRangedFlush";
rfc.outputName = "VulkanBufferRangedFlush";
rfc.type = ConfigurationType::Executable;
rfc.target = cfg.target;
rfc.march = cfg.march;
rfc.mtune = cfg.mtune;
rfc.debug = cfg.debug;
rfc.sysroot = cfg.sysroot;
rfc.dependencies = cfg.dependencies;
rfc.externalDependencies = cfg.externalDependencies;
rfc.compileFlags = cfg.compileFlags;
rfc.linkFlags = cfg.linkFlags;
rfc.defines = cfg.defines;
rfc.cFiles = cfg.cFiles;
std::vector<fs::path> rangedFlushImpls(impls.begin(), impls.end());
rangedFlushImpls.emplace_back("tests/VulkanBufferRangedFlush/main");
rfc.GetInterfacesAndImplementations(ifaces, rangedFlushImpls);
cfg.tests.push_back(std::move(rangedFlushTest));
// Issue #89: Device::PreferDirectDeviceWrite chooses the upload strategy
// for a CPU-written, GPU-read buffer — direct HOST_VISIBLE|DEVICE_LOCAL
// map+write on ReBAR/UMA vs. staged-into-pure-DEVICE_LOCAL on a small