feat(vulkan-rt): Mesh::BuildProcedural — AABB (procedural) BLAS build path (#33)

Adds the native counterpart of the WebGPU Mesh::BuildProcedural: uploads
the boxes as a 24-byte-stride VkAabbPositionsKHR-compatible device buffer
(AS-build-input read-only + device address) and builds the BLAS from one
VK_GEOMETRY_TYPE_AABBS_KHR geometry. opaque maps to
VK_GEOMETRY_OPAQUE_BIT_KHR, default-false semantics matching the WebGPU
path so any-hit shaders run. The BLAS sizing/create/build tail is factored
into a geometry-type-agnostic helper shared with the triangle path; no
TLAS/instance changes needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-13 00:17:29 +00:00
commit a9e42f9c90
2 changed files with 88 additions and 26 deletions

View file

@ -39,31 +39,10 @@ namespace {
constexpr VkBufferUsageFlags2 kIndexUsageBase = constexpr VkBufferUsageFlags2 kIndexUsageBase =
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) { // Shared BLAS sizing / creation / build-record tail — geometry-type
VkDeviceOrHostAddressConstKHR vertexAddr; // agnostic; both the triangle and the AABB (procedural) paths feed
vertexAddr.deviceAddress = self.vertexBuffer.address; // their single geometry through here.
void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkCommandBuffer cmd) {
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 = VK_GEOMETRY_OPAQUE_BIT_KHR
};
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{ VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
@ -72,7 +51,6 @@ namespace {
.pGeometries = &blasGeometry, .pGeometries = &blasGeometry,
}; };
auto primitiveCount = indexCount / 3;
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = { VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
}; };
@ -122,6 +100,34 @@ namespace {
}; };
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} }
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, 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 = VK_GEOMETRY_OPAQUE_BIT_KHR
};
RecordBLASBuildFromGeometry(self, blasGeometry, indexCount / 3, cmd);
}
} }
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) { void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
@ -203,3 +209,32 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd); RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd);
} }
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd) {
this->opaque = opaque;
// 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.
aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size()));
std::memcpy(aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb));
aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
VkAccelerationStructureGeometryAabbsDataKHR aabbsData {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR,
.data = { .deviceAddress = aabbBuffer.address },
.stride = sizeof(RTAabb)
};
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 = opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{}
};
RecordBLASBuildFromGeometry(*this, blasGeometry, static_cast<std::uint32_t>(aabbs.size()), cmd);
}

View file

@ -31,12 +31,27 @@ import Crafter.Asset;
import :VulkanBuffer; import :VulkanBuffer;
export namespace Crafter { export namespace Crafter {
// One procedural primitive's axis-aligned bounding box, in object
// space. Layout-compatible with VkAabbPositionsKHR (24-byte stride),
// so a span of these uploads directly as a
// VK_GEOMETRY_TYPE_AABBS_KHR build input. Mirrors the DOM-side
// definition below so portable user code compiles unchanged.
struct RTAabb {
float min[3];
float max[3];
};
static_assert(sizeof(RTAabb) == sizeof(VkAabbPositionsKHR));
class Mesh { class Mesh {
public: public:
VulkanBuffer<char, false> scratchBuffer; VulkanBuffer<char, false> scratchBuffer;
VulkanBuffer<char, false> blasBuffer; VulkanBuffer<char, false> blasBuffer;
VulkanBuffer<Vector<float, 3, 3>, true> vertexBuffer; VulkanBuffer<Vector<float, 3, 3>, true> vertexBuffer;
VulkanBuffer<std::uint32_t, true> indexBuffer; VulkanBuffer<std::uint32_t, true> indexBuffer;
// AABB build input for the procedural path (BuildProcedural).
// Lifetime contract matches vertexBuffer/indexBuffer: must stay
// alive until the build submitted on `cmd` completes.
VulkanBuffer<RTAabb, true> aabbBuffer;
// Lives until the cmd buffer issued by the compressed Build path // Lives until the cmd buffer issued by the compressed Build path
// completes execution. Kept as a member so the recorded // completes execution. Kept as a member so the recorded
// vkCmdDecompressMemoryEXT references valid memory until the queue // vkCmdDecompressMemoryEXT references valid memory until the queue
@ -57,6 +72,18 @@ export namespace Crafter {
// Region 2 (data) is not consumed here — the caller decompresses it // Region 2 (data) is not consumed here — the caller decompresses it
// into their own buffer if needed (or uses Compression::DecompressCPU). // into their own buffer if needed (or uses Compression::DecompressCPU).
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd); void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd);
// Build an AABB (procedural) BLAS from a list of object-space
// boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to
// instances of this mesh must be a
// VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR group
// carrying an intersection shader; that shader is invoked for
// each box the ray enters and reports the surface hit via
// reportIntersectionEXT. `opaque` maps to
// VK_GEOMETRY_OPAQUE_BIT_KHR: pass false (the WebGPU-path
// default) to let any-hit shaders run. Same scratch/lifetime
// handling as the triangle Build; instances reference blasAddr
// exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd);
}; };
} }
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM #endif // !CRAFTER_GRAPHICS_WINDOW_DOM