use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLResourceOptions};
use crate::gfx::render_types::TextDrawCall;
use super::context::write_buffer_region;
use super::transient::grow_to;
const TEXT_UPLOAD_ALIGN: u64 = 256;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct TextRange {
pub(super) vertex_offset: usize,
pub(super) index_offset: usize,
}
fn align_up(offset: usize) -> usize {
crate::gfx::fullscreen::align_up(offset as u64, TEXT_UPLOAD_ALIGN) as usize
}
fn plan_text_upload(text_calls: &[TextDrawCall], out: &mut Vec<TextRange>) -> usize {
out.clear();
out.reserve(text_calls.len());
let mut cursor = 0usize;
for call in text_calls {
let vertex_offset = align_up(cursor);
let index_offset =
align_up(vertex_offset + std::mem::size_of_val(call.vertices.as_slice()));
cursor = index_offset + std::mem::size_of_val(call.indices.as_slice());
out.push(TextRange {
vertex_offset,
index_offset,
});
}
cursor
}
pub(super) struct TextUploadRing {
slots: Vec<Option<Retained<ProtocolObject<dyn MTLBuffer>>>>,
ranges: Vec<TextRange>,
frame_slot: Option<usize>,
}
impl TextUploadRing {
pub(super) fn new(depth: usize) -> Self {
Self {
slots: (0..depth.max(1)).map(|_| None).collect(),
ranges: Vec::new(),
frame_slot: None,
}
}
pub(super) fn upload(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
slot: usize,
text_calls: &[TextDrawCall],
) -> Result<(), String> {
let Self {
slots,
ranges,
frame_slot,
} = self;
*frame_slot = None;
let total = plan_text_upload(text_calls, ranges);
if total == 0 {
return Ok(());
}
let idx = slot % slots.len();
let have = slots[idx].as_ref().map_or(0, |buf| buf.length());
if let Some(capacity) = grow_to(have, total) {
slots[idx] = Some(
device
.newBufferWithLength_options(capacity, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate text upload buffer")?,
);
}
let buffer = slots[idx]
.as_ref()
.expect("text ring slot was just ensured");
for (call, range) in text_calls.iter().zip(ranges.iter()) {
write_buffer_region(
buffer,
range.vertex_offset,
bytemuck::cast_slice(&call.vertices),
)?;
write_buffer_region(
buffer,
range.index_offset,
bytemuck::cast_slice(&call.indices),
)?;
}
*frame_slot = Some(idx);
Ok(())
}
pub(super) fn binding(&self) -> Option<(&ProtocolObject<dyn MTLBuffer>, &[TextRange])> {
let buffer = self.slots[self.frame_slot?].as_ref()?;
Some((buffer, &self.ranges))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::render_types::TextVertex;
fn glyph_call(glyphs: usize) -> TextDrawCall {
TextDrawCall {
vertices: vec![
TextVertex {
pos: [0.0; 2],
uv: [0.0; 2],
color: [0.0; 3],
mode: 0.0,
};
glyphs * 4
],
indices: vec![0u16; glyphs * 6],
atlas_slot: 0,
clip_rect: None,
layer: 0,
}
}
#[test]
fn planning_no_calls_needs_no_buffer() {
let mut ranges = Vec::new();
assert_eq!(plan_text_upload(&[], &mut ranges), 0);
assert!(ranges.is_empty());
}
#[test]
fn planning_emits_one_range_per_call() {
let calls = [glyph_call(2), glyph_call(0), glyph_call(5)];
let mut ranges = Vec::new();
plan_text_upload(&calls, &mut ranges);
assert_eq!(ranges.len(), calls.len());
}
#[test]
fn planned_blocks_are_aligned_and_ordered() {
let calls = [glyph_call(1), glyph_call(9), glyph_call(3)];
let mut ranges = Vec::new();
let total = plan_text_upload(&calls, &mut ranges);
let align = TEXT_UPLOAD_ALIGN as usize;
let mut prev_end = 0usize;
for (call, range) in calls.iter().zip(ranges.iter()) {
assert_eq!(range.vertex_offset % align, 0);
assert_eq!(range.index_offset % align, 0);
assert!(range.vertex_offset >= prev_end);
let vertex_end = range.vertex_offset + std::mem::size_of_val(call.vertices.as_slice());
assert!(range.index_offset >= vertex_end);
prev_end = range.index_offset + std::mem::size_of_val(call.indices.as_slice());
assert!(prev_end <= total);
}
}
#[test]
fn planned_total_fits_the_shared_reservation() {
let calls = [glyph_call(4), glyph_call(1), glyph_call(120)];
let mut ranges = Vec::new();
let total = plan_text_upload(&calls, &mut ranges) as u64;
let reserved = crate::gfx::fullscreen::text_upload_bytes(&calls, TEXT_UPLOAD_ALIGN);
assert!(
total <= reserved,
"planned {total} exceeded reserved {reserved}"
);
}
#[test]
fn replanning_drops_the_previous_frames_ranges() {
let mut ranges = Vec::new();
plan_text_upload(&[glyph_call(2), glyph_call(2)], &mut ranges);
assert_eq!(ranges.len(), 2);
plan_text_upload(&[glyph_call(1)], &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].vertex_offset, 0);
}
}