#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLAccelerationStructure, MTLBuffer, MTLDevice, MTLInstanceAccelerationStructureDescriptor,
MTLPrimitiveAccelerationStructureDescriptor, MTLResource as _, MTLResourceOptions,
};
use super::transient::grow_to;
type Buffer = Retained<ProtocolObject<dyn MTLBuffer>>;
type Structure = Retained<ProtocolObject<dyn MTLAccelerationStructure>>;
type PrimDesc = Retained<MTLPrimitiveAccelerationStructureDescriptor>;
const REFIT_LIMIT: u32 = 32;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) struct SkinnedShape {
pub index_offset: usize,
pub index_count: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum BlasUpdate {
Build,
Refit,
}
fn blas_update(shape_changed: bool, built: bool, refits: u32, limit: u32) -> BlasUpdate {
if shape_changed || !built || refits >= limit {
BlasUpdate::Build
} else {
BlasUpdate::Refit
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) struct TlasKey {
pub head_generation: u64,
pub slot_generation: u64,
pub instance_count: usize,
}
pub(super) struct SkinnedBlasSet {
pub blas: Vec<Structure>,
pub descs: Vec<PrimDesc>,
pub scratch_bytes: usize,
}
pub(super) struct RtFrameSlot {
deformed: Option<Buffer>,
blas: Vec<Structure>,
descs: Vec<PrimDesc>,
shape: Vec<SkinnedShape>,
blas_scratch: usize,
built: bool,
refits: u32,
tlas: Option<Structure>,
tlas_size: usize,
tlas_desc: Option<(
TlasKey,
Retained<MTLInstanceAccelerationStructureDescriptor>,
)>,
instances: Option<Buffer>,
geom_table: Option<Buffer>,
scratch: Option<Buffer>,
generation: u64,
}
impl RtFrameSlot {
fn new() -> Self {
Self {
deformed: None,
blas: Vec::new(),
descs: Vec::new(),
shape: Vec::new(),
blas_scratch: 0,
built: false,
refits: 0,
tlas: None,
tlas_size: 0,
tlas_desc: None,
instances: None,
geom_table: None,
scratch: None,
generation: 0,
}
}
pub(super) fn generation(&self) -> u64 {
self.generation
}
pub(super) fn deformed(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> Result<(Buffer, bool), String> {
let have = self.deformed.as_ref().map_or(0, |b| b.length());
let mut fresh = false;
if let Some(cap) = grow_to(have, bytes) {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate RT deformed-vertex buffer")?;
buf.setLabel(Some(&super::pipeline::ns_str("rt_deformed_verts")));
self.deformed = Some(buf);
self.generation = self.generation.wrapping_add(1);
fresh = true;
}
let buf = self
.deformed
.as_ref()
.expect("deformed slot was just ensured")
.clone();
Ok((buf, fresh))
}
pub(super) fn instances(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> Result<Buffer, String> {
let have = self.instances.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
self.instances = Some(shared_buffer(
device,
cap,
"rt_instances",
"RT instance descriptors",
)?);
self.generation = self.generation.wrapping_add(1);
}
Ok(self
.instances
.as_ref()
.expect("instance slot was just ensured")
.clone())
}
pub(super) fn geom_table(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> Result<Buffer, String> {
let have = self.geom_table.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
self.geom_table = Some(shared_buffer(
device,
cap,
"rt_geom_table",
"RT geometry table",
)?);
}
Ok(self
.geom_table
.as_ref()
.expect("geometry-table slot was just ensured")
.clone())
}
pub(super) fn scratch(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
) -> Result<Buffer, String> {
let have = self.scratch.as_ref().map_or(0, |b| b.length());
if let Some(cap) = grow_to(have, bytes) {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModePrivate)
.ok_or("failed to allocate RT scratch buffer")?;
buf.setLabel(Some(&super::pipeline::ns_str("rt_scratch")));
self.scratch = Some(buf);
}
Ok(self
.scratch
.as_ref()
.expect("scratch slot was just ensured")
.clone())
}
pub(super) fn shape_matches(&self, shapes: &[SkinnedShape]) -> bool {
self.shape == shapes
}
pub(super) fn set_skinned(&mut self, built: SkinnedBlasSet, shapes: &[SkinnedShape]) {
self.blas = built.blas;
self.descs = built.descs;
self.blas_scratch = built.scratch_bytes;
self.shape.clear();
self.shape.extend_from_slice(shapes);
self.built = false;
self.refits = 0;
self.generation = self.generation.wrapping_add(1);
}
pub(super) fn blas_scratch(&self) -> usize {
self.blas_scratch
}
pub(super) fn skinned_blas(&self) -> &[Structure] {
&self.blas
}
pub(super) fn skinned_descs(&self) -> &[PrimDesc] {
&self.descs
}
pub(super) fn plan_blas_update(&mut self, shape_changed: bool) -> BlasUpdate {
let update = blas_update(shape_changed, self.built, self.refits, REFIT_LIMIT);
match update {
BlasUpdate::Build => {
self.built = true;
self.refits = 0;
}
BlasUpdate::Refit => self.refits += 1,
}
update
}
pub(super) fn tlas(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
size: usize,
) -> Result<Structure, String> {
if self.tlas.is_none() || self.tlas_size < size {
let tlas = device
.newAccelerationStructureWithSize(size.max(1))
.ok_or("failed to allocate TLAS")?;
tlas.setLabel(Some(&super::pipeline::ns_str("rt_tlas")));
self.tlas = Some(tlas);
self.tlas_size = size;
}
Ok(self
.tlas
.as_ref()
.expect("TLAS slot was just ensured")
.clone())
}
pub(super) fn tlas_desc(
&self,
key: TlasKey,
) -> Option<Retained<MTLInstanceAccelerationStructureDescriptor>> {
self.tlas_desc
.as_ref()
.filter(|(cached, _)| *cached == key)
.map(|(_, desc)| desc.clone())
}
pub(super) fn set_tlas_desc(
&mut self,
key: TlasKey,
desc: Retained<MTLInstanceAccelerationStructureDescriptor>,
) {
self.tlas_desc = Some((key, desc));
}
pub(super) fn release(&mut self) {
if self.blas.is_empty() && !self.built {
return;
}
self.blas.clear();
self.descs.clear();
self.shape.clear();
self.blas_scratch = 0;
self.built = false;
self.refits = 0;
self.generation = self.generation.wrapping_add(1);
}
}
pub(super) struct RtFrameRing {
slots: Vec<RtFrameSlot>,
}
impl RtFrameRing {
pub(super) fn new(depth: usize) -> Self {
Self {
slots: (0..depth.max(1)).map(|_| RtFrameSlot::new()).collect(),
}
}
pub(super) fn slot(&mut self, ring_slot: usize) -> &mut RtFrameSlot {
let idx = ring_slot % self.slots.len();
&mut self.slots[idx]
}
pub(super) fn release_all(&mut self) {
for slot in &mut self.slots {
slot.release();
}
}
}
fn shared_buffer(
device: &ProtocolObject<dyn MTLDevice>,
bytes: usize,
label: &str,
what: &str,
) -> Result<Buffer, String> {
let buf = device
.newBufferWithLength_options(bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| format!("failed to allocate buffer for {what}"))?;
buf.setLabel(Some(&super::pipeline::ns_str(label)));
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_changed_triangle_set_forces_a_full_build() {
assert_eq!(blas_update(true, true, 0, 32), BlasUpdate::Build);
}
#[test]
fn an_unbuilt_slot_cannot_be_refit() {
assert_eq!(blas_update(false, false, 0, 32), BlasUpdate::Build);
}
#[test]
fn a_stable_shape_refits_until_the_limit() {
assert_eq!(blas_update(false, true, 0, 32), BlasUpdate::Refit);
assert_eq!(blas_update(false, true, 31, 32), BlasUpdate::Refit);
assert_eq!(blas_update(false, true, 32, 32), BlasUpdate::Build);
assert_eq!(blas_update(false, true, 99, 32), BlasUpdate::Build);
}
#[test]
fn a_zero_limit_never_refits() {
assert_eq!(blas_update(false, true, 0, 0), BlasUpdate::Build);
}
#[test]
fn shape_equality_is_offset_and_count() {
let a = SkinnedShape {
index_offset: 12,
index_count: 300,
};
assert_eq!(a, a);
assert_ne!(
a,
SkinnedShape {
index_offset: 13,
index_count: 300,
}
);
assert_ne!(
a,
SkinnedShape {
index_offset: 12,
index_count: 303,
}
);
}
#[test]
fn tlas_key_separates_head_slot_and_instance_count() {
let base = TlasKey {
head_generation: 1,
slot_generation: 2,
instance_count: 3,
};
assert_eq!(base, base);
assert_ne!(
base,
TlasKey {
head_generation: 2,
..base
}
);
assert_ne!(
base,
TlasKey {
slot_generation: 3,
..base
}
);
assert_ne!(
base,
TlasKey {
instance_count: 4,
..base
}
);
}
}