fix(device): fence-keyed deferred resource-deletion queue (#101)
Since #40 dropped the per-frame vkQueueWaitIdle, frames are pipelined: a resource the CPU is done with may still be read by the GPU for up to numFrames-1 more frames. VulkanBuffer::Resize's destroy-and-recreate path was therefore a live use-after-free (#63), no longer masked by the wait-idle. Device gains a monotonic, frame-counter-keyed deletion queue: EnqueueDeletion tags {buffer, memory} with the current frameCounter; ReclaimDeletions (called per frame after the fence wait) frees entries once framesInFlight frames have elapsed; DrainDeletions frees everything after a wait-idle. VulkanBuffer::DeferredClear hands handles to the queue and nulls the handle, and Resize uses it instead of immediate Clear(). Window::Render sets framesInFlight at init, reclaims after the per-image fence wait, bumps Device::frameCounter once per frame, and drains on the resize / OUT_OF_DATE wait-idle paths. The destructor keeps immediate Clear() (callers destroying mid-flight remain responsible, unchanged). Adds the DeferredDeletion test: drives the retire timing on a real headless device with real buffers, stepping Device::frameCounter to pin the exact reclaim frame, asserting validation stays silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c3d7f52891
commit
cb012d7068
6 changed files with 312 additions and 1 deletions
31
project.cpp
31
project.cpp
|
|
@ -586,6 +586,37 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
rc.GetInterfacesAndImplementations(ifaces, resizeImpls);
|
||||
cfg.tests.push_back(std::move(resizeTest));
|
||||
|
||||
// Issue #101: fence-keyed deferred resource-deletion queue. Since #40
|
||||
// dropped the per-frame wait-idle, destroying a buffer the GPU may
|
||||
// still read (Resize's reallocate path) is a use-after-free.
|
||||
// VulkanBuffer::DeferredClear / Resize now hand handles to Device's
|
||||
// queue, which ReclaimDeletions frees only after framesInFlight frames
|
||||
// and DrainDeletions frees on a wait-idle. The retire timing is driven
|
||||
// on a real headless device (real buffers so the frees execute and the
|
||||
// validation layer can object) by stepping Device::frameCounter — no
|
||||
// swapchain/window needed, so it shares the native build settings.
|
||||
Test deferredTest;
|
||||
Configuration& dc = deferredTest.config;
|
||||
dc.path = cfg.path;
|
||||
dc.name = "DeferredDeletion";
|
||||
dc.outputName = "DeferredDeletion";
|
||||
dc.type = ConfigurationType::Executable;
|
||||
dc.target = cfg.target;
|
||||
dc.march = cfg.march;
|
||||
dc.mtune = cfg.mtune;
|
||||
dc.debug = cfg.debug;
|
||||
dc.sysroot = cfg.sysroot;
|
||||
dc.dependencies = cfg.dependencies;
|
||||
dc.externalDependencies = cfg.externalDependencies;
|
||||
dc.compileFlags = cfg.compileFlags;
|
||||
dc.linkFlags = cfg.linkFlags;
|
||||
dc.defines = cfg.defines;
|
||||
dc.cFiles = cfg.cFiles;
|
||||
std::vector<fs::path> deferredImpls(impls.begin(), impls.end());
|
||||
deferredImpls.emplace_back("tests/DeferredDeletion/main");
|
||||
dc.GetInterfacesAndImplementations(ifaces, deferredImpls);
|
||||
cfg.tests.push_back(std::move(deferredTest));
|
||||
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue