perf(mesh): dirty-range vertex upload for deforming-mesh Refit (#119)
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>
This commit is contained in:
parent
5948bb91c1
commit
1f4c77000a
5 changed files with 199 additions and 8 deletions
|
|
@ -157,6 +157,42 @@ int main() {
|
|||
Check(tri.blasAddr == triAddrBefore,
|
||||
"Refit kept the same blasAddr (instances stay valid)");
|
||||
|
||||
// Dirty-range refit (issue #119): move only a sub-window of the vertices
|
||||
// and pass it as [offset, count). The ranged upload patches just those
|
||||
// vertices in place; the UPDATE must still keep the same AS handle /
|
||||
// blasAddr, and the validation layer (checked at the end) is the real proof
|
||||
// that the ranged host-write / flush / barrier — and the staged variant
|
||||
// below — are spec-correct. The untouched vertices retain their last values
|
||||
// in the device buffer, so the refit BLAS is over the full deformed cube.
|
||||
{
|
||||
// Full array of moved positions; we declare only the back half [4,8)
|
||||
// as the dirty window, so only those 4 vertices are re-uploaded.
|
||||
auto verts = CubeVerts(2.0f);
|
||||
auto idx = CubeIndices();
|
||||
VkCommandBuffer cmd = BeginCmd();
|
||||
tri.Refit(verts, idx, /*dirtyVertexOffset*/ 4, /*dirtyVertexCount*/ 4, cmd);
|
||||
SubmitWait(cmd);
|
||||
}
|
||||
Check(tri.accelerationStructure == triHandleBefore,
|
||||
"dirty-range Refit kept the same AS handle (in-place UPDATE, #119)");
|
||||
Check(tri.blasAddr == triAddrBefore,
|
||||
"dirty-range Refit kept the same blasAddr (#119)");
|
||||
|
||||
// A dirty-range refit on a mesh whose counts changed must still fall back
|
||||
// to a full rebuild (the dirty window is irrelevant on that path) and
|
||||
// succeed — same robustness as the full-span Refit fallback.
|
||||
{
|
||||
auto verts = CubeVerts(1.0f);
|
||||
verts.push_back({2.0f, 2.0f, 2.0f}); // count change → topology change
|
||||
auto idx = CubeIndices();
|
||||
idx.insert(idx.end(), {0u, 1u, 8u});
|
||||
VkCommandBuffer cmd = BeginCmd();
|
||||
tri.Refit(verts, idx, /*dirtyVertexOffset*/ 8, /*dirtyVertexCount*/ 1, cmd);
|
||||
SubmitWait(cmd);
|
||||
}
|
||||
Check(tri.blasAddr != 0 && tri.builtPrimitiveCount == 13,
|
||||
"dirty-range Refit with a count change rebuilt the BLAS (13 prims, #119)");
|
||||
|
||||
// ── Triangle BLAS: fast-build, no update → flags reflect the choice. ─
|
||||
Mesh triFast;
|
||||
{
|
||||
|
|
@ -340,6 +376,19 @@ int main() {
|
|||
Check(staged.accelerationStructure == stagedHandle,
|
||||
"staged-upload Refit kept the same AS handle (in-place UPDATE, #73)");
|
||||
|
||||
// Dirty-range refit on the staged path (#119): the ranged upload must
|
||||
// take the stage-a-sub-slice + vkCmdCopyBuffer-at-offset path (the
|
||||
// buffer is pure DEVICE_LOCAL), keep the AS handle, and stay
|
||||
// validation-clean. Only the front half [0,4) is declared dirty.
|
||||
{
|
||||
auto verts3 = CubeVerts(2.0f);
|
||||
VkCommandBuffer cmd = BeginCmd();
|
||||
staged.Refit(verts3, idx, /*dirtyVertexOffset*/ 0, /*dirtyVertexCount*/ 4, cmd);
|
||||
SubmitWait(cmd);
|
||||
}
|
||||
Check(staged.accelerationStructure == stagedHandle,
|
||||
"staged-upload dirty-range Refit kept the same AS handle (#119)");
|
||||
|
||||
Device::directWriteBudget = savedBudget;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue