use super::error::allocation_failed;
use concinnity_core::render::buffer_growth::grow_capacity;
use concinnity_core::render::error::RenderResult;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLResourceOptions};
use super::context::{bytes_of_slice, write_buffer_region};
pub(super) fn grow_to(have: usize, needed: usize) -> Option<usize> {
grow_capacity(have as u64, needed.max(1) as u64, 256).map(|c| c as usize)
}
pub(super) struct TransientRing {
slots: Vec<Option<Retained<ProtocolObject<dyn MTLBuffer>>>>,
}
impl TransientRing {
pub(super) fn new(depth: usize) -> Self {
Self {
slots: (0..depth.max(1)).map(|_| None).collect(),
}
}
pub(super) fn slot(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
min_len: usize,
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
Ok(self.slot_fresh(device, slot, min_len)?.0)
}
pub(super) fn slot_fresh(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
min_len: usize,
) -> RenderResult<(Retained<ProtocolObject<dyn MTLBuffer>>, bool)> {
let idx = slot % self.slots.len();
let have = self.slots[idx].as_ref().map_or(0, |buf| buf.length());
let grown = grow_to(have, min_len);
if let Some(cap) = grown {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("transient ring buffer"))?;
self.slots[idx] = Some(buf);
}
Ok((
self.slots[idx]
.as_ref()
.expect("ring slot was just ensured")
.clone(),
grown.is_some(),
))
}
pub(super) fn write(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
bytes: &[u8],
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let buf = self.slot(device, slot, bytes.len().max(1))?;
write_buffer_region(&buf, 0, bytes)?;
Ok(buf)
}
}
type ObjectBuffer = Option<Retained<ProtocolObject<dyn MTLBuffer>>>;
#[derive(Default)]
struct SkinSlot {
palette: ObjectBuffer,
weights: ObjectBuffer,
}
pub(super) struct JointRing {
slots: Vec<Vec<SkinSlot>>,
}
impl JointRing {
pub(super) fn new(depth: usize) -> Self {
Self {
slots: (0..depth.max(1)).map(|_| Vec::new()).collect(),
}
}
pub(super) fn write_all(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
palettes: &[Vec<[[f32; 4]; 4]>],
) -> RenderResult<Vec<Retained<ProtocolObject<dyn MTLBuffer>>>> {
self.objects(slot, palettes.len())
.iter_mut()
.zip(palettes)
.map(|(o, mats)| fill(device, &mut o.palette, bytes_of_slice(mats), "joint"))
.collect()
}
pub(super) fn write_weights(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
weights: &[Vec<f32>],
) -> RenderResult<Vec<Retained<ProtocolObject<dyn MTLBuffer>>>> {
self.objects(slot, weights.len())
.iter_mut()
.zip(weights)
.map(|(o, w)| fill(device, &mut o.weights, bytes_of_slice(w), "morph weight"))
.collect()
}
fn objects(&mut self, slot: usize, count: usize) -> &mut [SkinSlot] {
let idx = slot % self.slots.len();
let slots = &mut self.slots[idx];
if slots.len() < count {
slots.resize_with(count, SkinSlot::default);
}
&mut slots[..count]
}
}
fn fill(
device: &ProtocolObject<dyn MTLDevice>,
cell: &mut ObjectBuffer,
bytes: &[u8],
what: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let have = cell.as_ref().map_or(0, |buf| buf.length());
if let Some(cap) = grow_to(have, bytes.len()) {
let buf = device
.newBufferWithLength_options(cap, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed(format_args!("{what} ring buffer")))?;
*cell = Some(buf);
}
let buf = cell.as_ref().expect("ring slot was just ensured");
write_buffer_region(buf, 0, bytes)?;
Ok(buf.clone())
}
#[cfg(test)]
mod tests {
use super::{TransientRing, grow_to};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice};
fn device() -> Option<Retained<ProtocolObject<dyn MTLDevice>>> {
objc2_metal::MTLCreateSystemDefaultDevice()
}
fn read_back(buf: &ProtocolObject<dyn MTLBuffer>, len: usize) -> Vec<u8> {
unsafe { std::slice::from_raw_parts(buf.contents().as_ptr() as *const u8, len).to_vec() }
}
#[test]
fn an_empty_slot_always_gets_a_buffer() {
assert_eq!(grow_to(0, 0), Some(256));
assert_eq!(grow_to(256, 0), None);
}
#[test]
fn steady_state_writes_reuse_one_buffer_per_slot() {
let Some(device) = device() else {
return;
};
let mut ring = TransientRing::new(2);
let first = ring.write(&device, 0, &[7u8; 64]).expect("first write");
for _ in 0..8 {
let again = ring.write(&device, 0, &[7u8; 64]).expect("repeat write");
assert!(
std::ptr::eq(&*first, &*again),
"a slot that already fits must not reallocate"
);
}
}
#[test]
fn write_copies_the_bytes_into_the_slot() {
let Some(device) = device() else {
return;
};
let mut ring = TransientRing::new(2);
let payload: Vec<u8> = (0..96u8).collect();
let buf = ring.write(&device, 0, &payload).expect("write");
assert_eq!(read_back(&buf, payload.len()), payload);
let next = vec![0xABu8; 32];
let buf = ring.write(&device, 0, &next).expect("rewrite");
assert_eq!(read_back(&buf, next.len()), next);
}
#[test]
fn distinct_slots_get_distinct_buffers() {
let Some(device) = device() else {
return;
};
let mut ring = TransientRing::new(2);
let a = ring.write(&device, 0, &[1u8; 48]).expect("slot 0");
let b = ring.write(&device, 1, &[2u8; 48]).expect("slot 1");
assert!(!std::ptr::eq(&*a, &*b));
assert_eq!(read_back(&a, 48), vec![1u8; 48]);
assert_eq!(read_back(&b, 48), vec![2u8; 48]);
}
#[test]
fn slot_indices_wrap_modulo_depth() {
let Some(device) = device() else {
return;
};
let mut ring = TransientRing::new(2);
let a = ring.write(&device, 0, &[1u8; 16]).expect("frame 0");
let c = ring.write(&device, 2, &[3u8; 16]).expect("frame 2");
assert!(std::ptr::eq(&*a, &*c), "frame 2 must reuse slot 0");
}
#[test]
fn a_slot_grows_once_and_never_shrinks() {
let Some(device) = device() else {
return;
};
let mut ring = TransientRing::new(1);
let small = ring.write(&device, 0, &[0u8; 16]).expect("small");
let small_len = small.length();
let big = ring.write(&device, 0, &[9u8; 4096]).expect("big");
assert!(big.length() >= 4096);
assert!(big.length() > small_len);
let shrunk = ring.write(&device, 0, &[5u8; 8]).expect("small again");
assert!(
std::ptr::eq(&*big, &*shrunk),
"a grown slot must not be reallocated by a smaller write"
);
}
}