Window - updateInterval runs Update on the window's own clock while the compositor sends no frame callbacks (hidden, minimized): the Wayland loop is a poll() loop, and the Win32 loop sleeps until the next update. Both read the interval each pass. - SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or whose swapchain has other than numFrames images (Mesa's Mailbox has four), falls back to FIFO until the mode is set again. - AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors. - The swapchain has TRANSFER_DST where the surface allows it. - CaptureNextFrame replaces SaveFrame: it records the copy in the frame, before the present, where SaveFrame read an image already presented. A frame unwound past before the write still frees the readback buffer. - Inter-pass barriers on the swapchain image carry the access masks of the stages the passes use. Deferred deletion (issue #101) - VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand their handles to the deletion queue from their destructors and moves, as a frame in flight may still use them; a static owner goes after the driver has torn down. VulkanShader and PipelineRTVulkan move and can't be copied; re-Load and re-Init queue the old pipeline. - The queue and its mutex are never destroyed, and the queue takes handles from any thread; Device::frameCounter is atomic. Both the queue and the staging rings count frames as one window renders them (documented). - DrainDeletions also lets every buffer's staging ring start over: without a Window nothing else reclaims either, so an app without one calls it after waiting for its work. Meshes and buffers - A Mesh rebuild queues the old acceleration structure and builds into the same storage after a barrier, so the BLAS address stays put when it fits. The refit and rebuild barriers cover compute ray queries too. - RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque, and the refit and rebuild fallbacks keep the flags. - A re-upload into a buffer a frame in flight may read (UploadDeviceLocal of the same size, UploadDeviceLocalRange) copies on the GPU, after those reads, through a per-frame staging ring; on ReBAR it used to write the buffer in place. Empty uploads record nothing. - A compressed Mesh::Build that keeps its buffers decompresses into them after the reads of frames in flight, and the hit shaders and ray queries wait for the decompress as well as the BLAS build. Its vertex buffer takes a ranged refit's copies. - Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced in place or erased from a vector. - VulkanBuffer::Create asks for a device address only for buffers with SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the others have address 0. UIRenderer::RegisterBuffer refuses those. - Build inputs are synchronized as shader reads. Other - RTPass pushes the app's own push data and traces at an explicit size. - DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and keeps its free lists sorted. - ImageVulkan's mip chain clamps each level to 1 texel: a non-square image's shorter side went to 0. - VulkanShader throws on a missing file instead of failing in bad_alloc. - CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct GPUs Vulkan lists; anything but such a number is an error. - The debug messenger prints info messages only with CRAFTER_GRAPHICS_VALIDATION set. - Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT headers don't compile as C++), -lcombase dropped, a reference leak fixed. - project.cpp honours --local for Crafter.Event, Math and Asset, as Asset does for Math, and builds every test through one helper, projectArgs included. - Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection, HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime, RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress, VulkanShaderLifetime, WindowUpdateClock; others extended. Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is std::atomic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
548 lines
29 KiB
C++
548 lines
29 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module;
|
|
#include "vulkan/vulkan.h"
|
|
module Crafter.Graphics:Mesh_impl;
|
|
import Crafter.Math;
|
|
import Crafter.Asset;
|
|
import :Mesh;
|
|
import :Device;
|
|
import :Decompress;
|
|
import :Types;
|
|
import std;
|
|
|
|
using namespace Crafter;
|
|
|
|
namespace {
|
|
// Buffer-usage flag set shared by both Build paths. The compressed path
|
|
// appends VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT, and the staged
|
|
// upload path appends VK_BUFFER_USAGE_TRANSFER_DST_BIT. TRANSFER_SRC is in
|
|
// the base because the geometry is now device-local (#73): once it no longer
|
|
// lives in host-mappable memory, a transfer copy is the only way to read it
|
|
// back (debugging, GPU-driven workflows, decompression validation) — a free
|
|
// usage flag that keeps device-local geometry inspectable.
|
|
constexpr VkBufferUsageFlags2 kVertexUsageBase =
|
|
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
|
|
| VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
|
|
| VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
constexpr VkBufferUsageFlags2 kIndexUsageBase =
|
|
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
|
|
|
// Where the geometry buffers are read: the BLAS build, and hit shaders or
|
|
// compute ray queries fetching vertices by address. An upload is ordered
|
|
// before these reads, and a re-upload after the earlier ones, which frames
|
|
// still in flight may be doing. A build reads its inputs as a shader read.
|
|
constexpr VkPipelineStageFlags kGeometryReaders =
|
|
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR
|
|
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR
|
|
| VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
|
// The same, for synchronization2's barriers.
|
|
constexpr VkPipelineStageFlags2 kGeometryReaders2 =
|
|
VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR
|
|
| VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR
|
|
| VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
|
|
|
|
// Translate the portable RTBuildOptions to Vulkan build flags. The two
|
|
// preferences are mutually exclusive; ALLOW_UPDATE is layered on top so
|
|
// a later in-place refit (UPDATE mode) is permitted.
|
|
VkBuildAccelerationStructureFlagsKHR BlasFlags(const RTBuildOptions& options) {
|
|
VkBuildAccelerationStructureFlagsKHR flags =
|
|
options.preference == RTBuildPreference::FastBuild
|
|
? VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR
|
|
: VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR;
|
|
if (options.allowUpdate)
|
|
flags |= VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR;
|
|
return flags;
|
|
}
|
|
|
|
// The geometry's flags for a build: opaque or not, and whether any-hit
|
|
// may run more than once per primitive.
|
|
VkGeometryFlagsKHR GeometryFlags(bool opaque, const RTBuildOptions& options) {
|
|
VkGeometryFlagsKHR flags = opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{};
|
|
if (options.noDuplicateAnyHit)
|
|
flags |= VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR;
|
|
return flags;
|
|
}
|
|
|
|
// Shared BLAS sizing / creation / build-record tail — geometry-type
|
|
// agnostic; both the triangle and the AABB (procedural) paths feed
|
|
// their single geometry through here. `update` selects an in-place
|
|
// refit (VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR, src == dst)
|
|
// over a fresh build; the caller guarantees the geometry topology and
|
|
// primitiveCount match the existing AS and that `flags` carried
|
|
// ALLOW_UPDATE on the original build.
|
|
void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
|
|
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
|
|
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
|
|
.flags = flags,
|
|
.mode = update
|
|
? VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR
|
|
: VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR,
|
|
.geometryCount = 1,
|
|
.pGeometries = &blasGeometry,
|
|
};
|
|
|
|
// A refit writes the BLAS in place, and a rebuild writes the same
|
|
// storage and scratch whenever the new BLAS fits. Earlier work, in
|
|
// this command buffer or in a frame still in flight, may read the BLAS
|
|
// (a TLAS build, a trace, a compute ray query) or write the scratch (a
|
|
// build), so order that before this build. A first build touches
|
|
// nothing earlier.
|
|
if (self.accelerationStructure != VK_NULL_HANDLE) {
|
|
VkMemoryBarrier asBarrier {
|
|
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
|
.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR,
|
|
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR
|
|
};
|
|
vkCmdPipelineBarrier(cmd,
|
|
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
|
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
|
0, 1, &asBarrier, 0, nullptr, 0, nullptr);
|
|
}
|
|
|
|
if (!update) {
|
|
// Fresh build: query sizes, (re)allocate scratch + AS storage,
|
|
// create a new AS handle. The scratch buffer is sized for the
|
|
// build (always >= the update scratch requirement), so it also
|
|
// covers later in-place refits without resizing.
|
|
//
|
|
// On a rebuild the old handle may still be referenced by this
|
|
// command buffer and by frames in flight, so it goes to the
|
|
// deletion queue instead of being destroyed here. It is queued
|
|
// before the Resizes below, which queue the old storage when the
|
|
// new BLAS doesn't fit, so it is freed before the buffer it lives in.
|
|
Device::EnqueueDeletion(self.accelerationStructure);
|
|
self.accelerationStructure = VK_NULL_HANDLE;
|
|
|
|
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
|
|
};
|
|
Device::vkGetAccelerationStructureBuildSizesKHR(
|
|
Device::device,
|
|
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
|
|
&blasBuildGeometryInfo,
|
|
&primitiveCount,
|
|
&blasBuildSizes
|
|
);
|
|
|
|
self.scratchBuffer.Resize(
|
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
blasBuildSizes.buildScratchSize);
|
|
|
|
self.blasBuffer.Resize(
|
|
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR
|
|
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
|
|
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
blasBuildSizes.accelerationStructureSize);
|
|
|
|
VkAccelerationStructureCreateInfoKHR blasCreateInfo{
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
|
|
.buffer = self.blasBuffer.buffer,
|
|
.offset = 0,
|
|
.size = blasBuildSizes.accelerationStructureSize,
|
|
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
|
|
};
|
|
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure));
|
|
|
|
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
|
|
.accelerationStructure = self.accelerationStructure
|
|
};
|
|
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
|
}
|
|
|
|
blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address;
|
|
blasBuildGeometryInfo.dstAccelerationStructure = self.accelerationStructure;
|
|
// UPDATE refits in place (src == dst); a fresh build has no source.
|
|
blasBuildGeometryInfo.srcAccelerationStructure =
|
|
update ? self.accelerationStructure : VK_NULL_HANDLE;
|
|
|
|
VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo {
|
|
.primitiveCount = primitiveCount,
|
|
.primitiveOffset = 0,
|
|
.firstVertex = 0,
|
|
.transformOffset = 0
|
|
};
|
|
VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo;
|
|
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP);
|
|
|
|
self.buildFlags = flags;
|
|
self.builtPrimitiveCount = primitiveCount;
|
|
|
|
// Static (!allowUpdate) meshes can never refit, so their per-mesh
|
|
// scratch is dead weight the moment this build's GPU work completes —
|
|
// release it instead of keeping a persistent DEVICE_LOCAL allocation
|
|
// per mesh (the waste that dominates many-mesh scenes, issue #66). The
|
|
// fresh build recorded above still reads scratchBuffer.address from
|
|
// this command buffer, so a plain Clear() would be a use-after-free;
|
|
// DeferredClear() retires the allocation only once the build's frame
|
|
// has passed its fence (the queue from #101/#102). A later Refit on a
|
|
// static mesh takes the rebuild fallback, whose Resize sees the nulled
|
|
// handle and re-Creates the scratch. Refit-capable meshes keep theirs:
|
|
// an in-place UPDATE reuses it every frame (build scratch ≥ update
|
|
// scratch, so no resize). Only applies to a fresh build — an UPDATE
|
|
// never reaches here for a static mesh.
|
|
if (!update && !self.allowUpdate) {
|
|
self.scratchBuffer.DeferredClear();
|
|
}
|
|
}
|
|
|
|
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
|
|
VkDeviceOrHostAddressConstKHR vertexAddr;
|
|
vertexAddr.deviceAddress = self.vertexBuffer.address;
|
|
|
|
VkDeviceOrHostAddressConstKHR indexAddr;
|
|
indexAddr.deviceAddress = self.indexBuffer.address;
|
|
|
|
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
|
|
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
|
|
.vertexData = vertexAddr,
|
|
.vertexStride = sizeof(Vector<float, 3, 3>),
|
|
.maxVertex = vertexCount - 1,
|
|
.indexType = VK_INDEX_TYPE_UINT32,
|
|
.indexData = indexAddr,
|
|
.transformData = {.deviceAddress = 0}
|
|
};
|
|
VkAccelerationStructureGeometryDataKHR geometryData(trianglesData);
|
|
VkAccelerationStructureGeometryKHR blasGeometry {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
|
|
.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR,
|
|
.geometry = geometryData,
|
|
.flags = self.geometryFlags
|
|
};
|
|
|
|
RecordBLASBuildFromGeometry(self, blasGeometry, indexCount / 3, flags, update, cmd);
|
|
}
|
|
}
|
|
|
|
Mesh::Mesh(Mesh&& other)
|
|
: scratchBuffer(std::move(other.scratchBuffer))
|
|
, blasBuffer(std::move(other.blasBuffer))
|
|
, vertexBuffer(std::move(other.vertexBuffer))
|
|
, indexBuffer(std::move(other.indexBuffer))
|
|
, aabbBuffer(std::move(other.aabbBuffer))
|
|
, compressedStaging(std::move(other.compressedStaging))
|
|
, accelerationStructure(std::exchange(other.accelerationStructure, VK_NULL_HANDLE))
|
|
, blasAddr(std::exchange(other.blasAddr, 0))
|
|
, geometryFlags(other.geometryFlags)
|
|
, buildFlags(other.buildFlags)
|
|
, builtPrimitiveCount(other.builtPrimitiveCount)
|
|
, builtInputCount(other.builtInputCount)
|
|
, allowUpdate(other.allowUpdate) {
|
|
}
|
|
|
|
Mesh& Mesh::operator=(Mesh&& other) {
|
|
if (this == &other) return *this;
|
|
// Ahead of blasBuffer, which it lives in and which queues its own as it
|
|
// takes other's over.
|
|
Device::EnqueueDeletion(accelerationStructure);
|
|
scratchBuffer = std::move(other.scratchBuffer);
|
|
blasBuffer = std::move(other.blasBuffer);
|
|
vertexBuffer = std::move(other.vertexBuffer);
|
|
indexBuffer = std::move(other.indexBuffer);
|
|
aabbBuffer = std::move(other.aabbBuffer);
|
|
compressedStaging = std::move(other.compressedStaging);
|
|
accelerationStructure = std::exchange(other.accelerationStructure, VK_NULL_HANDLE);
|
|
blasAddr = std::exchange(other.blasAddr, 0);
|
|
geometryFlags = other.geometryFlags;
|
|
buildFlags = other.buildFlags;
|
|
builtPrimitiveCount = other.builtPrimitiveCount;
|
|
builtInputCount = other.builtInputCount;
|
|
allowUpdate = other.allowUpdate;
|
|
return *this;
|
|
}
|
|
|
|
Mesh::~Mesh() {
|
|
// The body runs before the members' destructors, so the handle is queued
|
|
// ahead of blasBuffer, which it lives in.
|
|
Device::EnqueueDeletion(accelerationStructure);
|
|
}
|
|
|
|
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options) {
|
|
// Every Build* records an acceleration-structure build; without ray
|
|
// tracing those entry points are not loaded. Fail with the reason.
|
|
Device::RequireRayTracing();
|
|
// Place both inputs in device-local memory (direct map on ReBAR/UMA,
|
|
// staged copy otherwise — UploadDeviceLocal decides) and barrier the
|
|
// upload before the BLAS build reads them. Replaces the previous
|
|
// HOST_VISIBLE allocation that the build (and any hit-shader fetch) would
|
|
// read over PCIe every frame (#73).
|
|
vertexBuffer.UploadDeviceLocal(kVertexUsageBase, verticies.data(), static_cast<std::uint32_t>(verticies.size()), cmd, VK_ACCESS_SHADER_READ_BIT, kGeometryReaders);
|
|
indexBuffer.UploadDeviceLocal(kIndexUsageBase, indicies.data(), static_cast<std::uint32_t>(indicies.size()), cmd, VK_ACCESS_SHADER_READ_BIT, kGeometryReaders);
|
|
|
|
allowUpdate = options.allowUpdate;
|
|
geometryFlags = GeometryFlags(/*opaque*/ true, options);
|
|
builtInputCount = static_cast<std::uint32_t>(verticies.size());
|
|
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), BlasFlags(options), /*update*/ false, cmd);
|
|
}
|
|
|
|
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options) {
|
|
Device::RequireRayTracing();
|
|
if (!Device::memoryDecompressionSupported) {
|
|
// CPU fallback: decompress into temporary host vectors, then take
|
|
// the existing uncompressed path. The data region is decompressed
|
|
// into a discard buffer (consumer is expected to handle data-stream
|
|
// decoding via Compression::DecompressCPU on its own buffer).
|
|
std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount);
|
|
std::vector<std::uint32_t> indices(asset.indexCount);
|
|
std::vector<std::byte> dataDiscard(
|
|
asset.blob.regions.size() >= 3 ? asset.blob.regions[2].decompressedSize : 0);
|
|
std::array<std::span<std::byte>, 3> outputs = {
|
|
std::as_writable_bytes(std::span(vertices)),
|
|
std::as_writable_bytes(std::span(indices)),
|
|
std::span<std::byte>(dataDiscard),
|
|
};
|
|
Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size()));
|
|
Build(vertices, indices, cmd, options);
|
|
return;
|
|
}
|
|
|
|
// The GPU decompressor writes vertex/index directly (the build input is
|
|
// never host-written on this path), so they want pure DEVICE_LOCAL — no
|
|
// host visibility, no map. This is where the BLAS build and any hit-shader
|
|
// fetch then read them from (#73). A ranged Refit copies the moved
|
|
// vertices in, hence TRANSFER_DST.
|
|
const VkBuffer previousVertices = vertexBuffer.buffer;
|
|
const VkBuffer previousIndices = indexBuffer.buffer;
|
|
vertexBuffer.Resize(
|
|
kVertexUsageBase | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
asset.vertexCount);
|
|
indexBuffer.Resize(
|
|
kIndexUsageBase | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
|
asset.indexCount);
|
|
|
|
compressedStaging.Resize(
|
|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
|
|
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
|
|
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
|
|
static_cast<std::uint32_t>(asset.blob.bytes.size()));
|
|
std::memcpy(compressedStaging.value, asset.blob.bytes.data(), asset.blob.bytes.size());
|
|
compressedStaging.FlushDevice();
|
|
|
|
std::array<VkDeviceAddress, 3> dstAddresses = {
|
|
vertexBuffer.address,
|
|
indexBuffer.address,
|
|
0, // data region is not consumed by Mesh; caller handles it separately.
|
|
};
|
|
std::vector<VkDecompressMemoryRegionEXT> regions;
|
|
for (std::size_t i = 0; i < asset.blob.regions.size() && i < 2; ++i) {
|
|
const Compression::RegionMeta& r = asset.blob.regions[i];
|
|
if (r.decompressedSize == 0) continue;
|
|
std::span<const std::byte> streamBytes(
|
|
asset.blob.bytes.data() + r.srcOffset,
|
|
static_cast<std::size_t>(r.compressedSize));
|
|
Decompress::ExpandStreamToTileRegions(
|
|
streamBytes,
|
|
compressedStaging.address + r.srcOffset,
|
|
dstAddresses[i],
|
|
regions);
|
|
}
|
|
|
|
// A rebuild that keeps its buffers (Resize keeps an allocation that fits)
|
|
// decompresses into what frames in flight may still be reading: the
|
|
// decompress waits for those reads. Fresh buffers nothing reads yet.
|
|
if ((vertexBuffer.buffer != VK_NULL_HANDLE && vertexBuffer.buffer == previousVertices)
|
|
|| (indexBuffer.buffer != VK_NULL_HANDLE && indexBuffer.buffer == previousIndices)) {
|
|
const VkMemoryBarrier2 readsDone {
|
|
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
|
.srcStageMask = kGeometryReaders2,
|
|
.srcAccessMask = 0,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_DECOMPRESSION_WRITE_BIT_EXT,
|
|
};
|
|
const VkDependencyInfo beforeDecompress {
|
|
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
|
|
.memoryBarrierCount = 1,
|
|
.pMemoryBarriers = &readsDone,
|
|
};
|
|
vkCmdPipelineBarrier2(cmd, &beforeDecompress);
|
|
}
|
|
|
|
// Then the BLAS build reads the decompressed geometry as a shader read,
|
|
// as do the hit shaders and ray queries that fetch it by address.
|
|
Decompress::DecompressOnGPU(
|
|
cmd,
|
|
regions,
|
|
kGeometryReaders2,
|
|
VK_ACCESS_2_SHADER_READ_BIT);
|
|
|
|
// The compressed staging is only read by the decompress recorded above; the
|
|
// subsequent BLAS build reads the decompressed vertex/index buffers, never
|
|
// this. So hand it to the fence-keyed deletion queue (#101/#102) now rather
|
|
// than pinning host-visible memory for the mesh's whole life. The recorded
|
|
// vkCmdDecompressMemoryEXT still references compressedStaging.address, so it
|
|
// must outlive this submit — which the queue guarantees: it retires the
|
|
// allocation only after framesInFlight frames have elapsed, by which point
|
|
// the decompress submit's fence has cleared.
|
|
compressedStaging.DeferredClear();
|
|
|
|
allowUpdate = options.allowUpdate;
|
|
geometryFlags = GeometryFlags(/*opaque*/ true, options);
|
|
builtInputCount = asset.vertexCount;
|
|
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, BlasFlags(options), /*update*/ false, cmd);
|
|
}
|
|
|
|
namespace {
|
|
// Record an AABB BLAS build (fresh or in-place refit) from a device
|
|
// address that already holds the VkAabbPositionsKHR-compatible boxes.
|
|
// Geometry-source agnostic: the host-upload path (RecordProceduralBuild)
|
|
// points this at self.aabbBuffer, while the device-buffer overloads
|
|
// point it straight at the producer's buffer — no copy involved either
|
|
// way. The geometry's flags are read from self.geometryFlags so an UPDATE
|
|
// keeps the exact geometry description of the original build.
|
|
void RecordProceduralBuildFromAddress(Mesh& self, VkDeviceAddress aabbAddress, std::uint32_t count, std::uint32_t stride, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
|
|
VkAccelerationStructureGeometryAabbsDataKHR aabbsData {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR,
|
|
.data = { .deviceAddress = aabbAddress },
|
|
.stride = stride
|
|
};
|
|
VkAccelerationStructureGeometryDataKHR geometryData;
|
|
geometryData.aabbs = aabbsData;
|
|
VkAccelerationStructureGeometryKHR blasGeometry {
|
|
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR,
|
|
.geometryType = VK_GEOMETRY_TYPE_AABBS_KHR,
|
|
.geometry = geometryData,
|
|
// Non-opaque by default (mirrors the WebGPU path) so any-hit
|
|
// shaders run for procedural geometry unless the caller opts out.
|
|
.flags = self.geometryFlags
|
|
};
|
|
|
|
RecordBLASBuildFromGeometry(self, blasGeometry, count, flags, update, cmd);
|
|
}
|
|
|
|
// Re-upload AABB build input from host memory and record an AABB BLAS
|
|
// build (fresh or in-place refit). Shared by the host-span BuildProcedural
|
|
// and RefitProcedural.
|
|
void RecordProceduralBuild(Mesh& self, std::span<const RTAabb> aabbs, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
|
|
// 24-byte-stride VkAabbPositionsKHR-compatible build input
|
|
// (static_assert'd in the interface). Same usage set as the triangle
|
|
// inputs: AS-build read-only + device address. UploadDeviceLocal places
|
|
// it in device-local memory (direct map on ReBAR/UMA, staged otherwise,
|
|
// #73) and barriers the upload before the build reads it. A refit
|
|
// re-uploads into the same same-sized buffer: Resize reuses the existing
|
|
// allocation (count unchanged), so the device address — and the geometry
|
|
// it feeds — stays stable for an in-place UPDATE.
|
|
self.aabbBuffer.UploadDeviceLocal(kVertexUsageBase, aabbs.data(), static_cast<std::uint32_t>(aabbs.size()), cmd, VK_ACCESS_SHADER_READ_BIT, kGeometryReaders);
|
|
|
|
RecordProceduralBuildFromAddress(self, self.aabbBuffer.address, static_cast<std::uint32_t>(aabbs.size()), sizeof(RTAabb), flags, update, cmd);
|
|
}
|
|
}
|
|
|
|
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) {
|
|
Device::RequireRayTracing();
|
|
geometryFlags = GeometryFlags(opaque, options);
|
|
allowUpdate = options.allowUpdate;
|
|
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
|
|
|
|
RecordProceduralBuild(*this, aabbs, BlasFlags(options), /*update*/ false, cmd);
|
|
}
|
|
|
|
void Mesh::BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options, std::uint32_t stride) {
|
|
Device::RequireRayTracing();
|
|
// Zero-copy procedural build: the AABBs already live in `aabbAddress`
|
|
// (a device buffer a GPU compute pass wrote). Nothing touches
|
|
// self.aabbBuffer — the build input is the producer's buffer directly.
|
|
// The caller owns that buffer's lifetime + usage flags
|
|
// (ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY + SHADER_DEVICE_ADDRESS)
|
|
// and must barrier the producing writes before this build reads them.
|
|
geometryFlags = GeometryFlags(opaque, options);
|
|
allowUpdate = options.allowUpdate;
|
|
builtInputCount = count;
|
|
|
|
RecordProceduralBuildFromAddress(*this, aabbAddress, count, stride, BlasFlags(options), /*update*/ false, cmd);
|
|
}
|
|
|
|
void Mesh::Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
|
|
// Whole-vertex-array refit: the entire array is the dirty range.
|
|
Refit(verticies, indicies, 0, static_cast<std::uint32_t>(verticies.size()), cmd);
|
|
}
|
|
|
|
void Mesh::Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, std::uint32_t dirtyVertexOffset, std::uint32_t dirtyVertexCount, VkCommandBuffer cmd) {
|
|
// A hardware in-place UPDATE is only valid when the original build asked
|
|
// for it, an AS already exists, and the topology is unchanged. Otherwise
|
|
// fall back to a full rebuild (mode BUILD) of the same buffers — still
|
|
// correct, just not the cheap path.
|
|
const bool sameTopology =
|
|
accelerationStructure != VK_NULL_HANDLE
|
|
&& verticies.size() == builtInputCount
|
|
&& indicies.size() == static_cast<std::size_t>(builtPrimitiveCount) * 3;
|
|
const bool update = allowUpdate && sameTopology;
|
|
|
|
if (!update) {
|
|
// Counts may have changed (or update wasn't permitted): take the
|
|
// resizing build path, preserving the caller's original flags. The
|
|
// dirty window is irrelevant here — a fresh build re-uploads the whole
|
|
// array, which is why the full arrays are passed even on this overload.
|
|
Build(verticies, indicies, cmd, RTBuildOptions{
|
|
.preference = (buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR)
|
|
? RTBuildPreference::FastBuild : RTBuildPreference::FastTrace,
|
|
.allowUpdate = allowUpdate,
|
|
.noDuplicateAnyHit = (geometryFlags & VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR) != 0,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Same-sized buffers: overwrite the moved vertex positions in place so the
|
|
// device address (and thus the geometry the UPDATE reads) stays stable.
|
|
// The index buffer is deliberately left untouched: a hardware AS UPDATE
|
|
// cannot change topology, so the indices are immutable here — re-uploading
|
|
// them would be a wasted memcpy + flush + barrier of unchanged data every
|
|
// refit. This means a bitwise-different-but-topologically-equivalent index
|
|
// array passed on refit is ignored, consistent with the documented
|
|
// contract that a refit may only move vertex positions.
|
|
//
|
|
// Re-upload only the dirty sub-range [dirtyVertexOffset, +dirtyVertexCount):
|
|
// the rest of the vertex buffer already holds last refit's positions, so a
|
|
// ranged upload patches just the vertices that moved (#119). The window is
|
|
// clamped to the array — a caller window past the end would otherwise read
|
|
// out of bounds / overflow the copy. UploadDeviceLocalRange copies into the
|
|
// existing same-sized allocation (no Resize), so the address the UPDATE
|
|
// reads stays stable, after the reads of the frames still in flight and
|
|
// before the build, staging only the dirty slice.
|
|
std::uint32_t offset = std::min(dirtyVertexOffset, static_cast<std::uint32_t>(verticies.size()));
|
|
std::uint32_t count = std::min(dirtyVertexCount, static_cast<std::uint32_t>(verticies.size()) - offset);
|
|
if (count != 0) {
|
|
vertexBuffer.UploadDeviceLocalRange(verticies.data() + offset, offset, count, cmd, VK_ACCESS_SHADER_READ_BIT, kGeometryReaders);
|
|
}
|
|
|
|
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), buildFlags, /*update*/ true, cmd);
|
|
}
|
|
|
|
void Mesh::RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd) {
|
|
const bool sameTopology =
|
|
accelerationStructure != VK_NULL_HANDLE
|
|
&& aabbs.size() == builtInputCount;
|
|
const bool update = allowUpdate && sameTopology;
|
|
|
|
if (!update && aabbs.size() != builtInputCount) {
|
|
// Topology changed — a full rebuild needs the new count.
|
|
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
|
|
}
|
|
RecordProceduralBuild(*this, aabbs, buildFlags, update, cmd);
|
|
}
|
|
|
|
void Mesh::RefitProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, VkCommandBuffer cmd, std::uint32_t stride) {
|
|
// Zero-copy refit: the GPU producer rewrote the same device buffer in
|
|
// place; re-record the build (UPDATE when allowed + count unchanged,
|
|
// else a full rebuild) reading straight from `aabbAddress`. As with the
|
|
// device-buffer BuildProcedural, nothing touches self.aabbBuffer and the
|
|
// caller is responsible for barriering the producing writes.
|
|
const bool sameTopology =
|
|
accelerationStructure != VK_NULL_HANDLE
|
|
&& count == builtInputCount;
|
|
const bool update = allowUpdate && sameTopology;
|
|
|
|
if (!update && count != builtInputCount) {
|
|
builtInputCount = count;
|
|
}
|
|
RecordProceduralBuildFromAddress(*this, aabbAddress, count, stride, buildFlags, update, cmd);
|
|
}
|
|
|