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>
This commit is contained in:
parent
39b882a9cb
commit
5f858509c8
4 changed files with 161 additions and 23 deletions
|
|
@ -235,6 +235,83 @@ namespace Crafter {
|
|||
);
|
||||
}
|
||||
|
||||
// Upload `count` host elements from `src` into this buffer as
|
||||
// device-local, GPU-read geometry, choosing placement at runtime via
|
||||
// the #89 upload strategy (Device::PreferDirectDeviceWrite) and then
|
||||
// recording a barrier from the upload to (dstStageMask, dstAccessMask)
|
||||
// on `cmd`:
|
||||
// ReBAR / UMA (direct) — allocate HOST_VISIBLE with DEVICE_LOCAL as a
|
||||
// best-effort preference (GetMemoryType lands the host-visible
|
||||
// device-local type that the strategy already proved exists), map
|
||||
// transiently, memcpy, flush if the chosen type isn't coherent,
|
||||
// unmap. No staging buffer: it would be pure overhead on a bar where
|
||||
// the device-local heap is itself host-writable.
|
||||
// No / small BAR (staged) — allocate pure DEVICE_LOCAL (+ TRANSFER_DST),
|
||||
// fill a transient HOST_VISIBLE staging buffer, vkCmdCopyBuffer into
|
||||
// this buffer, then hand the staging buffer to the fence-keyed
|
||||
// deferred-deletion queue (#101/#102) so it outlives the copy submit.
|
||||
// Requires !Mapped: the destination must be free to be device-local-only
|
||||
// (a persistent map would force HOST_VISIBLE), so the direct path maps
|
||||
// just long enough to write. A same-size re-upload reuses the allocation
|
||||
// (Resize), so the device address stays stable across an in-place AS
|
||||
// UPDATE refit that re-calls this.
|
||||
void UploadDeviceLocal(VkBufferUsageFlags2 usageFlags, const T* src, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) {
|
||||
VkDeviceSize bytes = static_cast<VkDeviceSize>(count) * sizeof(T);
|
||||
VkAccessFlags srcAccessMask;
|
||||
VkPipelineStageFlags srcStageMask;
|
||||
if (Device::PreferDirectDeviceWrite(bytes)) {
|
||||
Resize(usageFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
void* mapped = nullptr;
|
||||
Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, VK_WHOLE_SIZE, 0, &mapped));
|
||||
std::memcpy(mapped, src, bytes);
|
||||
// Match FlushDevice()'s gate: a non-coherent type needs an
|
||||
// explicit flush before the device reads the written range.
|
||||
if (!(memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
|
||||
VkMappedMemoryRange range {
|
||||
.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
|
||||
.memory = memory,
|
||||
.offset = 0,
|
||||
.size = VK_WHOLE_SIZE
|
||||
};
|
||||
vkFlushMappedMemoryRanges(Device::device, 1, &range);
|
||||
}
|
||||
vkUnmapMemory(Device::device, memory);
|
||||
srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
|
||||
srcStageMask = VK_PIPELINE_STAGE_HOST_BIT;
|
||||
} else {
|
||||
Resize(usageFlags | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, count);
|
||||
VulkanBuffer<T, true> staging;
|
||||
// SHADER_DEVICE_ADDRESS: Create always queries the buffer device
|
||||
// address (and allocates with the device-address bit); the
|
||||
// staging buffer's own address is otherwise unused.
|
||||
staging.Create(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count);
|
||||
std::memcpy(staging.value, src, bytes);
|
||||
staging.FlushDevice();
|
||||
VkBufferCopy region { .srcOffset = 0, .dstOffset = 0, .size = bytes };
|
||||
vkCmdCopyBuffer(cmd, staging.buffer, buffer, 1, ®ion);
|
||||
// The queued copy still reads the staging buffer after this call
|
||||
// returns, so a plain Clear() would be a use-after-free; hand it
|
||||
// to the fence-keyed queue (#101/#102), which frees it once the
|
||||
// copy's frame clears its fence. DeferredClear nulls the handle,
|
||||
// so `staging`'s destructor here is a no-op.
|
||||
staging.DeferredClear();
|
||||
srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
}
|
||||
|
||||
VkBufferMemoryBarrier barrier = {
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
|
||||
.srcAccessMask = srcAccessMask,
|
||||
.dstAccessMask = dstAccessMask,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.buffer = buffer,
|
||||
.offset = 0,
|
||||
.size = VK_WHOLE_SIZE
|
||||
};
|
||||
vkCmdPipelineBarrier(cmd, srcStageMask, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL);
|
||||
}
|
||||
|
||||
void FlushDevice() requires(Mapped) {
|
||||
// Coherent memory needs no explicit flush — host writes are
|
||||
// automatically visible to the device.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue