Skip to main content

concinnity_render/
call_buffer.rs

1//! Assembly buffer for an overlay draw list: the calls built so far plus a
2//! pool of spent vertex/index buffers. A frame's spent list is recycled back
3//! in whole, so steady-state assembly reuses both the list and every call's
4//! geometry allocations.
5
6use crate::render_types::{TextDrawCall, TextVertex};
7use alloc::vec::Vec;
8
9#[derive(Default)]
10/// An overlay draw list under assembly, with its recycled geometry pool.
11pub struct TextCallBuffer {
12    /// Calls assembled so far, in draw order.
13    pub calls: Vec<TextDrawCall>,
14    // Spent geometry buffers (cleared, capacity intact) awaiting reuse.
15    spare: Vec<(Vec<TextVertex>, Vec<u16>)>,
16    // Index scratch for the layer sort, reused across frames.
17    sort_order: Vec<u32>,
18    sort_dest: Vec<u32>,
19}
20
21impl TextCallBuffer {
22    /// Reclaim a spent draw list: each call's geometry feeds later builds and
23    /// the drained list becomes the backing for the next `calls`.
24    pub fn recycle(&mut self, mut spent: Vec<TextDrawCall>) {
25        debug_assert!(self.calls.is_empty(), "recycling over an untaken build");
26        for call in spent.drain(..) {
27            let (mut vertices, mut indices) = (call.vertices, call.indices);
28            vertices.clear();
29            indices.clear();
30            self.spare.push((vertices, indices));
31        }
32        self.calls = spent;
33    }
34
35    /// An empty vertex/index buffer pair, reusing spent capacity when any is
36    /// pooled.
37    pub fn geometry(&mut self) -> (Vec<TextVertex>, Vec<u16>) {
38        self.spare.pop().unwrap_or_default()
39    }
40
41    /// Return a pair taken with [`TextCallBuffer::geometry`] that ended up
42    /// unused, so its capacity stays pooled.
43    pub fn park(&mut self, mut vertices: Vec<TextVertex>, mut indices: Vec<u16>) {
44        vertices.clear();
45        indices.clear();
46        self.spare.push((vertices, indices));
47    }
48
49    /// Hand the assembled list to its consumer, leaving this buffer empty.
50    pub fn take(&mut self) -> Vec<TextDrawCall> {
51        core::mem::take(&mut self.calls)
52    }
53
54    /// Reorder the assembled calls by ascending draw layer, keeping same-layer
55    /// calls in insertion order. Equivalent to a stable sort by layer, but the
56    /// sort runs over indices in persistent scratch and the permutation is
57    /// applied with swaps, so a steady-state frame allocates nothing. Skipped
58    /// entirely when every call sits at layer 0.
59    pub fn sort_by_layer(&mut self) {
60        if self.calls.iter().all(|c| c.layer == 0) {
61            return;
62        }
63        let calls = &mut self.calls;
64        // `order[new] = old`: ties broken by index, so equal layers keep their
65        // insertion order.
66        let order = &mut self.sort_order;
67        order.clear();
68        order.extend(0..calls.len() as u32);
69        order.sort_unstable_by_key(|&i| (calls[i as usize].layer, i));
70        // Invert to `dest[old] = new`, then walk each swap cycle in place.
71        let dest = &mut self.sort_dest;
72        dest.clear();
73        dest.resize(calls.len(), 0);
74        for (new, &old) in order.iter().enumerate() {
75            dest[old as usize] = new as u32;
76        }
77        for i in 0..calls.len() {
78            while dest[i] as usize != i {
79                let j = dest[i] as usize;
80                calls.swap(i, j);
81                dest.swap(i, j);
82            }
83        }
84    }
85}
86
87// TextDrawCall carries no Debug; systems holding a buffer still derive it.
88impl core::fmt::Debug for TextCallBuffer {
89    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90        f.debug_struct("TextCallBuffer")
91            .field("calls", &self.calls.len())
92            .field("spare", &self.spare.len())
93            .finish()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    use alloc::vec;
102    fn call(quads: usize) -> TextDrawCall {
103        TextDrawCall {
104            vertices: Vec::with_capacity(4 * quads),
105            indices: Vec::with_capacity(6 * quads),
106            atlas_slot: 0,
107            clip_rect: None,
108            layer: 0,
109        }
110    }
111
112    #[test]
113    fn recycle_reuses_the_list_and_its_geometry() {
114        let mut buf = TextCallBuffer::default();
115        let mut spent = vec![call(8), call(4)];
116        spent[0].vertices.push(TextVertex {
117            pos: [0.0, 0.0],
118            uv: [0.0, 0.0],
119            color: [0.0; 3],
120            mode: 0.0,
121        });
122        let list_ptr = spent.as_ptr();
123        let geom_cap = spent[1].vertices.capacity();
124
125        buf.recycle(spent);
126        assert_eq!(buf.calls.as_ptr(), list_ptr, "list backing reused");
127        assert!(buf.calls.is_empty());
128
129        // Pooled buffers come back cleared with their capacity intact.
130        let (vertices, indices) = buf.geometry();
131        assert!(vertices.is_empty() && indices.is_empty());
132        assert!(vertices.capacity() >= geom_cap);
133        let (second, _) = buf.geometry();
134        assert!(second.is_empty(), "the pushed vertex was cleared");
135        assert!(second.capacity() >= 32);
136        // The pool is drained; further requests allocate fresh.
137        assert_eq!(buf.geometry().0.capacity(), 0);
138    }
139
140    #[test]
141    fn take_leaves_an_empty_buffer() {
142        let mut buf = TextCallBuffer::default();
143        buf.calls.push(call(1));
144        assert_eq!(buf.take().len(), 1);
145        assert!(buf.calls.is_empty());
146    }
147
148    // A call tagged through `atlas_slot` so a sort's reordering is observable.
149    fn layered(layer: i32, tag: usize) -> TextDrawCall {
150        TextDrawCall {
151            layer,
152            atlas_slot: tag,
153            ..call(0)
154        }
155    }
156
157    #[test]
158    fn sort_by_layer_orders_ascending_and_keeps_same_layer_insertion_order() {
159        let mut buf = TextCallBuffer::default();
160        for (layer, tag) in [(2, 0), (0, 1), (1, 2), (0, 3), (2, 4)] {
161            buf.calls.push(layered(layer, tag));
162        }
163        buf.sort_by_layer();
164        let got: Vec<(i32, usize)> = buf.calls.iter().map(|c| (c.layer, c.atlas_slot)).collect();
165        assert_eq!(got, [(0, 1), (0, 3), (1, 2), (2, 0), (2, 4)]);
166    }
167
168    #[test]
169    fn sort_by_layer_leaves_an_unlayered_list_untouched() {
170        let mut buf = TextCallBuffer::default();
171        for tag in 0..4 {
172            buf.calls.push(layered(0, tag));
173        }
174        buf.sort_by_layer();
175        let tags: Vec<usize> = buf.calls.iter().map(|c| c.atlas_slot).collect();
176        assert_eq!(tags, [0, 1, 2, 3]);
177    }
178
179    // The scratch is reused across sorts of different lengths, so a second sort
180    // after a shorter frame still permutes correctly.
181    #[test]
182    fn sort_by_layer_is_correct_across_reuse() {
183        let mut buf = TextCallBuffer::default();
184        for (layer, tag) in [(3, 0), (1, 1), (2, 2)] {
185            buf.calls.push(layered(layer, tag));
186        }
187        buf.sort_by_layer();
188        buf.calls.clear();
189        for (layer, tag) in [(5, 0), (4, 1)] {
190            buf.calls.push(layered(layer, tag));
191        }
192        buf.sort_by_layer();
193        let got: Vec<(i32, usize)> = buf.calls.iter().map(|c| (c.layer, c.atlas_slot)).collect();
194        assert_eq!(got, [(4, 1), (5, 0)]);
195    }
196}