perf(buffer): reuse staging via a per-frame-in-flight ring in UploadDeviceLocal (#120) #139

Merged
catbot merged 1 commit from claude/issue-120 into master 2026-06-17 21:52:04 +02:00
Member

Problem

On non-ReBAR hardware the staged branch of VulkanBuffer::UploadDeviceLocal (interfaces/Crafter.Graphics-VulkanBuffer.cppm:281-300) did Create (create+getreq+alloc+map) + DeferredClear (later free) per call. Mesh::Refit / RecordProceduralBuild hit this every frame for deforming meshes — a full device-memory alloc/free cycle per mesh per frame, on exactly the hardware the staged path targets.

Fix

A persistent per-buffer staging ring sized to a high-water mark, reallocated only on growth (via Resize, which defers the outgrown allocation) — mirroring the TLAS instance/metadata high-water reuse.

Why a ring and not a single shared buffer

The vkCmdCopyBuffer still reads the staging buffer after the call returns (the reason the old code used DeferredClear). With frames pipelined framesInFlight deep, overwriting one shared staging buffer next frame would clobber data the previous frame's copy is still reading. Indexing by frameCounter % framesInFlight gives each in-flight frame its own slot; a slot is rewritten only after framesInFlight frames have elapsed — the exact window after which single-queue submission order (the same guarantee the #101 deletion queue relies on) ensures that copy has completed.

No ReBAR/UMA regression

The ring is grown lazily, on first entry into the staged branch. ReBAR/UMA hardware always takes the direct-write if branch and never touches the ring, so it never constructs a staging allocation it doesn't use — a runtime no-op there, a straight upgrade on non-resizable-BAR machines.

Testing

crafter-build test21 passed. Directly relevant:

  • BLASBuildOptions — exercises the staged Build→Refit path (the ring's core reuse case) with zero validation errors.
  • DeferredDeletion, MeshDecompressStagingRelease, TLASHighWaterMark, VulkanBufferResizeReuse.

Backend/invisible change — no screenshot.

Resolves #120

## Problem On non-ReBAR hardware the staged branch of `VulkanBuffer::UploadDeviceLocal` (`interfaces/Crafter.Graphics-VulkanBuffer.cppm:281-300`) did `Create` (create+getreq+alloc+map) + `DeferredClear` (later free) **per call**. `Mesh::Refit` / `RecordProceduralBuild` hit this every frame for deforming meshes — a full device-memory alloc/free cycle per mesh per frame, on exactly the hardware the staged path targets. ## Fix A persistent per-buffer **staging ring** sized to a high-water mark, reallocated only on growth (via `Resize`, which defers the outgrown allocation) — mirroring the TLAS instance/metadata high-water reuse. ### Why a ring and not a single shared buffer The `vkCmdCopyBuffer` still reads the staging buffer after the call returns (the reason the old code used `DeferredClear`). With frames pipelined `framesInFlight` deep, overwriting one shared staging buffer next frame would clobber data the previous frame's copy is still reading. Indexing by `frameCounter % framesInFlight` gives each in-flight frame its own slot; a slot is rewritten only after `framesInFlight` frames have elapsed — the exact window after which single-queue submission order (the same guarantee the #101 deletion queue relies on) ensures that copy has completed. ### No ReBAR/UMA regression The ring is grown **lazily**, on first entry into the staged branch. ReBAR/UMA hardware always takes the direct-write `if` branch and never touches the ring, so it never constructs a staging allocation it doesn't use — a runtime no-op there, a straight upgrade on non-resizable-BAR machines. ## Testing `crafter-build test` — **21 passed**. Directly relevant: - `BLASBuildOptions` — exercises the staged Build→Refit path (the ring's core reuse case) with zero validation errors. - `DeferredDeletion`, `MeshDecompressStagingRelease`, `TLASHighWaterMark`, `VulkanBufferResizeReuse`. Backend/invisible change — no screenshot. Resolves #120
The staged branch of VulkanBuffer::UploadDeviceLocal did a full
Create (create+getreq+alloc+map) + DeferredClear (later free) per call.
Mesh::Refit / RecordProceduralBuild hit this every frame for deforming
meshes on no-/small-BAR hardware — a device-memory alloc/free cycle per
mesh per frame, on exactly the hardware the staged path targets.

Replace it with a persistent per-buffer staging ring sized to a
high-water mark, reallocated (via Resize, which defers the outgrown
allocation) only on growth — mirroring the TLAS instance/metadata reuse.

The ring is a per-frame-in-flight ring, not a single shared buffer: the
vkCmdCopyBuffer still reads staging after the call returns, and with
frames pipelined framesInFlight deep, overwriting one shared buffer next
frame would clobber data the previous frame's copy is still reading.
Indexing by frameCounter % framesInFlight gives each in-flight frame its
own slot, reused only after framesInFlight frames elapse — the same
window the #101 deletion queue relies on for GPU completion.

The ring is grown lazily, on first entry into the staged branch, so
ReBAR/UMA hardware (which always takes the direct-write branch) never
constructs a staging allocation it does not use — keeping this a runtime
no-op there and a straight upgrade on non-resizable-BAR machines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 5a9d909f5d into master 2026-06-17 21:52:04 +02:00
catbot deleted branch claude/issue-120 2026-06-17 21:52:04 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!139
No description provided.