#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::render_types::{DrawObject, InstancedCluster, SkinnedDrawObject};
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::post::rt_reflections::RtReflectionSettings;
use concinnity_core::render::retire_pool::RetirePool;
use concinnity_core::render::rt_accel::{
AccelBook, EmptyHead, FailureStreak, InstanceBlas, RefreshMode, RtUpdate, SeedSet,
SlotLiveness, empty_head,
};
use concinnity_core::render::rt_geom::RtDynamicMode;
use concinnity_core::render::rt_refit::{BlasUpdate, SkinnedShape};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSArray;
use objc2_metal::{
MTLAccelerationStructure, MTLAccelerationStructureCommandEncoder,
MTLAccelerationStructureGeometryDescriptor, MTLAccelerationStructureInstanceDescriptor,
MTLAccelerationStructureInstanceDescriptorType, MTLAccelerationStructureInstanceOptions,
MTLAccelerationStructureTriangleGeometryDescriptor, MTLAccelerationStructureUsage,
MTLAttributeFormat, MTLBuffer, MTLCommandBuffer as _, MTLCommandEncoder as _,
MTLCommandQueue as _, MTLComputeCommandEncoder as _, MTLComputePipelineState, MTLDevice as _,
MTLIndexType, MTLInstanceAccelerationStructureDescriptor, MTLPackedFloat3, MTLPackedFloat4x3,
MTLPrimitiveAccelerationStructureDescriptor, MTLRenderCommandEncoder, MTLRenderPipelineState,
MTLRenderStages, MTLResource, MTLResourceOptions, MTLResourceUsage, MTLSize,
};
use std::ptr::NonNull;
use concinnity_core::render::uniforms::SkinParams;
use super::builtin_shaders::compute_pipeline;
use super::context::write_buffer_slice;
use super::encode::ComputeEncode;
use super::error::{allocation_failed, completed_command_buffer};
use super::rt_ring::{RtFrameRing, SkinnedBlasSet, TlasKey};
pub(in crate::metal) const VERTEX_STRIDE: usize = 56;
type Structure = Retained<ProtocolObject<dyn MTLAccelerationStructure>>;
pub(crate) struct RtState {
pub settings: Option<RtReflectionSettings>,
pub accel: Option<RtAccelData>,
pub dynamic_mode: RtDynamicMode,
pub skinned_geometry: bool,
pub update_streak: FailureStreak,
pub pipelines: RtPipelines,
pub retired: RetirePool<RtAccelData>,
}
impl RtState {
pub(crate) fn retire_accel(&mut self, frame_id: u64) {
if let Some(accel) = self.accel.take() {
self.retired.push(frame_id, accel);
}
}
pub(crate) fn collect_retired(&mut self, frame_id: u64, depth: usize) {
self.retired.collect(frame_id, depth as u64);
}
}
pub(crate) struct RtPipelines {
pub resolve: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub resolve_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub skin: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
}
pub(crate) struct RtAccelData {
book: AccelBook<Structure, MTLAccelerationStructureInstanceDescriptor>,
pub tlas: Structure,
pub geom_table: Retained<ProtocolObject<dyn MTLBuffer>>,
scratch: Retained<ProtocolObject<dyn MTLBuffer>>,
instance_buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
pub deformed_verts: Retained<ProtocolObject<dyn MTLBuffer>>,
pub skinned_indices: Retained<ProtocolObject<dyn MTLBuffer>>,
retire_pool: RetirePool<RetiredRt>,
ring: RtFrameRing,
deformed_dummy: Retained<ProtocolObject<dyn MTLBuffer>>,
identity_palette: Retained<ProtocolObject<dyn MTLBuffer>>,
head_generation: u64,
ring_published: bool,
ring_liveness: SlotLiveness,
}
struct RetiredRt {
#[expect(
dead_code,
reason = "held so the acceleration structures stay valid until RetirePool drops them"
)]
structures: Vec<Retained<ProtocolObject<dyn MTLAccelerationStructure>>>,
#[expect(
dead_code,
reason = "held so the backing buffers stay valid until RetirePool drops them"
)]
buffers: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
}
pub(crate) struct SkinnedRtInputs<'a> {
pub objects: &'a [SkinnedDrawObject],
pub vertex_buffer: &'a Retained<ProtocolObject<dyn MTLBuffer>>,
pub index_buffer: &'a Retained<ProtocolObject<dyn MTLBuffer>>,
pub joint_matrices: &'a [Vec<[[f32; 4]; 4]>],
pub skin_pipeline: &'a ProtocolObject<dyn MTLComputePipelineState>,
}
#[derive(Clone, Copy)]
pub(crate) struct RtGpu<'a> {
pub device: &'a ProtocolObject<dyn objc2_metal::MTLDevice>,
pub command_queue: &'a ProtocolObject<dyn objc2_metal::MTLCommandQueue>,
pub frames_in_flight: usize,
}
#[derive(Clone, Copy)]
pub(crate) struct RtFrame {
pub id: u64,
pub ring_slot: usize,
}
#[derive(Clone, Copy)]
pub(crate) struct RtStaticGeometry<'a> {
pub vertex_buffer: &'a ProtocolObject<dyn MTLBuffer>,
pub index_buffer: &'a ProtocolObject<dyn MTLBuffer>,
}
#[derive(Clone, Copy)]
pub(crate) struct RtSceneGeometry<'a> {
pub draw_objects: &'a [DrawObject],
pub clusters: &'a [InstancedCluster],
}
#[derive(Clone, Copy)]
pub(crate) struct RtTextureCounts {
pub albedo_count: usize,
}
#[derive(Clone, Copy)]
pub(crate) struct RefreshShape {
pub skinned_follows: bool,
pub skinned_present: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct RtTopologyRefreshOptions {
pub exclude_seethrough: bool,
pub mode: RefreshMode,
pub shape: RefreshShape,
pub frame_id: u64,
}
pub(crate) fn raytracing_supported(device: &ProtocolObject<dyn objc2_metal::MTLDevice>) -> bool {
device.supportsRaytracing()
}
pub(crate) fn pack_instance_transform(model: [[f32; 4]; 4]) -> MTLPackedFloat4x3 {
let col = |c: usize| MTLPackedFloat3 {
x: model[c][0],
y: model[c][1],
z: model[c][2],
};
MTLPackedFloat4x3 {
columns: [col(0), col(1), col(2), col(3)],
}
}
fn prim_desc_for(
vertex_buffer: &ProtocolObject<dyn MTLBuffer>,
index_buffer: &ProtocolObject<dyn MTLBuffer>,
base_vertex: usize,
index_offset: usize,
index_count: usize,
index_type: MTLIndexType,
usage: MTLAccelerationStructureUsage,
) -> Retained<MTLPrimitiveAccelerationStructureDescriptor> {
let index_bytes = match index_type {
MTLIndexType::UInt16 => 2,
_ => 4,
};
let geo = unsafe {
let g = MTLAccelerationStructureTriangleGeometryDescriptor::descriptor();
g.setVertexBuffer(Some(vertex_buffer));
g.setVertexBufferOffset(base_vertex * VERTEX_STRIDE);
g.setVertexStride(VERTEX_STRIDE);
g.setVertexFormat(MTLAttributeFormat::Float3);
g.setIndexBuffer(Some(index_buffer));
g.setIndexBufferOffset(index_offset * index_bytes);
g.setIndexType(index_type);
g.setTriangleCount(index_count / 3);
g
};
let geo_ref: &MTLAccelerationStructureGeometryDescriptor = &geo;
let geos = NSArray::from_slice(&[geo_ref]);
let prim = MTLPrimitiveAccelerationStructureDescriptor::descriptor();
prim.setGeometryDescriptors(Some(&geos));
prim.setUsage(usage);
prim
}
fn instance_desc_at(
model: [[f32; 4]; 4],
blas_index: u32,
) -> MTLAccelerationStructureInstanceDescriptor {
MTLAccelerationStructureInstanceDescriptor {
transformationMatrix: pack_instance_transform(model),
options: MTLAccelerationStructureInstanceOptions::Opaque,
mask: 0xFF,
intersectionFunctionTableOffset: 0,
accelerationStructureIndex: blas_index,
}
}
fn book_instance(
model: [[f32; 4]; 4],
blas: InstanceBlas<'_, Structure>,
static_blas_count: usize,
) -> MTLAccelerationStructureInstanceDescriptor {
let index = match blas {
InstanceBlas::Head { index, .. } | InstanceBlas::Fresh { index } => index,
InstanceBlas::Skinned { n } => static_blas_count + n,
};
instance_desc_at(model, index as u32)
}
fn make_tlas_desc_from_refs(
blas_refs: &[&ProtocolObject<dyn MTLAccelerationStructure>],
instance_buffer: &ProtocolObject<dyn MTLBuffer>,
instance_count: usize,
) -> Retained<MTLInstanceAccelerationStructureDescriptor> {
let blas_array = NSArray::from_slice(blas_refs);
let desc = MTLInstanceAccelerationStructureDescriptor::descriptor();
desc.setInstancedAccelerationStructures(Some(&blas_array));
desc.setInstanceCount(instance_count);
desc.setInstanceDescriptorBuffer(Some(instance_buffer));
desc.setInstanceDescriptorType(MTLAccelerationStructureInstanceDescriptorType::Default);
desc
}
fn make_tlas_desc(
blas: &[Retained<ProtocolObject<dyn MTLAccelerationStructure>>],
instance_buffer: &ProtocolObject<dyn MTLBuffer>,
instance_count: usize,
) -> Retained<MTLInstanceAccelerationStructureDescriptor> {
let blas_refs: Vec<&ProtocolObject<dyn MTLAccelerationStructure>> =
blas.iter().map(|b| b.as_ref()).collect();
make_tlas_desc_from_refs(&blas_refs, instance_buffer, instance_count)
}
fn declare_blas_resident<'a>(
enc: &ProtocolObject<dyn MTLAccelerationStructureCommandEncoder>,
blas: impl IntoIterator<Item = &'a Retained<ProtocolObject<dyn MTLAccelerationStructure>>>,
) {
for b in blas {
enc.useResource_usage(ProtocolObject::from_ref(&**b), MTLResourceUsage::Read);
}
}
pub(in crate::metal) fn use_blas_resident_fragment<'a>(
enc: &ProtocolObject<dyn MTLRenderCommandEncoder>,
blas: impl IntoIterator<Item = &'a Structure>,
) {
let res: Vec<NonNull<ProtocolObject<dyn MTLResource>>> = blas
.into_iter()
.map(|b| NonNull::from(ProtocolObject::from_ref(&**b)))
.collect();
if res.is_empty() {
return;
}
unsafe {
enc.useResources_count_usage_stages(
NonNull::new(res.as_ptr() as *mut NonNull<ProtocolObject<dyn MTLResource>>)
.expect("non-empty blas slice has a non-null pointer"),
res.len(),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
}
}
const IDENTITY4: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
fn dispatch_skin(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
command_queue: &ProtocolObject<dyn objc2_metal::MTLCommandQueue>,
skinned: &SkinnedRtInputs,
skinned_objects: &[usize],
deformed_verts: &ProtocolObject<dyn MTLBuffer>,
) -> RenderResult<()> {
let skin_cmd = command_queue
.commandBuffer()
.ok_or_else(|| RenderError::Other("failed to create RT skin command buffer".into()))?;
let cenc = skin_cmd
.computeCommandEncoder()
.ok_or_else(|| RenderError::Other("failed to create RT skin compute encoder".into()))?;
let palette_bufs = encode_skin_dispatch(
&cenc,
skinned,
skinned_objects,
deformed_verts,
SkinPalettes::Upload(device),
)?;
cenc.endEncoding();
skin_cmd.commit();
skin_cmd.waitUntilCompleted();
drop(palette_bufs);
check_build_status(&skin_cmd, "skinning compute")
}
enum SkinPalettes<'a> {
Upload(&'a ProtocolObject<dyn objc2_metal::MTLDevice>),
Prebuilt {
buffers: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
identity: &'a Retained<ProtocolObject<dyn MTLBuffer>>,
},
}
fn encode_skin_dispatch(
cenc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>,
skinned: &SkinnedRtInputs,
skinned_objects: &[usize],
deformed_verts: &ProtocolObject<dyn MTLBuffer>,
palettes: SkinPalettes,
) -> RenderResult<Vec<Retained<ProtocolObject<dyn MTLBuffer>>>> {
cenc.set_pipeline(skinned.skin_pipeline);
let threadgroup = skinned
.skin_pipeline
.maxTotalThreadsPerThreadgroup()
.clamp(1, 64);
let mut uploaded: Vec<Retained<ProtocolObject<dyn MTLBuffer>>> = Vec::new();
for &obj_idx in skinned_objects {
let obj = &skinned.objects[obj_idx];
let matrices: &[[[f32; 4]; 4]] = skinned
.joint_matrices
.get(obj_idx)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let joint_count = matrices.len().max(1);
let palette = match &palettes {
SkinPalettes::Upload(device) => {
let slice = if matrices.is_empty() {
std::slice::from_ref(&IDENTITY4)
} else {
matrices
};
let buf = upload_buffer(device, slice, "RT skin palette")?;
uploaded.push(buf.clone());
buf
}
SkinPalettes::Prebuilt { buffers, identity } => match buffers.get(obj_idx) {
Some(buf) if !matrices.is_empty() => buf.clone(),
_ => (*identity).clone(),
},
};
encode_skin_object(
cenc,
SkinDispatchBuffers {
vertex: skinned.vertex_buffer.as_ref(),
deformed: deformed_verts,
palette: palette.as_ref(),
},
obj,
joint_count,
threadgroup,
);
}
Ok(uploaded)
}
#[derive(Clone, Copy)]
struct SkinDispatchBuffers<'a> {
vertex: &'a ProtocolObject<dyn MTLBuffer>,
deformed: &'a ProtocolObject<dyn MTLBuffer>,
palette: &'a ProtocolObject<dyn MTLBuffer>,
}
fn encode_skin_object(
cenc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>,
bufs: SkinDispatchBuffers,
obj: &SkinnedDrawObject,
joint_count: usize,
threadgroup: usize,
) {
let params = SkinParams {
vertex_base: obj.vertex_base,
vertex_count: obj.vertex_count as u32,
joint_count: joint_count as u32,
target_count: 0,
};
cenc.set_buffer(bufs.vertex, 0, 0);
cenc.set_buffer(bufs.deformed, 0, 1);
cenc.set_buffer(bufs.palette, 0, 2);
cenc.set_value(¶ms, 3);
cenc.set_buffer(bufs.vertex, 0, 4);
cenc.set_buffer(bufs.vertex, 0, 5);
cenc.dispatchThreads_threadsPerThreadgroup(
MTLSize {
width: obj.vertex_count.max(1),
height: 1,
depth: 1,
},
MTLSize {
width: threadgroup,
height: 1,
depth: 1,
},
);
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct MainSkinBuffers<'a> {
pub joints: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
pub morph_weights: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
}
impl crate::metal::context::MtlContext {
pub(in crate::metal) fn encode_main_skin(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
deformed: &ProtocolObject<dyn MTLBuffer>,
bufs: MainSkinBuffers<'_>,
) -> RenderResult<()> {
let MainSkinBuffers {
joints: joint_bufs,
morph_weights: weight_bufs,
} = bufs;
let (Some(skin_pipeline), Some(svb)) = (
self.skinned.skin_pipeline.as_ref(),
self.skinned.vertex_buffer.as_ref(),
) else {
return Ok(());
};
if self.state.skinned.draw_objects.is_empty() {
return Ok(());
}
let cenc = cmd_buf.computeCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create main-skin compute encoder".into())
})?;
cenc.set_pipeline(skin_pipeline);
let tg = skin_pipeline.maxTotalThreadsPerThreadgroup().clamp(1, 64);
for (i, obj) in self.state.skinned.draw_objects.iter().enumerate() {
let Some(joint_buf) = joint_bufs.get(i) else {
continue;
};
let joint_count = self
.state
.skinned
.joint_matrices
.get(i)
.map(|m| m.len().max(1))
.unwrap_or(1);
let morph = self.skinned.morphs.get(i).and_then(|m| m.as_ref());
let weights = weight_bufs.get(i);
let params = SkinParams {
vertex_base: obj.vertex_base,
vertex_count: obj.vertex_count as u32,
joint_count: joint_count as u32,
target_count: match (morph, weights) {
(Some(m), Some(_)) => m.target_count,
_ => 0,
},
};
cenc.set_buffer(svb.as_ref(), 0, 0);
cenc.set_buffer(deformed, 0, 1);
cenc.set_buffer(joint_buf.as_ref(), 0, 2);
cenc.set_value(¶ms, 3);
cenc.set_buffer(morph.map_or(svb.as_ref(), |m| m.buffer.as_ref()), 0, 4);
cenc.set_buffer(weights.map_or(svb.as_ref(), |b| b.as_ref()), 0, 5);
cenc.dispatchThreads_threadsPerThreadgroup(
MTLSize {
width: obj.vertex_count.max(1),
height: 1,
depth: 1,
},
MTLSize {
width: tg,
height: 1,
depth: 1,
},
);
}
cenc.endEncoding();
Ok(())
}
}
pub(crate) fn build_rt_accel(
gpu: RtGpu,
static_geometry: RtStaticGeometry,
scene: RtSceneGeometry,
texture_counts: RtTextureCounts,
skinned: Option<SkinnedRtInputs>,
exclude_seethrough: bool,
) -> RenderResult<Option<RtAccelData>> {
let RtGpu {
device,
command_queue,
frames_in_flight,
} = gpu;
let RtStaticGeometry {
vertex_buffer,
index_buffer,
} = static_geometry;
let RtSceneGeometry {
draw_objects,
clusters,
} = scene;
let RtTextureCounts { albedo_count } = texture_counts;
let seed = SeedSet::new(draw_objects, clusters, exclude_seethrough);
let skinned_list: &[SkinnedDrawObject] = skinned.as_ref().map_or(&[], |s| s.objects);
if seed.builds_nothing(skinned.is_some()) {
return Ok(None);
}
let mut prim_descs: Vec<Retained<MTLPrimitiveAccelerationStructureDescriptor>> = seed
.objects
.iter()
.map(|&i| {
let obj = &draw_objects[i];
prim_desc_for(
vertex_buffer,
index_buffer,
obj.base_vertex as usize,
obj.index_offset,
obj.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::None,
)
})
.chain(seed.clusters.iter().map(|c| {
prim_desc_for(
vertex_buffer,
index_buffer,
0,
c.index_offset,
c.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::None,
)
}))
.collect();
let mut max_scratch: usize = 0;
let mut allocate = |prim: &MTLPrimitiveAccelerationStructureDescriptor| {
let sizes = device.accelerationStructureSizesWithDescriptor(prim);
max_scratch = max_scratch.max(sizes.buildScratchBufferSize);
device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("BLAS"))
};
let head = prim_descs
.iter()
.map(|prim| allocate(prim))
.collect::<RenderResult<Vec<Structure>>>()?;
let albedo_count = albedo_count as u32;
let mut book = AccelBook::new(&seed, head, draw_objects, albedo_count)?;
book.select_skinned(Some(skinned_list));
let skinned_objects = book.visible_skinned().to_vec();
let deformed_bytes =
(book.skinned_vertex_extent(skinned_list) as usize * VERTEX_STRIDE).max(VERTEX_STRIDE);
let deformed_dummy = device
.newBufferWithLength_options(VERTEX_STRIDE, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("RT deformed-vertex dummy buffer"))?;
let deformed_verts = if skinned_objects.is_empty() {
deformed_dummy.clone()
} else {
device
.newBufferWithLength_options(deformed_bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("RT deformed-vertex buffer"))?
};
let skinned_indices: Retained<ProtocolObject<dyn MTLBuffer>> = match &skinned {
Some(s) if !skinned_objects.is_empty() => s.index_buffer.clone(),
_ => device
.newBufferWithLength_options(
std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| allocation_failed("RT skinned-index dummy buffer"))?,
};
let skinned_descs: Vec<_> = skinned_objects
.iter()
.map(|&i| {
let obj = &skinned_list[i];
prim_desc_for(
deformed_verts.as_ref(),
skinned_indices.as_ref(),
0,
obj.index_offset,
obj.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::Refit,
)
})
.collect();
let tail = skinned_descs
.iter()
.map(|prim| allocate(prim))
.collect::<RenderResult<Vec<Structure>>>()?;
book.replace_tail(tail);
prim_descs.extend(skinned_descs);
let static_blas_count = book.static_blas_count();
book.fill_instances(draw_objects, Some(skinned_list), |model, _, blas| {
book_instance(model, blas, static_blas_count)
});
let instance_buffer = upload_buffer(device, book.instances(), "RT instance descriptors")?;
let geom_table = upload_buffer(device, book.geom_table(), "RT geometry table")?;
let tlas_desc = make_tlas_desc(book.blas(), &instance_buffer, book.instances().len());
let tlas_sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
let tlas = device
.newAccelerationStructureWithSize(tlas_sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("TLAS"))?;
max_scratch = max_scratch.max(tlas_sizes.buildScratchBufferSize);
let scratch = device
.newBufferWithLength_options(max_scratch.max(1), MTLResourceOptions::StorageModePrivate)
.ok_or_else(|| allocation_failed("RT scratch buffer"))?;
if let Some(s) = &skinned
&& !skinned_objects.is_empty()
{
dispatch_skin(
device,
command_queue,
s,
&skinned_objects,
deformed_verts.as_ref(),
)?;
}
let cmd = command_queue
.commandBuffer()
.ok_or_else(|| RenderError::Other("failed to create RT build command buffer".into()))?;
for (acc, prim) in book.blas().iter().zip(prim_descs.iter()) {
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
acc, prim, &scratch, 0,
);
enc.endEncoding();
}
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
declare_blas_resident(&enc, book.blas());
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
&tlas, &tlas_desc, &scratch, 0,
);
enc.endEncoding();
cmd.commit();
cmd.waitUntilCompleted();
check_build_status(&cmd, "acceleration-structure build")?;
let identity_palette = upload_buffer(device, &[IDENTITY4], "RT identity palette")?;
Ok(Some(RtAccelData {
book,
tlas,
geom_table,
scratch,
instance_buffer,
deformed_verts,
skinned_indices,
retire_pool: RetirePool::new(),
ring: RtFrameRing::new(frames_in_flight),
deformed_dummy,
identity_palette,
head_generation: 0,
ring_published: false,
ring_liveness: SlotLiveness::new(frames_in_flight),
}))
}
fn allocate_skinned_blas(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
deformed_verts: &ProtocolObject<dyn MTLBuffer>,
skinned_indices: &ProtocolObject<dyn MTLBuffer>,
shapes: &[SkinnedShape],
) -> RenderResult<SkinnedBlasSet> {
let mut blas = Vec::with_capacity(shapes.len());
let mut descs = Vec::with_capacity(shapes.len());
let mut scratch_bytes = 0usize;
for shape in shapes {
let prim = prim_desc_for(
deformed_verts,
skinned_indices,
0,
shape.index_offset,
shape.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::Refit,
);
let sizes = device.accelerationStructureSizesWithDescriptor(&prim);
let acc = device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("skinned BLAS"))?;
acc.setLabel(Some(&crate::metal::pipeline::ns_str("rt_skinned_blas")));
scratch_bytes = scratch_bytes.max(sizes.buildScratchBufferSize);
blas.push(acc);
descs.push(prim);
}
Ok(SkinnedBlasSet {
blas,
descs,
scratch_bytes,
})
}
fn slot_scratch_bytes(blas_scratch: usize, tlas_scratch: usize) -> usize {
blas_scratch.max(tlas_scratch).max(1)
}
impl RtAccelData {
pub(crate) fn rebuild_tlas(
&mut self,
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
command_queue: &ProtocolObject<dyn objc2_metal::MTLCommandQueue>,
draw_objects: &[DrawObject],
frame_id: u64,
) -> RenderResult<()> {
let static_blas_count = self.book.static_blas_count();
self.book
.fill_instances(draw_objects, None, |model, _, blas| {
book_instance(model, blas, static_blas_count)
});
let instance_count = self.book.instances().len();
let instance_buffer =
upload_buffer(device, self.book.instances(), "RT instance descriptors")?;
let geom_table = upload_buffer(device, self.book.geom_table(), "RT geometry table")?;
let tlas_desc = make_tlas_desc(self.book.head(), &instance_buffer, instance_count);
let sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
let tlas = device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("TLAS"))?;
if (sizes.buildScratchBufferSize as u64) > self.scratch.length() as u64 {
self.scratch = device
.newBufferWithLength_options(
sizes.buildScratchBufferSize.max(1),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| allocation_failed("grown RT scratch buffer"))?;
}
let cmd = command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("failed to create RT rebuild command buffer".into())
})?;
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
declare_blas_resident(&enc, self.book.head());
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
&tlas,
&tlas_desc,
&self.scratch,
0,
);
enc.endEncoding();
cmd.commit();
cmd.waitUntilCompleted();
check_build_status(&cmd, "TLAS rebuild")?;
self.tlas = tlas;
self.geom_table = geom_table;
if let Some(tail) = self.book.commit_static() {
self.release_skinned(tail, frame_id);
}
self.retire_parked(frame_id);
self.ring_published = false;
Ok(())
}
pub(crate) fn is_empty(&self) -> bool {
self.book.is_empty()
}
pub(crate) fn is_spent(&self, skinned_present: bool) -> bool {
self.book.is_spent(skinned_present)
}
pub(crate) fn release_skinned_tail(&mut self, frame_id: u64) {
if let Some(tail) = self.book.release_skinned() {
self.release_skinned(tail, frame_id);
}
}
pub(crate) fn blas(&self) -> &[Structure] {
self.book.blas()
}
pub(crate) fn traced_blas(&self) -> impl Iterator<Item = &Structure> {
self.book.traced_blas()
}
pub(crate) fn set_albedo_count(&mut self, albedo_count: usize) {
self.book.set_albedo_count(albedo_count as u32);
}
pub(crate) fn book_mut(
&mut self,
) -> &mut AccelBook<Structure, MTLAccelerationStructureInstanceDescriptor> {
&mut self.book
}
pub(crate) fn refresh_static_topology(
&mut self,
gpu: RtGpu,
static_geometry: RtStaticGeometry,
draw_objects: &[DrawObject],
options: RtTopologyRefreshOptions,
) -> RenderResult<()> {
let RtGpu {
device,
command_queue,
..
} = gpu;
let RtStaticGeometry {
vertex_buffer,
index_buffer,
} = static_geometry;
let RtTopologyRefreshOptions {
exclude_seethrough,
mode,
shape:
RefreshShape {
skinned_follows,
skinned_present,
},
frame_id,
} = options;
let refresh = self
.book
.plan_refresh(draw_objects, exclude_seethrough, mode);
let leaves_nothing = self.book.refresh_leaves_nothing(&refresh);
let mut fresh: Vec<Option<Structure>> =
(0..refresh.indices().len()).map(|_| None).collect();
let mut build_jobs: Vec<(usize, Retained<MTLPrimitiveAccelerationStructureDescriptor>)> =
Vec::new();
let mut max_scratch: usize = 0;
for (j, idx) in refresh.fresh_slots() {
let obj = &draw_objects[idx];
let prim = prim_desc_for(
vertex_buffer,
index_buffer,
obj.base_vertex as usize,
obj.index_offset,
obj.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::None,
);
let sizes = device.accelerationStructureSizesWithDescriptor(&prim);
let acc = device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("topology-refresh BLAS"))?;
acc.setLabel(Some(&crate::metal::pipeline::ns_str("rt_topology_blas")));
max_scratch = max_scratch.max(sizes.buildScratchBufferSize);
fresh[j] = Some(acc);
build_jobs.push((j, prim));
}
let empty = leaves_nothing.then(|| empty_head(skinned_follows, skinned_present));
let build_tlas = !skinned_follows && empty != Some(EmptyHead::Drop);
let tlas_build = if build_tlas {
self.book
.fill_refresh_instances(&refresh, draw_objects, |model, _, blas| {
book_instance(model, blas, 0)
});
let instance_count = self.book.instances().len();
let instance_buffer =
upload_buffer(device, self.book.instances(), "RT instance descriptors")?;
let geom_table = upload_buffer(device, self.book.geom_table(), "RT geometry table")?;
let head: Vec<&ProtocolObject<dyn MTLAccelerationStructure>> = self
.book
.refreshed_head(&refresh, &fresh)
.into_iter()
.map(|b| b.as_ref())
.collect();
let tlas_desc = make_tlas_desc_from_refs(&head, &instance_buffer, instance_count);
let tlas_sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
max_scratch = max_scratch.max(tlas_sizes.buildScratchBufferSize);
let tlas = device
.newAccelerationStructureWithSize(tlas_sizes.accelerationStructureSize)
.ok_or_else(|| allocation_failed("TLAS"))?;
tlas.setLabel(Some(&crate::metal::pipeline::ns_str("rt_tlas")));
Some((tlas, tlas_desc, instance_buffer, geom_table))
} else {
None
};
self.book.check_refresh(&refresh, &fresh)?;
let mut retire_buffers: Vec<Retained<ProtocolObject<dyn MTLBuffer>>> = Vec::new();
if !build_jobs.is_empty() || tlas_build.is_some() {
let scratch = device
.newBufferWithLength_options(
max_scratch.max(1),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| allocation_failed("topology-refresh scratch buffer"))?;
let cmd = command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("failed to create topology-refresh command buffer".into())
})?;
cmd.setLabel(Some(&crate::metal::pipeline::ns_str("rt_topology_build")));
for (j, prim) in &build_jobs {
let Some(acc) = fresh[*j].as_ref() else {
continue;
};
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
acc, prim, &scratch, 0,
);
enc.endEncoding();
}
if let Some((tlas, tlas_desc, _, _)) = &tlas_build {
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
declare_blas_resident(&enc, self.book.refreshed_head(&refresh, &fresh));
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
tlas, tlas_desc, &scratch, 0,
);
enc.endEncoding();
}
super::fault_log::attach_fault_logger(&cmd, "RT topology build");
cmd.commit();
retire_buffers.push(scratch);
}
let orphans = self.book.commit_refresh(refresh, fresh, draw_objects);
self.head_generation = self.head_generation.wrapping_add(1);
let mut retire_structures = Vec::new();
if skinned_follows {
self.book.park(orphans);
} else {
retire_structures.extend(orphans);
}
if empty == Some(EmptyHead::Drop) {
self.release_skinned_tail(frame_id);
}
if let Some((tlas, _, instance_buffer, geom_table)) = tlas_build {
retire_structures.extend(self.book.take_parked());
retire_structures.push(std::mem::replace(&mut self.tlas, tlas));
retire_buffers.push(std::mem::replace(&mut self.geom_table, geom_table));
retire_buffers.push(std::mem::replace(
&mut self.instance_buffer,
instance_buffer,
));
if let Some(tail) = self.book.release_skinned() {
self.release_skinned(tail, frame_id);
}
self.ring_published = false;
}
if !retire_structures.is_empty() || !retire_buffers.is_empty() {
self.retire_pool.push(
frame_id,
RetiredRt {
structures: retire_structures,
buffers: retire_buffers,
},
);
}
Ok(())
}
pub(crate) fn rebuild_skinned(
&mut self,
gpu: RtGpu,
draw_objects: &[DrawObject],
skinned: SkinnedRtInputs,
joint_buffers: &[Retained<ProtocolObject<dyn MTLBuffer>>],
frame: RtFrame,
full_build: bool,
) -> RenderResult<RtUpdate> {
let RtGpu {
device,
command_queue,
..
} = gpu;
if !self.ring_liveness.writable(frame.ring_slot, frame.id) {
return Ok(RtUpdate::Skipped);
}
let deformed_bytes = (self.book.skinned_vertex_extent(skinned.objects) as usize
* VERTEX_STRIDE)
.max(VERTEX_STRIDE);
self.book.fill_skinned_shapes(skinned.objects, 0);
let static_blas_count = self.book.static_blas_count();
let head_generation = self.head_generation;
self.book
.fill_instances(draw_objects, Some(skinned.objects), |model, _, blas| {
book_instance(model, blas, static_blas_count)
});
let book = &self.book;
let skinned_objects = book.visible_skinned();
let shapes = book.skinned_shapes();
let instances = book.instances();
let geom = book.geom_table();
let skinned_indices = skinned.index_buffer.clone();
let slot = self.ring.slot(frame.ring_slot);
let (deformed_verts, deformed_fresh) = slot.deformed(device, deformed_bytes)?;
let shape_changed = deformed_fresh || !slot.refit.matches(shapes);
if shape_changed {
slot.set_skinned(allocate_skinned_blas(
device,
deformed_verts.as_ref(),
skinned_indices.as_ref(),
shapes,
)?);
}
let instance_buffer = slot.instances(device, std::mem::size_of_val(instances))?;
let geom_table = slot.geom_table(device, std::mem::size_of_val(geom))?;
write_buffer_slice(&instance_buffer, instances)?;
write_buffer_slice(&geom_table, geom)?;
let key = TlasKey {
head_generation,
slot_generation: slot.generation(),
instance_count: instances.len(),
};
let cached = slot.tlas_desc(key);
let tlas_desc = match cached {
Some(desc) => desc,
None => {
let refs: Vec<&ProtocolObject<dyn MTLAccelerationStructure>> = book
.head()
.iter()
.map(|b| b.as_ref())
.chain(slot.skinned_blas().iter().map(|b| b.as_ref()))
.collect();
let desc = make_tlas_desc_from_refs(&refs, &instance_buffer, instances.len());
slot.set_tlas_desc(key, desc.clone());
desc
}
};
let tlas_sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
let tlas = slot.tlas(device, tlas_sizes.accelerationStructureSize)?;
let scratch_buffer = slot.scratch(
device,
slot_scratch_bytes(slot.blas_scratch(), tlas_sizes.buildScratchBufferSize),
)?;
{
let skin_cmd = command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("failed to create RT skin command buffer".into())
})?;
skin_cmd.setLabel(Some(&crate::metal::pipeline::ns_str("rt_skin")));
let cenc = skin_cmd.computeCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create RT skin compute encoder".into())
})?;
encode_skin_dispatch(
&cenc,
&skinned,
skinned_objects,
deformed_verts.as_ref(),
SkinPalettes::Prebuilt {
buffers: joint_buffers,
identity: &self.identity_palette,
},
)?;
cenc.endEncoding();
super::fault_log::attach_fault_logger(&skin_cmd, "RT skinning compute");
skin_cmd.commit();
}
let update = slot.refit.plan(shapes, shape_changed || full_build);
{
let cmd = command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("failed to create RT skinned rebuild command buffer".into())
})?;
cmd.setLabel(Some(&crate::metal::pipeline::ns_str("rt_build")));
for (acc, prim) in slot.skinned_blas().iter().zip(slot.skinned_descs()) {
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
match update {
BlasUpdate::Build => {
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
acc,
prim,
&scratch_buffer,
0,
);
}
BlasUpdate::Refit => unsafe {
enc.refitAccelerationStructure_descriptor_destination_scratchBuffer_scratchBufferOffset(
acc,
prim,
None,
Some(&scratch_buffer),
0,
);
},
}
enc.endEncoding();
}
let enc = cmd.accelerationStructureCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to create acceleration-structure encoder".into())
})?;
declare_blas_resident(&enc, book.head().iter().chain(slot.skinned_blas().iter()));
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
&tlas,
&tlas_desc,
&scratch_buffer,
0,
);
enc.endEncoding();
super::fault_log::attach_fault_logger(&cmd, "RT skinned BLAS + TLAS build");
cmd.commit();
}
let takeover = !self.ring_published;
let old_skinned = self.book.replace_tail(slot.skinned_blas().iter().cloned());
let old_tlas = std::mem::replace(&mut self.tlas, tlas);
let old_geom_table = std::mem::replace(&mut self.geom_table, geom_table);
let old_deformed = std::mem::replace(&mut self.deformed_verts, deformed_verts);
if takeover {
let mut structures = old_skinned;
structures.push(old_tlas);
self.retire_pool.push(
frame.id,
RetiredRt {
structures,
buffers: vec![old_geom_table, old_deformed],
},
);
}
self.skinned_indices = skinned_indices;
self.book.commit_skinned();
self.ring_published = true;
self.ring_liveness.publish(frame.ring_slot, frame.id);
self.retire_parked(frame.id);
Ok(RtUpdate::Done)
}
fn retire_parked(&mut self, frame_id: u64) {
let structures = self.book.take_parked();
if !structures.is_empty() {
self.retire_pool.push(
frame_id,
RetiredRt {
structures,
buffers: Vec::new(),
},
);
}
}
fn release_skinned(&mut self, tail: Vec<Structure>, frame_id: u64) {
if !tail.is_empty() {
self.retire_pool.push(
frame_id,
RetiredRt {
structures: tail,
buffers: Vec::new(),
},
);
}
self.deformed_verts = self.deformed_dummy.clone();
self.ring.release_all();
self.ring_liveness.unpublish(frame_id);
}
pub(crate) fn retire_completed(&mut self, frame_id: u64, depth: usize) {
self.retire_pool.collect(frame_id, depth as u64);
}
}
pub(crate) fn build_rt_skin_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>> {
compute_pipeline(device, &super::builtin_shaders::RT_SKIN, hot_reload)
}
fn check_build_status(
cmd: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
what: &str,
) -> RenderResult<()> {
completed_command_buffer(cmd, format_args!("RT {what}"))
}
fn upload_buffer<T: Copy>(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
data: &[T],
what: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let bytes = std::mem::size_of_val(data);
if bytes == 0 {
return device
.newBufferWithLength_options(
std::mem::size_of::<T>().max(1),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| allocation_failed(format_args!("buffer for {what}")));
}
let ptr = std::ptr::NonNull::new(data.as_ptr() as *mut std::ffi::c_void)
.ok_or_else(|| RenderError::Other(format!("{what}: null data pointer")))?;
unsafe {
device.newBufferWithBytes_length_options(ptr, bytes, MTLResourceOptions::StorageModeShared)
}
.ok_or_else(|| allocation_failed(format_args!("buffer for {what}")))
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::gfx::render_types;
#[test]
fn pack_instance_transform_drops_affine_row_and_keeps_columns() {
let model = [
[2.0, 0.0, 0.0, 0.0], [0.0, 3.0, 0.0, 0.0], [0.0, 0.0, 4.0, 0.0], [5.0, 6.0, 7.0, 1.0], ];
let p = pack_instance_transform(model);
assert_eq!(
(p.columns[0].x, p.columns[0].y, p.columns[0].z),
(2.0, 0.0, 0.0)
);
assert_eq!(
(p.columns[1].x, p.columns[1].y, p.columns[1].z),
(0.0, 3.0, 0.0)
);
assert_eq!(
(p.columns[2].x, p.columns[2].y, p.columns[2].z),
(0.0, 0.0, 4.0)
);
assert_eq!(
(p.columns[3].x, p.columns[3].y, p.columns[3].z),
(5.0, 6.0, 7.0)
);
}
#[test]
fn rt_geom_entry_is_128_bytes() {
assert_eq!(std::mem::size_of::<render_types::RtGeomEntry>(), 128);
}
#[test]
fn slot_scratch_covers_the_largest_build_and_never_reaches_zero() {
assert_eq!(slot_scratch_bytes(4096, 1024), 4096);
assert_eq!(slot_scratch_bytes(1024, 4096), 4096);
assert_eq!(slot_scratch_bytes(0, 0), 1);
}
}