Skip to main content

cranpose_ui_graphics/
shape_records.rs

1use bytemuck::{Pod, Zeroable};
2
3use crate::ShapeRecord;
4
5/// Shape properties independent of an arc's angles, in instance-buffer layout.
6#[repr(C)]
7#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
8pub struct ShapeRecordBody {
9    /// The stored local rectangle.
10    pub rect: [f32; 4],
11    /// The solid colour or first gradient stop.
12    pub color: [f32; 4],
13    /// The stroke width.
14    pub stroke_width: f32,
15    /// Packed shape, stroke, blend and arc facts from [`ShapeRecord::flags`].
16    pub flags: u32,
17    /// Zero for a solid brush, otherwise one plus its table index.
18    pub brush: u32,
19    /// The placement index when a renderer combines recordings.
20    pub placement: u32,
21    /// Arc centre x and y, followed by normalised inner and outer radii.
22    pub arc_geometry: [f32; 4],
23}
24
25/// Corner radii and arc-angle properties, in instance-buffer layout.
26#[repr(C)]
27#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
28pub struct ShapeRecordCurve {
29    /// Corner radii for rectangles, arc trigonometry for bands.
30    pub radii: [f32; 4],
31    /// Normalised arc start and sweep, then strip start and padded sweep.
32    pub arc_normalized: [f32; 4],
33}
34
35/// Recorded shapes stored in parallel columns, ready for GPU upload.
36///
37/// Angle changes leave the body column intact. Original arc arguments remain
38/// on the CPU so materialisation preserves exactly what the caller supplied.
39#[derive(Clone, Debug, Default, PartialEq)]
40pub struct ShapeRecords {
41    bodies: Vec<ShapeRecordBody>,
42    curves: Vec<ShapeRecordCurve>,
43    sources: Vec<[f32; 4]>,
44}
45
46impl ShapeRecords {
47    pub(crate) fn with_capacity(capacity: usize) -> Self {
48        Self {
49            bodies: Vec::with_capacity(capacity),
50            curves: Vec::with_capacity(capacity),
51            sources: Vec::with_capacity(capacity),
52        }
53    }
54
55    /// The number of recorded shapes.
56    pub fn len(&self) -> usize {
57        self.bodies.len()
58    }
59
60    /// Whether the recording has no shapes.
61    pub fn is_empty(&self) -> bool {
62        self.bodies.is_empty()
63    }
64
65    /// The angle-independent column, directly usable as GPU instance data.
66    pub fn bodies(&self) -> &[ShapeRecordBody] {
67        &self.bodies
68    }
69
70    /// The radius and angle column, directly usable as GPU instance data.
71    pub fn curves(&self) -> &[ShapeRecordCurve] {
72        &self.curves
73    }
74
75    /// Reconstructs one complete record, including the original arc arguments.
76    pub fn get(&self, index: usize) -> Option<ShapeRecord> {
77        self.bodies
78            .get(index)
79            .map(|body| reconstruct(body, &self.curves[index], self.sources[index]))
80    }
81
82    /// Iterates over complete records in draw order without allocating.
83    pub fn iter(&self) -> impl ExactSizeIterator<Item = ShapeRecord> + DoubleEndedIterator + '_ {
84        self.bodies
85            .iter()
86            .zip(&self.curves)
87            .zip(&self.sources)
88            .map(|((body, curve), &source)| reconstruct(body, curve, source))
89    }
90
91    pub(crate) fn capacity(&self) -> usize {
92        self.bodies
93            .capacity()
94            .min(self.curves.capacity())
95            .min(self.sources.capacity())
96    }
97
98    pub(crate) fn heap_bytes(&self) -> usize {
99        self.bodies.capacity() * std::mem::size_of::<ShapeRecordBody>()
100            + self.curves.capacity() * std::mem::size_of::<ShapeRecordCurve>()
101            + self.sources.capacity() * std::mem::size_of::<[f32; 4]>()
102    }
103
104    pub(crate) fn source_bytes(&self) -> &[u8] {
105        bytemuck::cast_slice(&self.sources)
106    }
107
108    pub(crate) fn clear(&mut self) {
109        self.bodies.clear();
110        self.curves.clear();
111        self.sources.clear();
112    }
113
114    pub(crate) fn reserve(&mut self, additional: usize) {
115        self.bodies.reserve(additional);
116        self.curves.reserve(additional);
117        self.sources.reserve(additional);
118    }
119
120    pub(crate) fn push(
121        &mut self,
122        body: ShapeRecordBody,
123        curve: ShapeRecordCurve,
124        source: [f32; 4],
125    ) {
126        self.bodies.push(body);
127        self.curves.push(curve);
128        self.sources.push(source);
129    }
130}
131
132fn reconstruct(body: &ShapeRecordBody, curve: &ShapeRecordCurve, source: [f32; 4]) -> ShapeRecord {
133    ShapeRecord {
134        rect: body.rect,
135        radii: curve.radii,
136        color: body.color,
137        stroke_width: body.stroke_width,
138        flags: body.flags,
139        brush: body.brush,
140        reserved: body.placement,
141        arc: [
142            body.arc_geometry[0],
143            body.arc_geometry[1],
144            source[0],
145            source[1],
146        ],
147        arc_band: [
148            source[2],
149            source[3],
150            body.arc_geometry[2],
151            body.arc_geometry[3],
152        ],
153        arc_normalized: curve.arc_normalized,
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn sample() -> ShapeRecord {
162        ShapeRecord {
163            rect: [1.0, 2.0, 3.0, 4.0],
164            radii: [5.0, 6.0, 7.0, 8.0],
165            color: [0.2, 0.4, 0.6, 0.8],
166            stroke_width: 9.0,
167            flags: 10,
168            brush: 11,
169            reserved: 12,
170            arc: [13.0, 14.0, 15.0, 16.0],
171            arc_band: [17.0, 18.0, 19.0, 20.0],
172            arc_normalized: [21.0, 22.0, 23.0, 24.0],
173        }
174    }
175
176    fn append_sample(records: &mut ShapeRecords) {
177        records.push(
178            ShapeRecordBody {
179                rect: [1.0, 2.0, 3.0, 4.0],
180                color: [0.2, 0.4, 0.6, 0.8],
181                stroke_width: 9.0,
182                flags: 10,
183                brush: 11,
184                placement: 12,
185                arc_geometry: [13.0, 14.0, 19.0, 20.0],
186            },
187            ShapeRecordCurve {
188                radii: [5.0, 6.0, 7.0, 8.0],
189                arc_normalized: [21.0, 22.0, 23.0, 24.0],
190            },
191            [15.0, 16.0, 17.0, 18.0],
192        );
193    }
194
195    #[test]
196    fn columns_preserve_every_record_bit_and_gpu_field() {
197        let record = sample();
198        let mut special = record;
199        special.arc = [
200            f32::NEG_INFINITY,
201            -0.0,
202            f32::from_bits(0x7fc0_0021),
203            f32::INFINITY,
204        ];
205        special.arc_band = [-0.0, f32::from_bits(0xffc0_0001), 1.0, 2.0];
206        let mut records = ShapeRecords::default();
207        assert!(records.is_empty());
208        assert_eq!(records.get(0), None);
209        append_sample(&mut records);
210        let mut body = records.bodies()[0];
211        body.arc_geometry = [f32::NEG_INFINITY, -0.0, 1.0, 2.0];
212        records.push(
213            body,
214            records.curves()[0],
215            [
216                special.arc[2],
217                special.arc[3],
218                special.arc_band[0],
219                special.arc_band[1],
220            ],
221        );
222        assert_eq!(records.len(), 2);
223        assert_eq!(records.iter().len(), 2);
224        for (actual, expected) in records.iter().zip([record, special]) {
225            assert_eq!(bytemuck::bytes_of(&actual), bytemuck::bytes_of(&expected));
226        }
227        assert_eq!(records.get(0), Some(record));
228        assert_eq!(records.get(2), None);
229        assert_eq!(records.iter().rev().nth(1), Some(record));
230        assert_eq!(records.bodies()[0].arc_geometry, [13.0, 14.0, 19.0, 20.0]);
231        assert_eq!(records.curves()[0].radii, record.radii);
232        assert_eq!(records.curves()[0].arc_normalized, record.arc_normalized);
233        assert_eq!(std::mem::size_of::<ShapeRecordBody>(), 64);
234        assert_eq!(std::mem::size_of::<ShapeRecordCurve>(), 32);
235        assert_eq!(
236            &records.source_bytes()[..16],
237            bytemuck::bytes_of(&[15.0f32, 16.0, 17.0, 18.0])
238        );
239    }
240
241    #[test]
242    fn clearing_and_reserving_keep_columns_aligned_without_reallocating() {
243        let mut records = ShapeRecords::with_capacity(4);
244        append_sample(&mut records);
245        let capacity = records.capacity();
246        let bytes = records.heap_bytes();
247        let bodies = records.bodies().as_ptr();
248        let curves = records.curves().as_ptr();
249        records.clear();
250        records.reserve(2);
251        append_sample(&mut records);
252        assert_eq!(records.capacity(), capacity);
253        assert_eq!(records.heap_bytes(), bytes);
254        assert_eq!(records.bodies().as_ptr(), bodies);
255        assert_eq!(records.curves().as_ptr(), curves);
256        assert_eq!(records.len(), records.curves().len());
257        assert_eq!(records.get(0), Some(sample()));
258        assert_eq!(records.clone(), records);
259    }
260}