perf(mesh): place RT geometry device-local via #89 upload strategy (#73) #109

Merged
catbot merged 3 commits from claude/issue-73 into master 2026-06-17 20:05:57 +02:00
Member

Resolves #73

Problem

vertexBuffer / indexBuffer / aabbBuffer were allocated HOST_VISIBLE (system RAM). Their usage flags (SHADER_DEVICE_ADDRESS, plus STORAGE_BUFFER on the index buffer) deliberately expose geometry for hit-shader fetch — the dominant RT shading pattern — so an app fetching geometry in closest-hit shaders reads vertex/index data from system RAM over PCIe every vkCmdTraceRaysKHR. The BLAS build (and every refit) also read these as build inputs.

Fix

New VulkanBuffer::UploadDeviceLocal places the buffer in device-local memory and picks how at runtime via the #89 strategy (Device::PreferDirectDeviceWrite) — never unconditional staging:

  • ReBAR / UMA (the dev machine): allocate HOST_VISIBLE | DEVICE_LOCAL (DEVICE_LOCAL preferred), map transiently, memcpy, flush if non-coherent. No staging buffer.
  • No / small BAR: allocate pure DEVICE_LOCAL + TRANSFER_DST, fill a transient HOST_VISIBLE staging buffer, vkCmdCopyBuffer, then DeferredClear() the staging buffer onto the fence-keyed deletion queue (#101/#102) so it outlives the copy submit.

The geometry buffers become non-mapped (Mapped==false) so the destination is free to be device-local-only. Same-size re-uploads reuse the allocation, keeping the device address stable across an in-place AS UPDATE refit. The compressed Build path now allocates vertex/index as pure DEVICE_LOCAL — the GPU decompressor fills them directly, with no host write.

Correctness caveats addressed

  1. Conditional win — placement is improved for build/refit reads regardless; the per-trace hit-fetch win applies only when the app fetches geometry in shaders. Not over-claimed.
  2. No unconditional staging — ReBAR/UMA takes the direct map path (#89 avoids the wasted copy).
  3. Staged path enqueues its staging buffer via DeferredClear() (#101/#102) — no lifetime hazard.
  4. Non-coherent chosen types still get an explicit flush (gated on memoryPropertyFlagsChosen).

Tests

BLASBuildOptions (real hardware + GPU-assisted validation):

  • asserts device-local placement on the triangle, procedural, and staged paths;
  • forces the staged path by zeroing Device::directWriteBudget, then validates a fresh staged build + an in-place UPDATE refit;
  • 0 Vulkan validation errors across direct and staged paths.

All 19 project tests pass (crafter-build test).

🤖 Generated with Claude Code

Resolves #73 ## Problem `vertexBuffer` / `indexBuffer` / `aabbBuffer` were allocated `HOST_VISIBLE` (system RAM). Their usage flags (`SHADER_DEVICE_ADDRESS`, plus `STORAGE_BUFFER` on the index buffer) deliberately expose geometry for hit-shader fetch — the dominant RT shading pattern — so an app fetching geometry in closest-hit shaders reads vertex/index data from system RAM over PCIe every `vkCmdTraceRaysKHR`. The BLAS build (and every refit) also read these as build inputs. ## Fix New `VulkanBuffer::UploadDeviceLocal` places the buffer in device-local memory and picks *how* at runtime via the #89 strategy (`Device::PreferDirectDeviceWrite`) — never unconditional staging: - **ReBAR / UMA** (the dev machine): allocate `HOST_VISIBLE | DEVICE_LOCAL` (DEVICE_LOCAL preferred), map transiently, memcpy, flush if non-coherent. **No staging buffer.** - **No / small BAR**: allocate pure `DEVICE_LOCAL` + `TRANSFER_DST`, fill a transient `HOST_VISIBLE` staging buffer, `vkCmdCopyBuffer`, then `DeferredClear()` the staging buffer onto the fence-keyed deletion queue (#101/#102) so it outlives the copy submit. The geometry buffers become non-mapped (`Mapped==false`) so the destination is free to be device-local-only. Same-size re-uploads reuse the allocation, keeping the device address stable across an in-place AS UPDATE refit. The compressed `Build` path now allocates vertex/index as pure `DEVICE_LOCAL` — the GPU decompressor fills them directly, with no host write. ### Correctness caveats addressed 1. Conditional win — placement is improved for build/refit reads regardless; the per-trace hit-fetch win applies only when the app fetches geometry in shaders. Not over-claimed. 2. No unconditional staging — ReBAR/UMA takes the direct map path (#89 avoids the wasted copy). 3. Staged path enqueues its staging buffer via `DeferredClear()` (#101/#102) — no lifetime hazard. 4. Non-coherent chosen types still get an explicit flush (gated on `memoryPropertyFlagsChosen`). ## Tests `BLASBuildOptions` (real hardware + GPU-assisted validation): - asserts device-local placement on the triangle, procedural, and staged paths; - forces the staged path by zeroing `Device::directWriteBudget`, then validates a fresh staged build + an in-place UPDATE refit; - **0 Vulkan validation errors** across direct and staged paths. All 19 project tests pass (`crafter-build test`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
vertexBuffer/indexBuffer/aabbBuffer were HOST_VISIBLE (system RAM), so the
BLAS build, every refit, and any hit-shader geometry fetch read them over
PCIe. The usage flags (SHADER_DEVICE_ADDRESS, plus STORAGE on the index
buffer) exist precisely to expose geometry for hit-shader fetch, the dominant
RT shading pattern.

Add VulkanBuffer::UploadDeviceLocal, which places a CPU-written/GPU-read
buffer in device-local memory and picks the upload mechanism at runtime via
the #89 strategy (Device::PreferDirectDeviceWrite):
  - ReBAR/UMA: allocate HOST_VISIBLE|DEVICE_LOCAL (DEVICE_LOCAL preferred),
    map transiently, memcpy, flush-if-non-coherent. No staging buffer.
  - no/small BAR: allocate pure DEVICE_LOCAL + TRANSFER_DST, fill a transient
    HOST_VISIBLE staging buffer, vkCmdCopyBuffer, then DeferredClear the
    staging buffer onto the fence-keyed deletion queue (#101/#102) so it
    outlives the copy submit.

The geometry buffers become non-mapped so the destination is free to be
device-local-only. Same-size re-uploads reuse the allocation, so the device
address stays stable across an in-place AS UPDATE refit. The compressed Build
path now allocates vertex/index as pure DEVICE_LOCAL — the GPU decompressor
fills them directly, no host write.

Tests: BLASBuildOptions asserts device-local placement on the triangle,
procedural, and (budget-forced) staged paths, and exercises the staged copy +
deferred-deletion + in-place refit under GPU-assisted validation with zero
validation errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	interfaces/Crafter.Graphics-Mesh.cppm
- Add TRANSFER_SRC to RT geometry usage so device-local geometry (now in
  VRAM, no longer host-mappable) stays copyable/inspectable.
- MeshDecompressStagingRelease: read decompressed vertex/index back via a
  device->host copy instead of the removed host-mapped .value/FlushHost, and
  build the mesh with allowUpdate=true so the retained per-mesh scratch (#66)
  doesn't also land in the deletion queue — isolating the assertion to the
  released compressed staging (#67).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 8b1db01222 into master 2026-06-17 20:05:57 +02:00
catbot deleted branch claude/issue-73 2026-06-17 20:05:57 +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!109
No description provided.