ClusterGeometry.kt
src/jvmMain/kotlin/afield/graphics/render/ClusterGeometry.kt · 185 lines
package afield.graphics.render
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.render.lighting.LightingGpuTimer
import afield.math.linalg.Rectanglef
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.NVClusterAccelerationStructure.*
import org.lwjgl.vulkan.KHRAccelerationStructure.*
import org.lwjgl.vulkan.KHRSynchronization2.VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR
import org.lwjgl.vulkan.KHRSynchronization2.VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR
import org.lwjgl.vulkan.KHRSynchronization2.VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR
import org.lwjgl.vulkan.KHRRayTracingPipeline.VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR
import org.lwjgl.vulkan.VK13.*
import java.nio.ByteBuffer
Builds NVIDIA cluster acceleration structures from compute-shader output: ClusterGeometry validates TriangleCluster records, builds cluster-level acceleration structures and one cluster BLAS for ray tracing.
It requires VK_NV_cluster_acceleration_structure; ClusterGeometryInput names the producer buffers it reads.
data class TriangleCluster(val firstVertex: Int,val firstIndex: Int,val vertexCount: Int,val triangleCount: Int,
One Field cluster description, eight uint32 words / 32 bytes, usable by ordinary compute shaders. Positions are vec4 (xyz in world/object units); indices are uint32 LOCAL to this cluster's vertex slice. A cluster may change counts, positions and indices on every build. IDs stay yours. Invalid GPU records become degenerate geometry and set ClusterGeometry.status rather than passing unchecked addresses/counts to Vulkan. No host readback occurs during builds.
data class TriangleCluster(val firstVertex: Int,val firstIndex: Int,val vertexCount: Int,val triangleCount: Int,
val clusterId: Int,val geometryId: Int=0) {
init
init { require(firstVertex>=0 && firstIndex>=0 && vertexCount>=3 && triangleCount>=1 && geometryId>=0) }
fun write(out: ByteBuffer)
fun write(out: ByteBuffer) {
require(out.order()==java.nio.ByteOrder.nativeOrder()) { "Cluster descriptions need a native-order ByteBuffer" }
out.putInt(firstVertex).putInt(firstIndex).putInt(vertexCount).putInt(triangleCount)
out.putInt(clusterId).putInt(geometryId).putInt(0).putInt(0) }
companion object { const val STRIDE=32 }
data class ClusterGeometryInput(val positions: VulkanBuffer,val indices: VulkanBuffer,val clusters: VulkanBuffer)
Borrowed producer buffers. Keep alive through build completion; synchronize any subsequent writes.
clusters contains TriangleCluster records. Buffer sizes, not arbitrary user-supplied addresses,
determine bounds in the GPU validation/conversion pass.
data class ClusterGeometryInput(val positions: VulkanBuffer,val indices: VulkanBuffer,val clusters: VulkanBuffer)
class ClusterGeometry(val renderer: Renderer,val capacity: Int,val verticesPerCluster: Int=128,
General NV Mega Geometry builder: Field compute -> validated clusters -> CLAS -> one cluster BLAS. It owns bounded build/storage/scratch resources, never producer buffers. Single graphics queue; call build explicitly after a producer changes geometry. Static geometry needs only one build. Recreate to increase capacity. A rebuild changes this BLAS's bounds: rebuild the containing TLAS. Cluster BLAS has an address but no VkAccelerationStructure handle; use it in RayTracingInstance.
class ClusterGeometry(val renderer: Renderer,val capacity: Int,val verticesPerCluster: Int=128,
val trianglesPerCluster: Int=128,val maxGeometryId: Int=0) : AccelerationStructureAddress,AutoCloseable {
init
init {
require(renderer.device.supportsClusterAccelerationStructure) { "VK_NV_cluster_acceleration_structure is not enabled" }
require(capacity in 1..1_000_000 && verticesPerCluster>=3 && trianglesPerCluster>=1 && maxGeometryId>=0)
}
data class Limits(val vertices: Int,val triangles: Int,val scratchAlignment: Int,val clusterAlignment: Int,
data class Limits(val vertices: Int,val triangles: Int,val scratchAlignment: Int,val clusterAlignment: Int,
val blasAlignment: Int,val maxGeometryId: Int)
val limits: Limits
private val owned=mutableListOf<VulkanBuffer>()
private val timer=LightingGpuTimer(renderer)
val gpuMilliseconds get()=timer.samples.toList()
private val positions: VulkanBuffer
private val indices: VulkanBuffer
internal val records: VulkanBuffer
private val addresses: VulkanBuffer
private val sizes: VulkanBuffer
private val blasRecord: VulkanBuffer
private val blasAddress: VulkanBuffer
private val clas: VulkanBuffer
private val blas: VulkanBuffer
private val scratch: VulkanBuffer
val status: VulkanBuffer
One uint per cluster: 0 valid, 1 invalid descriptor, 2 invalid index, 4 nonfinite vertex.
private val convert: ComputeModule
private val task: ComputeMesh
private var closed=false
private var built=false
val allocatedBytes get()=owned.sumOf { it.requestedSize }
init
body — 51 lines, from line 82
init {
require(renderer.device.supportsClusterAccelerationStructure) { "VK_NV_cluster_acceleration_structure is not enabled" }
limits=MemoryStack.stackPush().use { s ->
val p=VkPhysicalDeviceClusterAccelerationStructurePropertiesNV.calloc(s).`sType$Default`()
VK11.vkGetPhysicalDeviceProperties2(renderer.device.physicalDevice.vkPhysicalDevice,
VkPhysicalDeviceProperties2.calloc(s).`sType$Default`().pNext(p.address()))
Limits(p.maxVerticesPerCluster(),p.maxTrianglesPerCluster(),p.clusterScratchByteAlignment(),p.clusterByteAlignment(),
p.clusterBottomLevelByteAlignment(),p.maxClusterGeometryIndex())
}
require(capacity in 1..1_000_000 && verticesPerCluster in 3..limits.vertices && trianglesPerCluster in 1..limits.triangles && maxGeometryId in 0..limits.maxGeometryId)
check(VkClusterAccelerationStructureBuildTriangleClusterInfoNV.SIZEOF==64 && VkClusterAccelerationStructureBuildClustersBottomLevelInfoNV.SIZEOF==16)
try {
positions=allocate(capacity.toLong()*verticesPerCluster*16)
indices=allocate(capacity.toLong()*trianglesPerCluster*12)
records=allocate(capacity.toLong()*64);addresses=allocate(capacity.toLong()*8);sizes=allocate(capacity.toLong()*4)
status=allocate(capacity.toLong()*4);blasRecord=allocate(16);blasAddress=allocate(8)
val requirements=MemoryStack.stackPush().use { s ->
fun size(bottom: Boolean): Pair<Long,Long> {val p=VkAccelerationStructureBuildSizesInfoKHR.calloc(s).`sType$Default`()
vkGetClusterAccelerationStructureBuildSizesNV(renderer.device.vkDevice,inputInfo(s,bottom,capacity),p)
return p.accelerationStructureSize() to p.buildScratchSize() }
size(false) to size(true)
}
clas=allocate(requirements.first.first+limits.clusterAlignment,VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR)
blas=allocate(requirements.second.first+limits.blasAlignment,VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR)
scratch=allocate(maxOf(requirements.first.second,requirements.second.second,1)+limits.scratchAlignment)
convert=ComputeModule(parameters(renderer){name="cluster-input-validation";compute=SOURCE})
check(convert.valid) { "Cluster input converter did not compile" }
val pointer=convert.pointer();task=ComputeMesh(renderer,pointer);pointer+="convert" to task
task.push.setMembers(convert.ci.pushConstants.single().members)
} catch(t: Throwable) { owned.asReversed().forEach { it.cleanup() };timer.close();throw t }
}
private fun allocate(bytes: Long,extra: Int=0)=VulkanBuffer(renderer,bytes,
extra or VK_BUFFER_USAGE_STORAGE_BUFFER_BIT or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT or VK_BUFFER_USAGE_TRANSFER_SRC_BIT or
VK_BUFFER_USAGE_TRANSFER_DST_BIT or VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,needsDeviceAddress=true).also { owned+=it }
private fun address(b: VulkanBuffer,alignment: Int=1): Long {
check(b.gpuAddress>0);return (b.gpuAddress+alignment-1)/alignment*alignment
}
private fun inputInfo(s: MemoryStack,bottom: Boolean,count: Int): VkClusterAccelerationStructureInputInfoNV {
val info=VkClusterAccelerationStructureInputInfoNV.calloc(s).`sType$Default`()
.maxAccelerationStructureCount(if(bottom)1 else count).flags(VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR)
.opMode(if(bottom)VK_CLUSTER_ACCELERATION_STRUCTURE_OP_MODE_EXPLICIT_DESTINATIONS_NV else VK_CLUSTER_ACCELERATION_STRUCTURE_OP_MODE_IMPLICIT_DESTINATIONS_NV)
.opType(if(bottom)VK_CLUSTER_ACCELERATION_STRUCTURE_OP_TYPE_BUILD_CLUSTERS_BOTTOM_LEVEL_NV else VK_CLUSTER_ACCELERATION_STRUCTURE_OP_TYPE_BUILD_TRIANGLE_CLUSTER_NV)
if(bottom)info.opInput().pClustersBottomLevel(VkClusterAccelerationStructureClustersBottomLevelInputNV.calloc(s).`sType$Default`()
.maxTotalClusterCount(count).maxClusterCountPerAccelerationStructure(count))
else info.opInput().pTriangleClusters(VkClusterAccelerationStructureTriangleClusterInputNV.calloc(s).`sType$Default`()
.vertexFormat(VK_FORMAT_R32G32B32_SFLOAT).maxGeometryIndexValue(maxGeometryId).maxClusterUniqueGeometryCount(1)
.maxClusterTriangleCount(trianglesPerCluster).maxClusterVertexCount(verticesPerCluster)
.maxTotalTriangleCount(count*trianglesPerCluster).maxTotalVertexCount(count*verticesPerCluster).minPositionTruncateBitCount(0))
return info
}
fun build(command: CommandBuffer,pipeline: RayTracingModule,input: ClusterGeometryInput,count: Int=capacity)
Record outside a render pass on the graphics queue. The pipeline must opt into cluster AS; Vulkan requires it bound during cluster builds. Field rebinds descriptors/state when tracing.
fun build(command: CommandBuffer,pipeline: RayTracingModule,input: ClusterGeometryInput,count: Int=capacity) {
check(!closed && !command.inRenderPassInstance)
require(count in 1..capacity && pipeline.valid && pipeline.parameters.allowClusterAccelerationStructure)
require(pipeline.parameters.renderer===renderer)
listOf(input.positions,input.indices,input.clusters).forEach { require(it.renderer===renderer && it.gpuAddress>0) }
require(input.positions.requestedSize%16==0L && input.indices.requestedSize%4==0L && input.clusters.requestedSize>=count.toLong()*32)
require(input.positions.requestedSize/16<=Int.MAX_VALUE && input.indices.requestedSize/4<=Int.MAX_VALUE)
barrier(command,VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT,
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT or VK_PIPELINE_STAGE_2_TRANSFER_BIT,VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT or VK_ACCESS_2_TRANSFER_WRITE_BIT)
timer.begin(command,renderer.currentImage)
task.x=(count+63)/64
for((key,buffer) in mapOf("sourcePositions" to input.positions,"sourceIndices" to input.indices,"sourceClusters" to input.clusters,
"positions" to positions,"indices" to indices,"records" to records,"status" to status))task.push[key]=buffer
task.push["count"]=count;task.push["maxVertices"]=verticesPerCluster;task.push["maxTriangles"]=trianglesPerCluster
task.push["positionCapacity"]=(input.positions.requestedSize/16).toInt();task.push["indexCapacity"]=(input.indices.requestedSize/4).toInt()
task.push["maxGeometry"]=maxGeometryId
convert.render(command,Rectanglef())
MemoryStack.stackPush().use { s ->
vkCmdUpdateBuffer(command.vkCommandBuffer,blasRecord.buffer,0,s.malloc(16).putInt(count).putInt(8).putLong(address(addresses)).flip())
vkCmdUpdateBuffer(command.vkCommandBuffer,blasAddress.buffer,0,s.longs(address(blas,limits.blasAlignment)))
}
val stage=VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR
val access=VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR or VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR or VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT
barrier(command,VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT or VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_SHADER_WRITE_BIT or VK_ACCESS_2_TRANSFER_WRITE_BIT,stage,access)
vkCmdBindPipeline(command.vkCommandBuffer,VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR,pipeline.pipeline)
MemoryStack.stackPush().use { s ->
fun record(bottom: Boolean) {
val c=VkClusterAccelerationStructureCommandsInfoNV.calloc(s).`sType$Default`().input(inputInfo(s,bottom,count))
.scratchData(address(scratch,limits.scratchAlignment))
c.srcInfosArray().deviceAddress(address(if(bottom)blasRecord else records)).stride(if(bottom)16 else 64).size(if(bottom)16 else count.toLong()*64)
c.dstAddressesArray().deviceAddress(address(if(bottom)blasAddress else addresses)).stride(8).size(if(bottom)8 else count.toLong()*8)
if(!bottom){c.dstImplicitData(address(clas,limits.clusterAlignment));c.dstSizesArray().deviceAddress(address(sizes)).stride(4).size(count.toLong()*4)}
vkCmdBuildClusterAccelerationStructureIndirectNV(command.vkCommandBuffer,c)
}
record(false);barrier(command,stage,access,stage,access);record(true)
}
barrier(command,stage,access,VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,VK_ACCESS_2_MEMORY_READ_BIT)
timer.end(command,renderer.currentImage);built=true
}
override fun deviceAddress(): Long
override fun deviceAddress(): Long { check(built && !closed);return address(blas,limits.blasAlignment) }
override fun close()
override fun close() { if(closed)return;closed=true;renderer.cleanup+=Runnable { convert.cleanup();owned.asReversed().forEach { it.cleanup() };timer.close() } }
companion object
companion object {
private fun barrier(c: CommandBuffer,src: Long,srcAccess: Long,dst: Long,dstAccess: Long)=MemoryStack.stackPush().use { s ->
vkCmdPipelineBarrier2(c.vkCommandBuffer,VkDependencyInfo.calloc(s).`sType$Default`().pMemoryBarriers(
VkMemoryBarrier2.calloc(1,s).`sType$Default`().srcStageMask(src).srcAccessMask(srcAccess).dstStageMask(dst).dstAccessMask(dstAccess)))
}
private val SOURCE get()=ClusterGeometry::class.java.getResource("/afield/four/geometry/clusters.comp")!!.readText()
}
}