1use glam::{Vec2, Vec3, Vec4};
15use std::collections::HashMap;
16use crate::glyph::RenderLayer;
17
18#[repr(C)]
34#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
35pub struct GlyphInstance {
36 pub position: [f32; 3],
37 pub scale: [f32; 2],
38 pub rotation: f32,
39 pub color: [f32; 4],
40 pub emission: f32,
41 pub glow_color: [f32; 3],
42 pub glow_radius: f32,
43 pub uv_offset: [f32; 2],
44 pub uv_size: [f32; 2],
45 pub _pad: [f32; 2],
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub enum BlendMode {
55 Alpha,
57 Additive,
59 Multiply,
61 Screen,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct BatchKey {
70 pub layer: RenderLayerOrd,
71 pub blend: BlendMode,
72 pub atlas_page: u8,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
78pub struct RenderLayerOrd(pub u8);
79
80impl RenderLayerOrd {
81 pub fn from_layer(layer: RenderLayer) -> Self {
82 Self(match layer {
83 RenderLayer::Background => 0,
84 RenderLayer::World => 1,
85 RenderLayer::Entity => 2,
86 RenderLayer::Particle => 3,
87 RenderLayer::Overlay => 4,
88 RenderLayer::UI => 5,
89 })
90 }
91}
92
93impl BatchKey {
94 pub fn new(layer: RenderLayer, blend: BlendMode, atlas_page: u8) -> Self {
95 Self { layer: RenderLayerOrd::from_layer(layer), blend, atlas_page }
96 }
97
98 pub fn default_for_layer(layer: RenderLayer) -> Self {
99 let blend = match layer {
100 RenderLayer::Overlay | RenderLayer::Particle => BlendMode::Additive,
101 _ => BlendMode::Alpha,
102 };
103 Self::new(layer, blend, 0)
104 }
105}
106
107impl PartialOrd for BatchKey {
108 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
109 Some(self.cmp(other))
110 }
111}
112
113impl Ord for BatchKey {
114 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115 self.layer.cmp(&other.layer)
116 .then(self.blend.cmp(&other.blend))
117 .then(self.atlas_page.cmp(&other.atlas_page))
118 }
119}
120
121#[derive(Debug)]
125pub struct GlyphBatch {
126 pub key: BatchKey,
127 pub instances: Vec<GlyphInstance>,
128}
129
130impl GlyphBatch {
131 pub fn new(key: BatchKey) -> Self {
132 Self { key, instances: Vec::with_capacity(64) }
133 }
134
135 pub fn clear(&mut self) { self.instances.clear(); }
136
137 pub fn push(&mut self, inst: GlyphInstance) { self.instances.push(inst); }
138
139 pub fn len(&self) -> usize { self.instances.len() }
140 pub fn is_empty(&self) -> bool { self.instances.is_empty() }
141
142 pub fn as_bytes(&self) -> &[u8] {
144 bytemuck::cast_slice(&self.instances)
145 }
146
147 pub fn sort_back_to_front(&mut self) {
149 self.instances.sort_by(|a, b| {
150 b.position[2].partial_cmp(&a.position[2]).unwrap_or(std::cmp::Ordering::Equal)
151 });
152 }
153
154 pub fn sort_front_to_back(&mut self) {
156 self.instances.sort_by(|a, b| {
157 a.position[2].partial_cmp(&b.position[2]).unwrap_or(std::cmp::Ordering::Equal)
158 });
159 }
160}
161
162pub struct PendingGlyph {
166 pub key: BatchKey,
167 pub instance: GlyphInstance,
168 pub depth: f32, }
170
171pub struct GlyphBatcher {
181 pending: Vec<PendingGlyph>,
182 batches: Vec<GlyphBatch>,
183 stats: BatchStats,
184}
185
186#[derive(Default, Debug, Clone)]
188pub struct BatchStats {
189 pub total_glyphs: usize,
190 pub batch_count: usize,
191 pub alpha_glyphs: usize,
192 pub additive_glyphs: usize,
193}
194
195impl GlyphBatcher {
196 pub fn new() -> Self {
197 Self {
198 pending: Vec::with_capacity(4096),
199 batches: Vec::with_capacity(16),
200 stats: BatchStats::default(),
201 }
202 }
203
204 pub fn begin(&mut self) {
206 self.pending.clear();
207 self.stats = BatchStats::default();
208 }
209
210 pub fn push(&mut self, key: BatchKey, instance: GlyphInstance, depth: f32) {
212 self.pending.push(PendingGlyph { key, instance, depth });
213 }
214
215 pub fn push_default(&mut self, layer: RenderLayer, instance: GlyphInstance, depth: f32) {
217 let key = BatchKey::default_for_layer(layer);
218 self.push(key, instance, depth);
219 }
220
221 pub fn finish(&mut self) {
223 self.stats.total_glyphs = self.pending.len();
224
225 self.pending.sort_by(|a, b| {
227 a.key.cmp(&b.key)
228 .then(b.depth.partial_cmp(&a.depth).unwrap_or(std::cmp::Ordering::Equal))
229 });
230
231 self.batches.clear();
233 let mut current_key: Option<BatchKey> = None;
234
235 for item in &self.pending {
236 match &item.blend_type() {
237 BlendMode::Alpha | BlendMode::Multiply | BlendMode::Screen => {
238 self.stats.alpha_glyphs += 1;
239 }
240 BlendMode::Additive => {
241 self.stats.additive_glyphs += 1;
242 }
243 }
244
245 if current_key != Some(item.key) {
246 self.batches.push(GlyphBatch::new(item.key));
247 current_key = Some(item.key);
248 }
249
250 self.batches.last_mut().unwrap().instances.push(item.instance);
251 }
252
253 self.stats.batch_count = self.batches.len();
254 }
255
256 pub fn batches(&self) -> &[GlyphBatch] {
258 &self.batches
259 }
260
261 pub fn stats(&self) -> &BatchStats { &self.stats }
263
264 pub fn instance_count(&self) -> usize {
266 self.batches.iter().map(|b| b.len()).sum()
267 }
268}
269
270impl PendingGlyph {
271 fn blend_type(&self) -> BlendMode { self.key.blend }
272}
273
274impl Default for GlyphBatcher {
275 fn default() -> Self { Self::new() }
276}
277
278impl GlyphInstance {
281 pub fn build(
283 position: Vec3,
284 scale: Vec2,
285 rotation: f32,
286 color: Vec4,
287 emission: f32,
288 glow_color: Vec3,
289 glow_radius: f32,
290 uv_offset: Vec2,
291 uv_size: Vec2,
292 ) -> Self {
293 Self {
294 position: position.into(),
295 scale: scale.into(),
296 rotation,
297 color: color.into(),
298 emission,
299 glow_color: glow_color.into(),
300 glow_radius,
301 uv_offset: uv_offset.into(),
302 uv_size: uv_size.into(),
303 _pad: [0.0; 2],
304 }
305 }
306
307 pub fn simple(position: Vec3, uv_offset: Vec2, uv_size: Vec2) -> Self {
309 Self::build(
310 position,
311 Vec2::ONE,
312 0.0,
313 Vec4::ONE,
314 0.0,
315 Vec3::ZERO,
316 0.0,
317 uv_offset,
318 uv_size,
319 )
320 }
321
322 pub fn glowing(position: Vec3, color: Vec4, emission: f32, glow_radius: f32,
324 uv_offset: Vec2, uv_size: Vec2) -> Self {
325 Self::build(
326 position,
327 Vec2::ONE,
328 0.0,
329 color,
330 emission,
331 Vec3::new(color.x, color.y, color.z),
332 glow_radius,
333 uv_offset,
334 uv_size,
335 )
336 }
337}
338
339#[derive(Default)]
343pub struct AtlasUploadTracker {
344 dirty_pages: std::collections::HashSet<u8>,
345}
346
347impl AtlasUploadTracker {
348 pub fn mark_dirty(&mut self, page: u8) { self.dirty_pages.insert(page); }
349 pub fn is_dirty(&self, page: u8) -> bool { self.dirty_pages.contains(&page) }
350 pub fn clear(&mut self) { self.dirty_pages.clear(); }
351 pub fn dirty_pages(&self) -> impl Iterator<Item = u8> + '_ {
352 self.dirty_pages.iter().copied()
353 }
354}
355
356#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn make_instance(z: f32) -> GlyphInstance {
363 GlyphInstance::simple(Vec3::new(0.0, 0.0, z), Vec2::ZERO, Vec2::new(0.1, 0.1))
364 }
365
366 #[test]
367 fn batcher_groups_by_key() {
368 let mut batcher = GlyphBatcher::new();
369 batcher.begin();
370
371 let world_key = BatchKey::new(RenderLayer::World, BlendMode::Alpha, 0);
372 let ui_key = BatchKey::new(RenderLayer::UI, BlendMode::Alpha, 0);
373
374 batcher.push(world_key, make_instance(0.0), 0.0);
375 batcher.push(world_key, make_instance(1.0), 1.0);
376 batcher.push(ui_key, make_instance(0.0), 0.0);
377
378 batcher.finish();
379
380 assert_eq!(batcher.batches().len(), 2, "Expected 2 batches");
381 assert_eq!(batcher.instance_count(), 3);
382 }
383
384 #[test]
385 fn batches_sorted_by_layer() {
386 let mut batcher = GlyphBatcher::new();
387 batcher.begin();
388
389 batcher.push_default(RenderLayer::UI, make_instance(0.0), 0.0);
390 batcher.push_default(RenderLayer::World, make_instance(0.0), 0.0);
391
392 batcher.finish();
393
394 let batches = batcher.batches();
395 assert!(batches[0].key.layer < batches[1].key.layer);
397 }
398
399 #[test]
400 fn stats_correct() {
401 let mut batcher = GlyphBatcher::new();
402 batcher.begin();
403 batcher.push_default(RenderLayer::World, make_instance(0.0), 0.0);
404 batcher.push_default(RenderLayer::World, make_instance(1.0), 1.0);
405 batcher.push_default(RenderLayer::Particle, make_instance(0.0), 0.0);
406 batcher.finish();
407
408 assert_eq!(batcher.stats().total_glyphs, 3);
409 }
410
411 #[test]
412 fn glyph_instance_size_is_84() {
413 assert_eq!(std::mem::size_of::<GlyphInstance>(), 84);
414 }
415
416 #[test]
417 fn sort_back_to_front_orders_by_descending_z() {
418 let key = BatchKey::new(RenderLayer::World, BlendMode::Alpha, 0);
419 let mut batch = GlyphBatch::new(key);
420 batch.push(make_instance(0.0));
421 batch.push(make_instance(5.0));
422 batch.push(make_instance(2.0));
423 batch.sort_back_to_front();
424
425 let zs: Vec<f32> = batch.instances.iter().map(|i| i.position[2]).collect();
426 assert!(zs[0] >= zs[1] && zs[1] >= zs[2]);
427 }
428}