#![deny(unsafe_op_in_unsafe_fn)]
use std::ptr::NonNull;
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 _, MTLCommandBufferStatus,
MTLCommandEncoder as _, MTLCommandQueue as _, MTLComputeCommandEncoder as _,
MTLComputePipelineState, MTLDevice as _, MTLIndexType,
MTLInstanceAccelerationStructureDescriptor, MTLPackedFloat3, MTLPackedFloat4x3,
MTLPrimitiveAccelerationStructureDescriptor, MTLRenderCommandEncoder, MTLRenderPipelineState,
MTLRenderStages, MTLResource, MTLResourceOptions, MTLResourceUsage, MTLSize,
};
use super::context::write_buffer_slice;
use super::encode::ComputeEncode;
use super::rt_ring::{BlasUpdate, RtFrameRing, SkinnedBlasSet, SkinnedShape, TlasKey};
use super::transient::RetirePool;
use crate::gfx::render_types::{DrawObject, InstancedCluster, RtGeomEntry, SkinnedDrawObject};
use crate::gfx::rt_geom::{cluster_geom_entry, geom_entry, skinned_geom_entry};
use crate::gfx::rt_reflections::RtReflectionSettings;
pub(crate) use crate::gfx::rt_geom::RtDynamicMode;
use concinnity_render::uniforms::SkinParams;
pub(in crate::metal) const VERTEX_STRIDE: usize = 56;
pub(crate) struct RtState {
pub settings: Option<RtReflectionSettings>,
pub accel: Option<RtAccelData>,
pub dynamic_mode: RtDynamicMode,
pub skinned_geometry: bool,
pub update_failed: bool,
pub topology_dirty: bool,
pub pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub pipeline_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub skin_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct GeomSig {
base_vertex: i32,
vertex_offset: usize,
index_offset: usize,
index_count: usize,
generation: u32,
}
impl GeomSig {
fn of(obj: &DrawObject) -> Self {
Self {
base_vertex: obj.base_vertex,
vertex_offset: obj.vertex_offset,
index_offset: obj.index_offset,
index_count: obj.index_count,
generation: obj.geometry_generation,
}
}
}
struct TopologyPlan {
reuse: Vec<Option<usize>>,
retire: Vec<usize>,
}
fn plan_topology_refresh(
old_indices: &[usize],
old_sigs: &[GeomSig],
new_indices: &[usize],
new_sigs: &[GeomSig],
) -> TopologyPlan {
use std::collections::HashMap;
let mut by_idx: HashMap<usize, (usize, GeomSig)> = HashMap::with_capacity(old_indices.len());
for (k, (&idx, &sig)) in old_indices.iter().zip(old_sigs).enumerate() {
by_idx.insert(idx, (k, sig));
}
let mut used = vec![false; old_indices.len()];
let mut reuse = Vec::with_capacity(new_indices.len());
for (&idx, &sig) in new_indices.iter().zip(new_sigs) {
match by_idx.get(&idx) {
Some(&(k, old_sig)) if old_sig == sig && !used[k] => {
used[k] = true;
reuse.push(Some(k));
}
_ => reuse.push(None),
}
}
let retire = used
.iter()
.enumerate()
.filter(|&(_, &u)| !u)
.map(|(k, _)| k)
.collect();
TopologyPlan { reuse, retire }
}
pub(crate) struct RtAccelData {
pub blas: Vec<Retained<ProtocolObject<dyn MTLAccelerationStructure>>>,
static_blas_count: usize,
pub tlas: Retained<ProtocolObject<dyn MTLAccelerationStructure>>,
pub geom_table: Retained<ProtocolObject<dyn MTLBuffer>>,
object_indices: Vec<usize>,
draw_blas_sigs: Vec<GeomSig>,
cached_models: Vec<[[f32; 4]; 4]>,
cluster_instances: Vec<MTLAccelerationStructureInstanceDescriptor>,
cluster_geom: Vec<RtGeomEntry>,
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,
update_scratch: RtUpdateScratch,
}
#[derive(Default)]
struct RtUpdateScratch {
skinned: Vec<usize>,
shapes: Vec<SkinnedShape>,
instances: Vec<MTLAccelerationStructureInstanceDescriptor>,
geom: Vec<RtGeomEntry>,
}
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 RtTopologyRefreshOptions {
pub exclude_seethrough: bool,
pub build_tlas: bool,
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 instance_desc(obj: &DrawObject, i: usize) -> MTLAccelerationStructureInstanceDescriptor {
instance_desc_at(obj.model, i 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(
enc: &ProtocolObject<dyn MTLRenderCommandEncoder>,
blas: &[Retained<ProtocolObject<dyn MTLAccelerationStructure>>],
) {
if blas.is_empty() {
return;
}
let res: Vec<NonNull<ProtocolObject<dyn MTLResource>>> = blas
.iter()
.map(|b| NonNull::from(ProtocolObject::from_ref(&**b)))
.collect();
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,
);
}
}
fn attach_async_fault_logger(
cmd: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
what: &'static str,
) {
static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
let handler = block2::RcBlock::new(
move |cb: NonNull<ProtocolObject<dyn objc2_metal::MTLCommandBuffer>>| {
let cb = unsafe { cb.as_ref() };
if cb.status() == MTLCommandBufferStatus::Error
&& !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed)
{
tracing::error!("RT {what} faulted (async): {:?}", cb.error());
}
},
);
unsafe {
cmd.addCompletedHandler(block2::RcBlock::as_ptr(&handler));
}
}
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>,
) -> Result<(), String> {
let skin_cmd = command_queue
.commandBuffer()
.ok_or("failed to create RT skin command buffer")?;
let cenc = skin_cmd
.computeCommandEncoder()
.ok_or("failed to create RT skin compute encoder")?;
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,
) -> Result<Vec<Retained<ProtocolObject<dyn MTLBuffer>>>, String> {
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<'_>,
) -> Result<(), String> {
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.skinned.draw_objects.is_empty() {
return Ok(());
}
let cenc = cmd_buf
.computeCommandEncoder()
.ok_or("failed to create main-skin compute encoder")?;
cenc.set_pipeline(skin_pipeline);
let tg = skin_pipeline.maxTotalThreadsPerThreadgroup().clamp(1, 64);
for (i, obj) in self.skinned.draw_objects.iter().enumerate() {
let Some(joint_buf) = joint_bufs.get(i) else {
continue;
};
let joint_count = self
.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,
) -> Result<Option<RtAccelData>, String> {
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 object_indices: Vec<usize> = draw_objects
.iter()
.enumerate()
.filter(|(_, o)| {
o.resident && o.index_count >= 3 && !(exclude_seethrough && o.material.see_through != 0)
})
.map(|(i, _)| i)
.collect();
let cluster_list: Vec<&InstancedCluster> = clusters
.iter()
.filter(|c| c.index_count >= 3 && !c.instances.is_empty())
.collect();
let skinned_list: &[SkinnedDrawObject] = skinned.as_ref().map_or(&[], |s| s.objects);
let skinned_objects: Vec<usize> = skinned_list
.iter()
.enumerate()
.filter(|(_, o)| o.visible && o.index_count >= 3)
.map(|(i, _)| i)
.collect();
if object_indices.is_empty() && cluster_list.is_empty() && skinned_objects.is_empty() {
return Ok(None);
}
let objects: Vec<&DrawObject> = object_indices.iter().map(|&i| &draw_objects[i]).collect();
let deformed_extent: usize = skinned_objects
.iter()
.map(|&i| skinned_list[i].vertex_base as usize + skinned_list[i].vertex_count)
.max()
.unwrap_or(0);
let deformed_bytes = (deformed_extent * VERTEX_STRIDE).max(VERTEX_STRIDE);
let deformed_dummy = device
.newBufferWithLength_options(VERTEX_STRIDE, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate 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("failed to allocate 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("failed to allocate RT skinned-index dummy buffer")?,
};
let draw_blas_count = objects.len();
let skinned_blas_base = draw_blas_count + cluster_list.len();
let mut prim_descs: Vec<Retained<MTLPrimitiveAccelerationStructureDescriptor>> =
Vec::with_capacity(skinned_blas_base + skinned_objects.len());
for obj in &objects {
prim_descs.push(prim_desc_for(
vertex_buffer,
index_buffer,
obj.base_vertex as usize,
obj.index_offset,
obj.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::None,
));
}
for c in &cluster_list {
prim_descs.push(prim_desc_for(
vertex_buffer,
index_buffer,
0,
c.index_offset,
c.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::None,
));
}
for &i in &skinned_objects {
let obj = &skinned_list[i];
prim_descs.push(prim_desc_for(
deformed_verts.as_ref(),
skinned_indices.as_ref(),
0,
obj.index_offset,
obj.index_count,
MTLIndexType::UInt32,
MTLAccelerationStructureUsage::Refit,
));
}
let mut blas: Vec<Retained<ProtocolObject<dyn MTLAccelerationStructure>>> =
Vec::with_capacity(prim_descs.len());
let mut max_scratch: usize = 0;
for prim in &prim_descs {
let sizes = device.accelerationStructureSizesWithDescriptor(prim);
let acc = device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or("failed to allocate BLAS")?;
max_scratch = max_scratch.max(sizes.buildScratchBufferSize);
blas.push(acc);
}
let mut instance_descs: Vec<MTLAccelerationStructureInstanceDescriptor> = objects
.iter()
.enumerate()
.map(|(i, obj)| instance_desc(obj, i))
.collect();
let mut geom_entries: Vec<RtGeomEntry> = objects
.iter()
.map(|obj| geom_entry(obj, albedo_count as u32))
.collect();
let mut cluster_instances: Vec<MTLAccelerationStructureInstanceDescriptor> = Vec::new();
let mut cluster_geom: Vec<RtGeomEntry> = Vec::new();
for (ci, c) in cluster_list.iter().enumerate() {
let blas_index = (draw_blas_count + ci) as u32;
for model in &c.instances {
cluster_instances.push(instance_desc_at(*model, blas_index));
cluster_geom.push(cluster_geom_entry(c, *model, albedo_count as u32));
}
}
instance_descs.extend_from_slice(&cluster_instances);
geom_entries.extend_from_slice(&cluster_geom);
for (si, &i) in skinned_objects.iter().enumerate() {
let obj = &skinned_list[i];
let blas_index = (skinned_blas_base + si) as u32;
instance_descs.push(instance_desc_at(obj.model, blas_index));
geom_entries.push(skinned_geom_entry(obj, albedo_count as u32));
}
let instance_buffer = upload_buffer(device, &instance_descs, "RT instance descriptors")?;
let geom_table = upload_buffer(device, &geom_entries, "RT geometry table")?;
let tlas_desc = make_tlas_desc(&blas, &instance_buffer, instance_descs.len());
let tlas_sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
let tlas = device
.newAccelerationStructureWithSize(tlas_sizes.accelerationStructureSize)
.ok_or("failed to allocate TLAS")?;
max_scratch = max_scratch.max(tlas_sizes.buildScratchBufferSize);
let scratch = device
.newBufferWithLength_options(max_scratch.max(1), MTLResourceOptions::StorageModePrivate)
.ok_or("failed to allocate 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("failed to create RT build command buffer")?;
for (acc, prim) in blas.iter().zip(prim_descs.iter()) {
let enc = cmd
.accelerationStructureCommandEncoder()
.ok_or("failed to create acceleration-structure encoder")?;
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
acc, prim, &scratch, 0,
);
enc.endEncoding();
}
let enc = cmd
.accelerationStructureCommandEncoder()
.ok_or("failed to create acceleration-structure encoder")?;
declare_blas_resident(&enc, &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 cached_models = objects.iter().map(|o| o.model).collect();
let draw_blas_sigs = objects.iter().map(|o| GeomSig::of(o)).collect();
let identity_palette = upload_buffer(device, &[IDENTITY4], "RT identity palette")?;
Ok(Some(RtAccelData {
blas,
static_blas_count: skinned_blas_base,
tlas,
geom_table,
object_indices,
draw_blas_sigs,
cached_models,
cluster_instances,
cluster_geom,
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,
update_scratch: RtUpdateScratch::default(),
}))
}
fn allocate_skinned_blas(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
deformed_verts: &ProtocolObject<dyn MTLBuffer>,
skinned_indices: &ProtocolObject<dyn MTLBuffer>,
shapes: &[SkinnedShape],
) -> Result<SkinnedBlasSet, String> {
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("failed to allocate 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)
}
fn objects_current(object_indices: &[usize], draw_objects: &[DrawObject]) -> bool {
object_indices.iter().all(|&idx| {
draw_objects
.get(idx)
.is_some_and(|o| o.resident && o.index_count >= 3)
})
}
fn objects_in_blas_order<'a>(
object_indices: &'a [usize],
draw_objects: &'a [DrawObject],
) -> impl Iterator<Item = &'a DrawObject> + Clone {
object_indices
.iter()
.filter_map(move |&idx| draw_objects.get(idx))
}
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],
albedo_count: usize,
) -> Result<(), String> {
if !objects_current(&self.object_indices, draw_objects) {
return Ok(());
}
let mut scratch = std::mem::take(&mut self.update_scratch);
scratch.instances.clear();
scratch.geom.clear();
for (i, obj) in objects_in_blas_order(&self.object_indices, draw_objects).enumerate() {
scratch.instances.push(instance_desc(obj, i));
scratch.geom.push(geom_entry(obj, albedo_count as u32));
}
scratch.instances.extend_from_slice(&self.cluster_instances);
scratch.geom.extend_from_slice(&self.cluster_geom);
let instance_descs = &scratch.instances;
let geom_entries = &scratch.geom;
let instance_buffer = upload_buffer(device, instance_descs, "RT instance descriptors")?;
let geom_table = upload_buffer(device, geom_entries, "RT geometry table")?;
let tlas_desc = make_tlas_desc(&self.blas, &instance_buffer, instance_descs.len());
let sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
let tlas = device
.newAccelerationStructureWithSize(sizes.accelerationStructureSize)
.ok_or("failed to allocate TLAS")?;
if (sizes.buildScratchBufferSize as u64) > self.scratch.length() as u64 {
self.scratch = device
.newBufferWithLength_options(
sizes.buildScratchBufferSize.max(1),
MTLResourceOptions::StorageModePrivate,
)
.ok_or("failed to grow RT scratch buffer")?;
}
let cmd = command_queue
.commandBuffer()
.ok_or("failed to create RT rebuild command buffer")?;
let enc = cmd
.accelerationStructureCommandEncoder()
.ok_or("failed to create acceleration-structure encoder")?;
declare_blas_resident(&enc, &self.blas);
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;
self.ring_published = false;
self.cached_models.clear();
self.cached_models
.extend(objects_in_blas_order(&self.object_indices, draw_objects).map(|o| o.model));
self.update_scratch = scratch;
Ok(())
}
pub(crate) fn is_empty(&self) -> bool {
self.object_indices.is_empty() && self.cluster_instances.is_empty()
}
pub(crate) fn refresh_static_topology(
&mut self,
gpu: RtGpu,
static_geometry: RtStaticGeometry,
draw_objects: &[DrawObject],
texture_counts: RtTextureCounts,
options: RtTopologyRefreshOptions,
) -> Result<(), String> {
let RtGpu {
device,
command_queue,
..
} = gpu;
let RtStaticGeometry {
vertex_buffer,
index_buffer,
} = static_geometry;
let RtTextureCounts { albedo_count } = texture_counts;
let RtTopologyRefreshOptions {
exclude_seethrough,
build_tlas,
frame_id,
} = options;
let new_indices: Vec<usize> = draw_objects
.iter()
.enumerate()
.filter(|(_, o)| {
o.resident
&& o.index_count >= 3
&& !(exclude_seethrough && o.material.see_through != 0)
})
.map(|(i, _)| i)
.collect();
let new_sigs: Vec<GeomSig> = new_indices
.iter()
.map(|&i| GeomSig::of(&draw_objects[i]))
.collect();
let plan = plan_topology_refresh(
&self.object_indices,
&self.draw_blas_sigs,
&new_indices,
&new_sigs,
);
let old_draw_count = self.object_indices.len();
let cluster_count = self.static_blas_count - old_draw_count;
let mut fresh: Vec<Option<Retained<ProtocolObject<dyn MTLAccelerationStructure>>>> =
(0..new_indices.len()).map(|_| None).collect();
let mut build_jobs: Vec<(usize, Retained<MTLPrimitiveAccelerationStructureDescriptor>)> =
Vec::new();
let mut max_scratch: usize = 0;
for (j, reuse) in plan.reuse.iter().enumerate() {
if reuse.is_some() {
continue;
}
let obj = &draw_objects[new_indices[j]];
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("failed to allocate 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 old_blas = std::mem::take(&mut self.blas);
let mut new_blas: Vec<Retained<ProtocolObject<dyn MTLAccelerationStructure>>> =
Vec::with_capacity(new_indices.len() + (old_blas.len() - old_draw_count));
for (j, reuse) in plan.reuse.iter().enumerate() {
match reuse {
Some(k) => new_blas.push(old_blas[*k].clone()),
None => new_blas.push(fresh[j].clone().expect("fresh BLAS built above")),
}
}
for b in &old_blas[old_draw_count..] {
new_blas.push(b.clone());
}
let mut retire_structures: Vec<Retained<ProtocolObject<dyn MTLAccelerationStructure>>> =
plan.retire.iter().map(|&k| old_blas[k].clone()).collect();
let mut retire_buffers: Vec<Retained<ProtocolObject<dyn MTLBuffer>>> = Vec::new();
drop(old_blas);
let do_tlas = build_tlas && !(new_indices.is_empty() && cluster_count == 0);
let tlas_build = if do_tlas {
let objects: Vec<&DrawObject> = new_indices.iter().map(|&i| &draw_objects[i]).collect();
let mut instance_descs: Vec<MTLAccelerationStructureInstanceDescriptor> = objects
.iter()
.enumerate()
.map(|(i, obj)| instance_desc(obj, i))
.collect();
let mut geom_entries: Vec<RtGeomEntry> = objects
.iter()
.map(|obj| geom_entry(obj, albedo_count as u32))
.collect();
instance_descs.extend_from_slice(&self.cluster_instances);
geom_entries.extend_from_slice(&self.cluster_geom);
let instance_buffer =
upload_buffer(device, &instance_descs, "RT instance descriptors")?;
let geom_table = upload_buffer(device, &geom_entries, "RT geometry table")?;
let tlas_desc = make_tlas_desc(&new_blas, &instance_buffer, instance_descs.len());
let tlas_sizes = device.accelerationStructureSizesWithDescriptor(&tlas_desc);
max_scratch = max_scratch.max(tlas_sizes.buildScratchBufferSize);
let tlas = device
.newAccelerationStructureWithSize(tlas_sizes.accelerationStructureSize)
.ok_or("failed to allocate TLAS")?;
tlas.setLabel(Some(&crate::metal::pipeline::ns_str("rt_tlas")));
let cached_models: Vec<[[f32; 4]; 4]> = objects.iter().map(|o| o.model).collect();
Some((tlas, tlas_desc, instance_buffer, geom_table, cached_models))
} else {
None
};
if !build_jobs.is_empty() || tlas_build.is_some() {
let scratch = device
.newBufferWithLength_options(
max_scratch.max(1),
MTLResourceOptions::StorageModePrivate,
)
.ok_or("failed to allocate topology-refresh scratch buffer")?;
let cmd = command_queue
.commandBuffer()
.ok_or("failed to create topology-refresh command buffer")?;
cmd.setLabel(Some(&crate::metal::pipeline::ns_str("rt_topology_build")));
for (j, prim) in &build_jobs {
let acc = fresh[*j].as_ref().expect("fresh BLAS allocated above");
let enc = cmd
.accelerationStructureCommandEncoder()
.ok_or("failed to create acceleration-structure encoder")?;
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("failed to create acceleration-structure encoder")?;
declare_blas_resident(&enc, &new_blas);
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
tlas, tlas_desc, &scratch, 0,
);
enc.endEncoding();
}
attach_async_fault_logger(&cmd, "RT topology build");
cmd.commit();
retire_buffers.push(scratch);
}
self.blas = new_blas;
self.static_blas_count = new_indices.len() + cluster_count;
self.object_indices = new_indices;
self.draw_blas_sigs = new_sigs;
self.head_generation = self.head_generation.wrapping_add(1);
if let Some((tlas, _, instance_buffer, geom_table, cached_models)) = tlas_build {
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,
));
self.cached_models = cached_models;
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>>],
texture_counts: RtTextureCounts,
frame: RtFrame,
) -> Result<(), String> {
let RtGpu {
device,
command_queue,
..
} = gpu;
let RtTextureCounts { albedo_count } = texture_counts;
if !objects_current(&self.object_indices, draw_objects) {
return Ok(());
}
let mut scratch = std::mem::take(&mut self.update_scratch);
let RtUpdateScratch {
skinned: skinned_objects,
shapes,
instances,
geom,
} = &mut scratch;
skinned_objects.clear();
skinned_objects.extend(
skinned
.objects
.iter()
.enumerate()
.filter(|(_, o)| o.visible && o.index_count >= 3)
.map(|(i, _)| i),
);
if skinned_objects.is_empty() {
self.update_scratch = scratch;
self.release_skinned();
return self.rebuild_tlas(device, command_queue, draw_objects, albedo_count);
}
let deformed_extent = skinned_objects
.iter()
.map(|&i| skinned.objects[i].vertex_base as usize + skinned.objects[i].vertex_count)
.max()
.unwrap_or(0);
let deformed_bytes = (deformed_extent * VERTEX_STRIDE).max(VERTEX_STRIDE);
shapes.clear();
shapes.extend(skinned_objects.iter().map(|&i| SkinnedShape {
index_offset: skinned.objects[i].index_offset,
index_count: skinned.objects[i].index_count,
}));
let static_blas_count = self.static_blas_count;
let head_generation = self.head_generation;
instances.clear();
geom.clear();
for (i, obj) in objects_in_blas_order(&self.object_indices, draw_objects).enumerate() {
instances.push(instance_desc(obj, i));
geom.push(geom_entry(obj, albedo_count as u32));
}
instances.extend_from_slice(&self.cluster_instances);
geom.extend_from_slice(&self.cluster_geom);
for (si, &oi) in skinned_objects.iter().enumerate() {
let obj = &skinned.objects[oi];
instances.push(instance_desc_at(obj.model, (static_blas_count + si) as u32));
geom.push(skinned_geom_entry(obj, albedo_count as u32));
}
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.shape_matches(shapes);
if shape_changed {
slot.set_skinned(
allocate_skinned_blas(
device,
deformed_verts.as_ref(),
skinned_indices.as_ref(),
shapes,
)?,
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>> = self.blas
[..static_blas_count]
.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("failed to create RT skin command buffer")?;
skin_cmd.setLabel(Some(&crate::metal::pipeline::ns_str("rt_skin")));
let cenc = skin_cmd
.computeCommandEncoder()
.ok_or("failed to create RT skin compute encoder")?;
encode_skin_dispatch(
&cenc,
&skinned,
skinned_objects,
deformed_verts.as_ref(),
SkinPalettes::Prebuilt {
buffers: joint_buffers,
identity: &self.identity_palette,
},
)?;
cenc.endEncoding();
attach_async_fault_logger(&skin_cmd, "skinning compute");
skin_cmd.commit();
}
let update = slot.plan_blas_update(shape_changed);
{
let cmd = command_queue
.commandBuffer()
.ok_or("failed to create RT skinned rebuild command buffer")?;
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("failed to create acceleration-structure encoder")?;
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("failed to create acceleration-structure encoder")?;
declare_blas_resident(
&enc,
self.blas[..static_blas_count]
.iter()
.chain(slot.skinned_blas().iter()),
);
enc.buildAccelerationStructure_descriptor_scratchBuffer_scratchBufferOffset(
&tlas,
&tlas_desc,
&scratch_buffer,
0,
);
enc.endEncoding();
attach_async_fault_logger(&cmd, "skinned BLAS + TLAS build");
cmd.commit();
}
let takeover = !self.ring_published;
let old_skinned = if takeover {
self.blas.split_off(static_blas_count)
} else {
self.blas.truncate(static_blas_count);
Vec::new()
};
self.blas.extend(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.cached_models.clear();
self.cached_models
.extend(objects_in_blas_order(&self.object_indices, draw_objects).map(|o| o.model));
self.ring_published = true;
self.update_scratch = scratch;
Ok(())
}
fn release_skinned(&mut self) {
self.blas.truncate(self.static_blas_count);
self.deformed_verts = self.deformed_dummy.clone();
self.ring.release_all();
}
pub(crate) fn retire_completed(&mut self, frame_id: u64, depth: usize) {
self.retire_pool.collect(frame_id, depth as u64);
}
pub(crate) fn transforms_dirty(&self, draw_objects: &[DrawObject]) -> bool {
models_dirty(&self.object_indices, &self.cached_models, |idx| {
draw_objects.get(idx).map(|o| o.model)
})
}
}
fn models_dirty(
object_indices: &[usize],
cached_models: &[[[f32; 4]; 4]],
current: impl Fn(usize) -> Option<[[f32; 4]; 4]>,
) -> bool {
if object_indices.len() != cached_models.len() {
return true;
}
object_indices
.iter()
.zip(cached_models.iter())
.any(|(&idx, cached)| current(idx) != Some(*cached))
}
pub(crate) fn build_rt_skin_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>, String> {
use objc2_metal::{MTLDevice as _, MTLLibrary as _};
let library = crate::metal::slang_shaders::RT_SKIN.library(device, hot_reload)?;
let func = library
.newFunctionWithName(&crate::metal::pipeline::ns_str("rt_skin"))
.ok_or("rt_skin kernel not found")?;
device
.newComputePipelineStateWithFunction_error(&func)
.map_err(|e| format!("failed to create RT skin pipeline: {:?}", e))
}
fn check_build_status(
cmd: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
what: &str,
) -> Result<(), String> {
if cmd.status() == MTLCommandBufferStatus::Error {
return Err(format!("RT {what} faulted on the GPU: {:?}", cmd.error()));
}
Ok(())
}
fn upload_buffer<T: Copy>(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
data: &[T],
what: &str,
) -> Result<Retained<ProtocolObject<dyn MTLBuffer>>, String> {
let bytes = std::mem::size_of_val(data);
if bytes == 0 {
return device
.newBufferWithLength_options(1, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| format!("failed to allocate buffer for {what}"));
}
let ptr = std::ptr::NonNull::new(data.as_ptr() as *mut std::ffi::c_void)
.ok_or_else(|| format!("{what}: null data pointer"))?;
unsafe {
device.newBufferWithBytes_length_options(ptr, bytes, MTLResourceOptions::StorageModeShared)
}
.ok_or_else(|| format!("failed to allocate buffer for {what}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[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)
);
}
fn sig(tag: usize) -> GeomSig {
GeomSig {
base_vertex: tag as i32,
vertex_offset: tag * 100,
index_offset: tag,
index_count: 3,
generation: 0,
}
}
#[test]
fn topology_plan_reuses_an_unchanged_set() {
let old_i = [2usize, 5, 7];
let old_s = [sig(2), sig(5), sig(7)];
let plan = plan_topology_refresh(&old_i, &old_s, &old_i, &old_s);
assert_eq!(plan.reuse, vec![Some(0), Some(1), Some(2)]);
assert!(plan.retire.is_empty());
}
#[test]
fn topology_plan_builds_only_the_added_slot() {
let old_i = [2usize, 5];
let old_s = [sig(2), sig(5)];
let new_i = [2usize, 5, 9];
let new_s = [sig(2), sig(5), sig(9)];
let plan = plan_topology_refresh(&old_i, &old_s, &new_i, &new_s);
assert_eq!(plan.reuse, vec![Some(0), Some(1), None]);
assert!(plan.retire.is_empty());
}
#[test]
fn topology_plan_retires_a_removed_slot() {
let old_i = [2usize, 5, 7];
let old_s = [sig(2), sig(5), sig(7)];
let new_i = [2usize, 7];
let new_s = [sig(2), sig(7)];
let plan = plan_topology_refresh(&old_i, &old_s, &new_i, &new_s);
assert_eq!(plan.reuse, vec![Some(0), Some(2)]);
assert_eq!(plan.retire, vec![1]); }
#[test]
fn topology_plan_rebuilds_a_recycled_slot_whose_geometry_moved() {
let old_i = [5usize];
let old_s = [sig(5)];
let new_i = [5usize];
let new_s = [sig(8)]; let plan = plan_topology_refresh(&old_i, &old_s, &new_i, &new_s);
assert_eq!(plan.reuse, vec![None]);
assert_eq!(plan.retire, vec![0]);
}
#[test]
fn topology_plan_reuses_across_reorder_by_index() {
let old_i = [2usize, 5];
let old_s = [sig(2), sig(5)];
let new_i = [5usize, 2];
let new_s = [sig(5), sig(2)];
let plan = plan_topology_refresh(&old_i, &old_s, &new_i, &new_s);
assert_eq!(plan.reuse, vec![Some(1), Some(0)]);
assert!(plan.retire.is_empty());
}
#[test]
fn rt_geom_entry_is_128_bytes() {
assert_eq!(std::mem::size_of::<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);
}
fn draw_object(generation: u32) -> DrawObject {
DrawObject {
vertex_offset: 0,
vertex_count: 8,
index_offset: 0,
index_count: 6,
base_vertex: 0,
geometry_generation: generation,
shader_bucket: 0,
model: [[0.0; 4]; 4],
texture_slot: 0,
normal_map_slot: 0,
material: crate::gfx::render_types::MaterialUniforms::DEFAULT,
visible: true,
resident: true,
bb_min: [0.0; 3],
bb_max: [1.0; 3],
cull_distance: 0.0,
lod_alternates: Vec::new(),
}
}
#[test]
fn objects_current_rejects_a_changed_draw_list() {
let objects = vec![draw_object(0), draw_object(1), draw_object(2)];
assert!(objects_current(&[0, 2], &objects));
assert!(!objects_current(&[0, 5], &objects));
let mut evicted = vec![draw_object(0), draw_object(1)];
evicted[1].resident = false;
assert!(!objects_current(&[0, 1], &evicted));
let mut degenerate = vec![draw_object(0), draw_object(1)];
degenerate[1].index_count = 0;
assert!(!objects_current(&[0, 1], °enerate));
}
#[test]
fn objects_in_blas_order_follows_the_index_list() {
let objects = vec![draw_object(0), draw_object(1), draw_object(2)];
let seen: Vec<u32> = objects_in_blas_order(&[2, 0], &objects)
.map(|o| o.geometry_generation)
.collect();
assert_eq!(seen, vec![2, 0]);
let seen: Vec<u32> = objects_in_blas_order(&[1, 9], &objects)
.map(|o| o.geometry_generation)
.collect();
assert_eq!(seen, vec![1]);
}
#[test]
fn models_dirty_detects_moves_and_shape_changes() {
let ident = [
[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],
];
let mut moved = ident;
moved[3][0] = 5.0;
let indices = vec![0usize, 2usize];
let cached = vec![ident, ident];
assert!(!models_dirty(&indices, &cached, |idx| match idx {
0 | 2 => Some(ident),
_ => None,
}));
assert!(models_dirty(&indices, &cached, |idx| match idx {
0 => Some(moved),
2 => Some(ident),
_ => None,
}));
assert!(models_dirty(&indices, &cached, |idx| match idx {
0 => Some(ident),
_ => None,
}));
assert!(models_dirty(&[0usize], &cached, |_| Some(ident)));
}
}