use std::{cell::Cell, ops::Range, rc::Rc};
use crate::{
frame_graph::{FrameCommandStats, write_buffer},
render::GlyphInstance,
};
const MIN_CHUNK_QUADS: u32 = 1024;
const MAX_CHUNK_QUADS: u32 = 8192;
const MAX_RUN_QUADS: u32 = 1 << 20;
#[derive(Debug)]
pub(crate) struct SpanAllocator {
capacity: u32,
free: Vec<Range<u32>>,
}
impl SpanAllocator {
pub(crate) fn new(capacity: u32) -> Self {
let mut free = Vec::new();
if capacity > 0 {
free.push(0..capacity);
}
Self { capacity, free }
}
pub(crate) fn capacity(&self) -> u32 {
self.capacity
}
pub(crate) fn allocate(&mut self, len: u32) -> Option<u32> {
if len == 0 {
return None;
}
let slot = self
.free
.iter()
.position(|range| range.end - range.start >= len)?;
let range = &mut self.free[slot];
let start = range.start;
range.start += len;
if range.start == range.end {
self.free.remove(slot);
}
Some(start)
}
pub(crate) fn release(&mut self, span: Range<u32>) {
if span.start >= span.end || span.end > self.capacity {
return;
}
let slot = self.free.partition_point(|range| range.start < span.start);
let joins_previous = slot > 0 && self.free[slot - 1].end == span.start;
let joins_next = self
.free
.get(slot)
.is_some_and(|next| next.start == span.end);
match (joins_previous, joins_next) {
(true, true) => {
let end = self.free[slot].end;
self.free[slot - 1].end = end;
self.free.remove(slot);
}
(true, false) => self.free[slot - 1].end = span.end,
(false, true) => self.free[slot].start = span.start,
(false, false) => self.free.insert(slot, span),
}
}
pub(crate) fn is_unused(&self) -> bool {
self.capacity == 0 || self.free.first() == Some(&(0..self.capacity))
}
}
#[derive(Debug)]
struct RetiredSpan {
chunk: u64,
quads: Range<u32>,
}
#[derive(Default)]
struct RetiredSpans(Cell<Vec<RetiredSpan>>);
impl RetiredSpans {
fn push(&self, span: RetiredSpan) {
let mut spans = self.0.take();
spans.push(span);
self.0.set(spans);
}
fn take(&self) -> Vec<RetiredSpan> {
self.0.take()
}
}
pub(crate) struct GlyphRunSpan {
buffer: wgpu::Buffer,
chunk: u64,
quads: Range<u32>,
retired: Rc<RetiredSpans>,
}
impl GlyphRunSpan {
pub(crate) fn instance_buffer(&self) -> &wgpu::Buffer {
&self.buffer
}
pub(crate) fn instances(&self) -> Range<u32> {
self.quads.clone()
}
}
impl Drop for GlyphRunSpan {
fn drop(&mut self) {
self.retired.push(RetiredSpan {
chunk: self.chunk,
quads: self.quads.clone(),
});
}
}
struct Chunk {
id: u64,
buffer: wgpu::Buffer,
spans: SpanAllocator,
}
struct StagedSpan {
chunk: u64,
first_quad: u32,
instances: Range<usize>,
}
#[derive(Default)]
pub(crate) struct GlyphRunArena {
chunks: Vec<Chunk>,
next_chunk: u64,
staged_instances: Vec<GlyphInstance>,
staged: Vec<StagedSpan>,
retired: Rc<RetiredSpans>,
}
impl GlyphRunArena {
pub(crate) fn insert<I>(&mut self, device: &wgpu::Device, quads: I) -> Option<GlyphRunSpan>
where
I: IntoIterator<Item = GlyphInstance>,
{
let start = self.staged_instances.len();
self.staged_instances.extend(quads);
let count = self.staged_instances.len() - start;
let Some(count) = u32::try_from(count)
.ok()
.filter(|count| (1..=MAX_RUN_QUADS).contains(count))
else {
self.staged_instances.truncate(start);
return None;
};
let (chunk, first_quad) = match self.allocate(count) {
Some(place) => place,
None => self.open_chunk(device, count),
};
self.staged.push(StagedSpan {
chunk: self.chunks[chunk].id,
first_quad,
instances: start..self.staged_instances.len(),
});
Some(GlyphRunSpan {
buffer: self.chunks[chunk].buffer.clone(),
chunk: self.chunks[chunk].id,
quads: first_quad..first_quad + count,
retired: Rc::clone(&self.retired),
})
}
pub(crate) fn begin_frame(&mut self) {
for span in self.retired.take() {
if let Some(chunk) = self.chunks.iter_mut().find(|chunk| chunk.id == span.chunk) {
chunk.spans.release(span.quads);
}
}
let mut keep_empty = self.chunks.iter().all(|chunk| chunk.spans.is_unused());
self.chunks.retain(|chunk| {
if !chunk.spans.is_unused() {
return true;
}
let keep = keep_empty && chunk.spans.capacity() <= MAX_CHUNK_QUADS;
keep_empty &= !keep;
keep
});
}
pub(crate) fn flush(&mut self, queue: &wgpu::Queue) -> FrameCommandStats {
let mut stats = FrameCommandStats::default();
let mut index = 0;
while index < self.staged.len() {
let first = &self.staged[index];
let mut end_quad = first.first_quad + span_quads(first);
let mut instances = first.instances.clone();
let mut next = index + 1;
while let Some(span) = self.staged.get(next) {
if span.chunk != first.chunk
|| span.first_quad != end_quad
|| span.instances.start != instances.end
{
break;
}
end_quad += span_quads(span);
instances.end = span.instances.end;
next += 1;
}
if let Some(chunk) = self.chunks.iter().find(|chunk| chunk.id == first.chunk) {
stats += write_buffer(
queue,
&chunk.buffer,
instance_offset(first.first_quad),
bytemuck::cast_slice(&self.staged_instances[instances]),
);
}
index = next;
}
self.staged.clear();
self.staged_instances.clear();
stats
}
fn allocate(&mut self, quads: u32) -> Option<(usize, u32)> {
self.chunks
.iter_mut()
.enumerate()
.find_map(|(slot, chunk)| chunk.spans.allocate(quads).map(|first| (slot, first)))
}
fn open_chunk(&mut self, device: &wgpu::Device, quads: u32) -> (usize, u32) {
let largest = self
.chunks
.iter()
.map(|chunk| chunk.spans.capacity())
.filter(|capacity| *capacity <= MAX_CHUNK_QUADS)
.max();
let capacity = largest
.map_or(MIN_CHUNK_QUADS, |largest| {
largest.saturating_mul(2).min(MAX_CHUNK_QUADS)
})
.max(quads);
let mut spans = SpanAllocator::new(capacity);
let first_quad = spans.allocate(quads).unwrap_or_default();
self.chunks.push(Chunk {
id: self.next_chunk,
buffer: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Retained Text Glyph Instances"),
size: instance_offset(capacity),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
spans,
});
self.next_chunk += 1;
(self.chunks.len() - 1, first_quad)
}
}
fn span_quads(span: &StagedSpan) -> u32 {
(span.instances.end - span.instances.start) as u32
}
fn instance_offset(quads: u32) -> u64 {
u64::from(quads) * std::mem::size_of::<GlyphInstance>() as u64
}
#[cfg(test)]
#[path = "tests/glyph_run_arena_tests.rs"]
mod tests;