perf(mesh): dirty-range vertex upload for deforming-mesh Refit (#119) #138

Merged
catbot merged 1 commit from claude/issue-119 into master 2026-06-17 21:51:26 +02:00
Member

Problem

Deforming-mesh Mesh::Refit UploadDeviceLocals the entire vertex array every frame on the in-place UPDATE path (full host write + flush + barrier on the direct path; full re-stage + copy on the staged path), even when only a few vertices moved (Crafter.Graphics-Mesh.cpp:407-409).

Fix

  • VulkanBuffer::UploadDeviceLocalRange — a dirty-range counterpart to UploadDeviceLocal that writes/flushes (direct) or stages/copies (staged) and barriers only the half-open element range [offset, offset+count) of an already-allocated device-local buffer. No Resize, so the device address (and the geometry an AS UPDATE reads) stays stable. The direct-vs-staged choice is read from the buffer's actual memory type — not the sub-range size, which PreferDirectDeviceWrite would mis-route onto a non-host-visible buffer. The ranged flush is rounded to nonCoherentAtomSize and clamped to the allocation size (mappedSize is now recorded for every buffer).
  • Dirty-range Mesh::Refit overload taking the full vertex/index arrays plus a (dirtyVertexOffset, dirtyVertexCount) window. The existing full-span Refit delegates to it with the whole array as the window, so its behaviour is unchanged. On the UPDATE path only the window is uploaded; the rest of the device buffer keeps last refit's positions. When an UPDATE isn't possible (no allowUpdate, or counts changed) it falls back to the full-span rebuild — which is why the full arrays are still passed (window clamped to bounds).
  • WebGPU/DOM keeps API symmetry: with no hardware AS it ignores the window and rebuilds the host BVH from the full geometry, identical to the full-span Refit.

Contract change

Callers wanting the optimization pass the dirty window: Refit(verts, indices, dirtyVertexOffset, dirtyVertexCount, cmd). The original Refit(verts, indices, cmd) is untouched.

Testing

crafter-build test — all 20 tests pass. BLASBuildOptions now exercises the dirty-range refit on the direct path, the staged path (forced via the zeroed upload budget), and the count-change rebuild fallback, asserting AS-handle / blasAddr stability and zero Vulkan validation errors. The wasm/wasm32-wasip1 target also builds cleanly, covering the DOM overload.

Resolves #119

🤖 Generated with Claude Code

## Problem Deforming-mesh `Mesh::Refit` `UploadDeviceLocal`s the **entire** vertex array every frame on the in-place UPDATE path (full host write + flush + barrier on the direct path; full re-stage + copy on the staged path), even when only a few vertices moved (`Crafter.Graphics-Mesh.cpp:407-409`). ## Fix - **`VulkanBuffer::UploadDeviceLocalRange`** — a dirty-range counterpart to `UploadDeviceLocal` that writes/flushes (direct) or stages/copies (staged) and barriers only the half-open element range `[offset, offset+count)` of an already-allocated device-local buffer. No `Resize`, so the device address (and the geometry an AS UPDATE reads) stays stable. The direct-vs-staged choice is read from the buffer's actual memory type — not the sub-range size, which `PreferDirectDeviceWrite` would mis-route onto a non-host-visible buffer. The ranged flush is rounded to `nonCoherentAtomSize` and clamped to the allocation size (`mappedSize` is now recorded for every buffer). - **Dirty-range `Mesh::Refit` overload** taking the full vertex/index arrays plus a `(dirtyVertexOffset, dirtyVertexCount)` window. The existing full-span `Refit` delegates to it with the whole array as the window, so its behaviour is unchanged. On the UPDATE path only the window is uploaded; the rest of the device buffer keeps last refit's positions. When an UPDATE isn't possible (no `allowUpdate`, or counts changed) it falls back to the full-span rebuild — which is why the full arrays are still passed (window clamped to bounds). - **WebGPU/DOM** keeps API symmetry: with no hardware AS it ignores the window and rebuilds the host BVH from the full geometry, identical to the full-span `Refit`. ## Contract change Callers wanting the optimization pass the dirty window: `Refit(verts, indices, dirtyVertexOffset, dirtyVertexCount, cmd)`. The original `Refit(verts, indices, cmd)` is untouched. ## Testing `crafter-build test` — all 20 tests pass. `BLASBuildOptions` now exercises the dirty-range refit on the **direct** path, the **staged** path (forced via the zeroed upload budget), and the **count-change rebuild fallback**, asserting AS-handle / `blasAddr` stability and **zero Vulkan validation errors**. The wasm/`wasm32-wasip1` target also builds cleanly, covering the DOM overload. Resolves #119 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Mesh::Refit re-uploaded the entire vertex array every frame on the
in-place UPDATE path (full host write + flush + barrier on the direct
path; full re-stage + copy on the staged path), even when only a handful
of vertices moved.

Add VulkanBuffer::UploadDeviceLocalRange — a dirty-range counterpart to
UploadDeviceLocal that writes/flushes/stages/copies and barriers only the
half-open element range [offset, offset+count) of an already-allocated
device-local buffer. It picks direct-map vs staged-copy from the memory
type the buffer was actually allocated with (not the sub-range size, which
PreferDirectDeviceWrite would mis-route), and the direct path's ranged
flush is rounded to nonCoherentAtomSize and clamped to the allocation size
(mappedSize is now recorded for every buffer, not just mapped ones).

Add a dirty-range Refit overload taking the full vertex/index arrays plus
a (dirtyVertexOffset, dirtyVertexCount) window. The full-span Refit now
delegates to it with the whole array as the window. On the in-place UPDATE
path only the declared window is uploaded — the rest of the device buffer
retains last refit's positions; when an UPDATE isn't possible it falls
back to the full-span rebuild, which is why the full arrays are still
passed. The WebGPU/DOM backend keeps API symmetry: it has no hardware AS,
so it ignores the window and rebuilds the host BVH from the full geometry.

BLASBuildOptions exercises the dirty-range refit on the direct path, the
staged path, and the count-change rebuild fallback, asserting AS-handle /
blasAddr stability and zero Vulkan validation errors.

Resolves #119

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit e3edb87c0f into master 2026-06-17 21:51:26 +02:00
catbot deleted branch claude/issue-119 2026-06-17 21:51:26 +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!138
No description provided.