1use std::collections::HashMap;
14use std::collections::hash_map::DefaultHasher;
15use std::hash::{Hash, Hasher};
16use std::ops::Range;
17
18use dotloom_geometry::Aabb;
19use dotloom_scene::{SceneDelta, SceneItem};
20
21use crate::color::Theme;
22use crate::gpu::ChunkBuffers;
23use crate::tess::{
24 FillVertex, GlyphInstance, LineInstance, Look, Mesh, item_has_curves, item_has_text, tessellate_item,
25};
26use crate::text::TextSystem;
27
28pub const CHUNK_ITEMS: usize = 256;
30pub const MAX_CHUNK_EXTENT: f64 = 1.0e5;
32
33struct Entry {
34 item: SceneItem,
35 version: u64,
36 mesh: Option<CachedMesh>,
37 curves: bool,
38 text: bool,
39}
40
41struct CachedMesh {
42 mesh: Mesh,
43 lod: i32,
44 text_gen: u64,
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub(crate) struct Run {
50 pub fills: Range<u32>,
52 pub lines: Range<u32>,
54 pub glyphs: Range<u32>,
56}
57
58#[derive(Debug, Default)]
60pub(crate) struct ChunkData {
61 pub origin: [f64; 2],
62 pub lines: Vec<LineInstance>,
63 pub fill_vertices: Vec<FillVertex>,
64 pub fill_indices: Vec<u32>,
65 pub glyphs: Vec<GlyphInstance>,
66 pub runs: Vec<Run>,
67}
68
69pub(crate) struct Chunk {
71 pub ids: Vec<u64>,
72 pub bbox: Aabb,
73 pub unbounded: bool,
75 pub key: u64,
77 pub curves: bool,
78 pub text: bool,
79 pub built: Option<(u64, i32, u64, u64)>,
81 pub data: Option<ChunkData>,
82 pub gpu: ChunkBuffers,
84 pub uploaded: Option<(u64, i32, u64, u64)>,
85}
86
87impl Chunk {
88 fn new(ids: Vec<u64>, key: u64, bbox: Aabb, unbounded: bool, curves: bool, text: bool) -> Self {
89 Self {
90 ids,
91 bbox,
92 unbounded,
93 key,
94 curves,
95 text,
96 built: None,
97 data: None,
98 gpu: ChunkBuffers::default(),
99 uploaded: None,
100 }
101 }
102
103 fn wanted(&self, lod: i32, text_gen: u64, epoch: u64) -> (u64, i32, u64, u64) {
104 (self.key, if self.curves { lod } else { 0 }, if self.text { text_gen } else { 0 }, epoch)
105 }
106}
107
108#[derive(Clone, Copy)]
110pub(crate) struct FrameParams<'a> {
111 pub visible: Aabb,
113 pub margin: f64,
115 pub lod: i32,
117 pub lod_tol: f64,
119 pub theme: &'a Theme,
121}
122
123#[derive(Debug, Default, Clone, Copy)]
125pub(crate) struct PrepareStats {
126 pub rebuilt_chunks: u32,
127 pub tessellated_items: u32,
128}
129
130#[derive(Default)]
132pub struct SceneCache {
133 entries: HashMap<u64, Entry>,
134 order: Vec<u64>,
135 next_version: u64,
136 revision: u64,
137 pub(crate) chunks: Vec<Chunk>,
138 chunk_of: HashMap<u64, usize>,
139 partition_dirty: bool,
140 epoch: u64,
142}
143
144impl std::fmt::Debug for SceneCache {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("SceneCache")
147 .field("items", &self.entries.len())
148 .field("chunks", &self.chunks.len())
149 .field("revision", &self.revision)
150 .finish()
151 }
152}
153
154fn finite_box(b: Aabb) -> Option<Aabb> {
155 (!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then_some(b)
156}
157
158impl SceneCache {
159 #[must_use]
161 pub fn len(&self) -> usize {
162 self.entries.len()
163 }
164
165 #[must_use]
167 pub fn is_empty(&self) -> bool {
168 self.entries.is_empty()
169 }
170
171 #[must_use]
173 pub fn revision(&self) -> u64 {
174 self.revision
175 }
176
177 #[must_use]
179 pub fn item(&self, id: u64) -> Option<&SceneItem> {
180 self.entries.get(&id).map(|e| &e.item)
181 }
182
183 #[must_use]
185 pub fn bounds(&self) -> Aabb {
186 self.entries.values().filter_map(|e| finite_box(e.item.bbox)).fold(Aabb::EMPTY, Aabb::union)
187 }
188
189 pub fn apply(&mut self, delta: SceneDelta) {
191 if delta.reset {
192 self.entries.clear();
193 self.order.clear();
194 self.partition_dirty = true;
195 }
196 for id in &delta.removals {
197 if self.entries.remove(id).is_some() {
198 self.partition_dirty = true;
199 }
200 }
201 for item in delta.upserts {
202 self.next_version += 1;
203 let id = item.id;
204 let curves = item_has_curves(&item);
205 let text = item_has_text(&item);
206 let layer_changed = self.entries.get(&id).is_none_or(|e| e.item.layer != item.layer);
207 if layer_changed {
208 self.partition_dirty = true;
209 }
210 if !self.partition_dirty
212 && let Some(&ci) = self.chunk_of.get(&id)
213 && let Some(c) = self.chunks.get_mut(ci)
214 {
215 c.key = 0;
216 }
217 self.entries.insert(id, Entry { item, version: self.next_version, mesh: None, curves, text });
218 }
219 if let Some(order) = delta.order {
220 self.order = order;
221 self.partition_dirty = true;
222 }
223 if !delta.preview {
224 self.revision = delta.revision;
225 }
226 if !self.partition_dirty && self.chunks.iter().any(|c| c.key == 0) {
227 for ci in 0..self.chunks.len() {
229 if self.chunks[ci].key == 0 {
230 self.refresh_chunk(ci);
231 }
232 }
233 }
234 }
235
236 pub(crate) fn invalidate_all(&mut self) {
238 self.epoch += 1;
239 for e in self.entries.values_mut() {
240 e.mesh = None;
241 }
242 }
243
244 fn refresh_chunk(&mut self, ci: usize) {
245 let Some(c) = self.chunks.get(ci) else { return };
246 let ids = c.ids.clone();
247 let (key, bbox, unbounded, curves, text) = self.describe(&ids);
248 if let Some(c) = self.chunks.get_mut(ci) {
249 c.key = key;
250 c.bbox = bbox;
251 c.unbounded = unbounded;
252 c.curves = curves;
253 c.text = text;
254 }
255 }
256
257 fn describe(&self, ids: &[u64]) -> (u64, Aabb, bool, bool, bool) {
258 let mut h = DefaultHasher::new();
259 let mut bbox = Aabb::EMPTY;
260 let mut unbounded = false;
261 let (mut curves, mut text) = (false, false);
262 for id in ids {
263 id.hash(&mut h);
264 if let Some(e) = self.entries.get(id) {
265 e.version.hash(&mut h);
266 match finite_box(e.item.bbox) {
267 Some(b) => bbox = bbox.union(b),
268 None => unbounded |= !e.item.prims.is_empty(),
269 }
270 curves |= e.curves;
271 text |= e.text;
272 }
273 }
274 (h.finish().max(1), bbox, unbounded, curves, text)
276 }
277
278 fn sorted_ids(&self) -> Vec<u64> {
279 let pos: HashMap<u64, usize> = self.order.iter().enumerate().map(|(i, id)| (*id, i)).collect();
280 let mut ids: Vec<u64> = self.entries.keys().copied().collect();
281 ids.sort_by_key(|id| {
282 let layer = self.entries.get(id).map_or(u32::MAX, |e| e.item.layer);
283 (layer, pos.get(id).copied().unwrap_or(usize::MAX), *id)
284 });
285 ids
286 }
287
288 fn repartition(&mut self) {
289 let ids = self.sorted_ids();
290 let mut groups: Vec<Vec<u64>> = Vec::new();
291 let mut cur: Vec<u64> = Vec::new();
292 let mut cur_box = Aabb::EMPTY;
293 for id in ids {
294 let b = self.entries.get(&id).and_then(|e| finite_box(e.item.bbox));
295 let grown = b.map_or(cur_box, |b| cur_box.union(b));
296 let too_wide = !grown.is_empty() && grown.width().max(grown.height()) > MAX_CHUNK_EXTENT;
297 if !cur.is_empty() && (cur.len() >= CHUNK_ITEMS || too_wide) {
298 groups.push(core::mem::take(&mut cur));
299 cur_box = Aabb::EMPTY;
300 }
301 if let Some(b) = b {
302 cur_box = cur_box.union(b);
303 }
304 cur.push(id);
305 }
306 if !cur.is_empty() {
307 groups.push(cur);
308 }
309 let mut old: HashMap<u64, Chunk> = self.chunks.drain(..).map(|c| (c.key, c)).collect();
310 let mut chunks = Vec::with_capacity(groups.len());
311 for g in groups {
312 let (key, bbox, unbounded, curves, text) = self.describe(&g);
313 match old.remove(&key) {
314 Some(mut c) if c.ids == g => {
315 c.bbox = bbox;
316 c.unbounded = unbounded;
317 chunks.push(c);
318 }
319 _ => chunks.push(Chunk::new(g, key, bbox, unbounded, curves, text)),
320 }
321 }
322 let mut spare: Vec<ChunkBuffers> = old.into_values().map(|c| c.gpu).collect();
324 for c in &mut chunks {
325 if c.uploaded.is_none()
326 && c.gpu.is_empty()
327 && let Some(b) = spare.pop()
328 {
329 c.gpu = b;
330 }
331 }
332 for b in spare {
333 b.destroy();
334 }
335 self.chunk_of.clear();
336 for (ci, c) in chunks.iter().enumerate() {
337 for id in &c.ids {
338 self.chunk_of.insert(*id, ci);
339 }
340 }
341 self.chunks = chunks;
342 self.partition_dirty = false;
343 }
344
345 fn mesh_ok(e: &Entry, lod: i32, text_gen: u64) -> bool {
346 e.mesh.as_ref().is_some_and(|m| (!e.curves || m.lod == lod) && (!e.text || m.text_gen == text_gen))
347 }
348
349 pub(crate) fn prepare(
352 &mut self,
353 fp: &FrameParams<'_>,
354 ts: &mut TextSystem,
355 stats: &mut PrepareStats,
356 ) -> Vec<usize> {
357 let FrameParams { visible, margin, lod, lod_tol, theme } = *fp;
358 if self.partition_dirty {
359 self.repartition();
360 }
361 let view = visible.inflate(margin);
362 let vis: Vec<usize> = (0..self.chunks.len())
363 .filter(|&ci| {
364 let c = &self.chunks[ci];
365 c.unbounded || (!c.bbox.is_empty() && c.bbox.intersects(view))
366 })
367 .collect();
368 for _ in 0..3 {
370 let text_gen = ts.generation;
371 let mut reset = false;
372 for &ci in &vis {
373 let want = self.chunks[ci].wanted(lod, text_gen, self.epoch);
374 if self.chunks[ci].built == Some(want) {
375 continue;
376 }
377 let ids = self.chunks[ci].ids.clone();
378 for id in &ids {
379 let Some(e) = self.entries.get_mut(id) else { continue };
380 if Self::mesh_ok(e, lod, text_gen) {
381 continue;
382 }
383 match tessellate_item(&e.item, Look { flags: e.item.flags }, lod_tol, theme, ts) {
384 Some(mesh) => {
385 e.mesh = Some(CachedMesh { mesh, lod, text_gen });
386 stats.tessellated_items += 1;
387 }
388 None => {
389 reset = true;
390 break;
391 }
392 }
393 }
394 if reset {
395 break;
396 }
397 let data = self.assemble(&ids);
398 let c = &mut self.chunks[ci];
399 c.data = Some(data);
400 c.built = Some(want);
401 stats.rebuilt_chunks += 1;
402 }
403 if !reset {
404 break;
405 }
406 }
407 vis
408 }
409
410 fn assemble(&self, ids: &[u64]) -> ChunkData {
411 let meshes: Vec<(&Mesh, Aabb)> = ids
412 .iter()
413 .filter_map(|id| self.entries.get(id))
414 .filter_map(|e| e.mesh.as_ref().map(|m| (&m.mesh, e.item.bbox)))
415 .collect();
416 let origin = {
417 let b = meshes.iter().filter_map(|(_, b)| finite_box(*b)).fold(Aabb::EMPTY, Aabb::union);
418 if b.is_empty() {
419 meshes.first().map_or([0.0, 0.0], |(m, _)| m.origin)
420 } else {
421 let c = b.center();
422 [c.x, c.y]
423 }
424 };
425 let mut d = ChunkData { origin, ..ChunkData::default() };
426 let mut run = Run::default();
427 let mut line_or_glyph: Vec<Aabb> = Vec::new();
429 let mut glyph_boxes: Vec<Aabb> = Vec::new();
430 let overlaps = |list: &[Aabb], b: Option<Aabb>| match b {
431 Some(b) => list.iter().any(|x| x.intersects(b)),
432 None => !list.is_empty(),
433 };
434 for (m, bbox) in meshes {
435 if m.is_empty() {
436 continue;
437 }
438 let b = finite_box(bbox).map(|b| b.inflate(1e-9));
439 let has_fill = !m.fill_indices.is_empty();
440 let has_line = !m.lines.is_empty();
441 let has_glyph = !m.glyphs.is_empty();
442 let split = (has_fill && overlaps(&line_or_glyph, b)) || (has_line && overlaps(&glyph_boxes, b));
443 if split {
444 d.runs.push(core::mem::take(&mut run));
445 let (f, l, g) = (d.fill_indices.len() as u32, d.lines.len() as u32, d.glyphs.len() as u32);
446 run = Run { fills: f..f, lines: l..l, glyphs: g..g };
447 line_or_glyph.clear();
448 glyph_boxes.clear();
449 }
450 let off = [(m.origin[0] - origin[0]) as f32, (m.origin[1] - origin[1]) as f32];
451 let add = |p: [f32; 2]| [p[0] + off[0], p[1] + off[1]];
452 let base = d.fill_vertices.len() as u32;
453 d.fill_vertices.extend(m.fill_vertices.iter().map(|v| FillVertex { pos: add(v.pos), ..*v }));
454 d.fill_indices.extend(m.fill_indices.iter().map(|i| i + base));
455 d.lines.extend(m.lines.iter().map(|l| LineInstance { p0: add(l.p0), p1: add(l.p1), ..*l }));
456 d.glyphs.extend(m.glyphs.iter().map(|g| GlyphInstance { origin: add(g.origin), ..*g }));
457 run.fills.end = d.fill_indices.len() as u32;
458 run.lines.end = d.lines.len() as u32;
459 run.glyphs.end = d.glyphs.len() as u32;
460 if let Some(b) = b {
461 if has_line || has_glyph {
462 line_or_glyph.push(b);
463 }
464 if has_glyph {
465 glyph_boxes.push(b);
466 }
467 } else {
468 if has_line || has_glyph {
470 line_or_glyph.push(Aabb::from_corners(
471 dotloom_geometry::Point::new(f64::MIN, f64::MIN),
472 dotloom_geometry::Point::new(f64::MAX, f64::MAX),
473 ));
474 }
475 }
476 }
477 if run != Run::default() {
478 d.runs.push(run);
479 }
480 d.runs.retain(|r| !(r.fills.is_empty() && r.lines.is_empty() && r.glyphs.is_empty()));
481 d
482 }
483
484 pub(crate) fn destroy_gpu(&mut self) {
486 for c in &mut self.chunks {
487 core::mem::take(&mut c.gpu).destroy();
488 c.uploaded = None;
489 }
490 }
491
492 #[must_use]
494 pub fn draw_order(&mut self) -> Vec<u64> {
495 if self.partition_dirty {
496 self.repartition();
497 }
498 self.chunks.iter().flat_map(|c| c.ids.iter().copied()).collect()
499 }
500
501 #[cfg(test)]
503 pub(crate) fn chunk_ids(&mut self) -> Vec<Vec<u64>> {
504 if self.partition_dirty {
505 self.repartition();
506 }
507 self.chunks.iter().map(|c| c.ids.clone()).collect()
508 }
509
510 #[cfg(test)]
512 pub(crate) fn keys(&self) -> std::collections::HashSet<u64> {
513 self.chunks.iter().map(|c| c.key).collect()
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use dotloom_geometry::{Point, Rect, Segment, Shape};
520 use dotloom_scene::{Primitive, Stroke};
521
522 use super::*;
523
524 fn line_item(id: u64, x: f64, layer: u32) -> SceneItem {
525 let shape = Shape::Line(Segment::new(Point::new(x, 0.0), Point::new(x + 1.0, 0.0)));
526 SceneItem {
527 id,
528 layer,
529 bbox: shape.bbox(),
530 flags: 0,
531 prims: vec![Primitive::Shape {
532 shape,
533 stroke: Some(Stroke { color: 0, width: 1.0, dash: vec![] }),
534 fill: None,
535 }],
536 }
537 }
538
539 fn rect_item(id: u64, x: f64, fill: bool, stroke: bool) -> SceneItem {
540 let shape = Shape::Rect(Rect { origin: Point::new(x, 0.0), width: 10.0, height: 10.0 });
541 SceneItem {
542 id,
543 layer: 0,
544 bbox: shape.bbox(),
545 flags: 0,
546 prims: vec![Primitive::Shape {
547 shape,
548 stroke: stroke.then(|| Stroke { color: 0, width: 1.0, dash: vec![] }),
549 fill: fill.then_some(0xff00_00ff),
550 }],
551 }
552 }
553
554 fn delta(upserts: Vec<SceneItem>, order: Option<Vec<u64>>) -> SceneDelta {
555 SceneDelta { revision: 1, preview: false, reset: false, upserts, removals: vec![], order }
556 }
557
558 fn prepare(c: &mut SceneCache) -> Vec<usize> {
559 let mut ts = TextSystem::with_default_font().unwrap();
560 let mut st = PrepareStats::default();
561 let all = Aabb::from_corners(Point::new(-1e7, -1e7), Point::new(1e7, 1e7));
562 c.prepare(
563 &FrameParams { visible: all, margin: 0.0, lod: 0, lod_tol: 0.1, theme: &Theme::light() },
564 &mut ts,
565 &mut st,
566 )
567 }
568
569 #[test]
570 fn order_follows_layer_then_scene_order() {
571 let mut c = SceneCache::default();
572 c.apply(delta(vec![line_item(1, 0.0, 1), line_item(2, 0.0, 0), line_item(3, 0.0, 0)], Some(vec![1, 3, 2])));
573 assert_eq!(c.draw_order(), vec![3, 2, 1]);
574 }
575
576 #[test]
577 fn chunks_split_by_count_and_reuse_unchanged_chunks() {
578 let mut c = SceneCache::default();
579 let n = CHUNK_ITEMS as u64 * 2 + 10;
580 let items: Vec<SceneItem> = (1..=n).map(|i| line_item(i, i as f64, 0)).collect();
581 c.apply(delta(items, Some((1..=n).collect())));
582 let ids = c.chunk_ids();
583 assert_eq!(ids.len(), 3);
584 let before = c.keys();
585 c.apply(delta(vec![line_item(n + 1, 0.0, 0)], Some((1..=n + 1).collect())));
587 c.chunk_ids();
588 let after = c.keys();
589 assert_eq!(before.intersection(&after).count(), 2);
590 c.apply(delta(vec![line_item(5, 99.0, 0)], None));
592 let changed = c.keys();
593 assert_eq!(after.intersection(&changed).count(), 2);
594 }
595
596 #[test]
597 fn far_apart_items_get_separate_chunks() {
598 let mut c = SceneCache::default();
599 c.apply(delta(vec![line_item(1, 0.0, 0), line_item(2, 1.0e6, 0)], Some(vec![1, 2])));
600 assert_eq!(c.chunk_ids().len(), 2);
601 }
602
603 #[test]
604 fn runs_preserve_stacking() {
605 let mut c = SceneCache::default();
606 c.apply(delta(
609 vec![rect_item(1, 0.0, false, true), rect_item(2, 5.0, true, false), rect_item(3, 100.0, true, false)],
610 Some(vec![1, 2, 3]),
611 ));
612 let vis = prepare(&mut c);
613 assert_eq!(vis, vec![0]);
614 let d = c.chunks[0].data.as_ref().unwrap();
615 assert_eq!(d.runs.len(), 2, "{:?}", d.runs);
616 assert_eq!(d.runs[0].lines.len(), 4);
617 assert!(d.runs[0].fills.is_empty());
618 assert_eq!(d.runs[1].fills.len(), 12);
619 }
620
621 #[test]
622 fn culling_and_lod_rebuilds() {
623 let mut c = SceneCache::default();
624 let circle = Shape::Circle(dotloom_geometry::Circle::new(Point::new(0.0, 0.0), 50.0).unwrap());
625 let it = SceneItem {
626 id: 1,
627 layer: 0,
628 bbox: circle.bbox(),
629 flags: 0,
630 prims: vec![Primitive::Shape {
631 shape: circle,
632 stroke: Some(Stroke { color: 0, width: 1.0, dash: vec![] }),
633 fill: None,
634 }],
635 };
636 c.apply(delta(vec![it, line_item(2, 1.0e6, 0)], Some(vec![1, 2])));
637 let mut ts = TextSystem::with_default_font().unwrap();
638 let mut st = PrepareStats::default();
639 let view = Aabb::from_corners(Point::new(-100.0, -100.0), Point::new(100.0, 100.0));
640 let vis = c.prepare(
641 &FrameParams { visible: view, margin: 0.0, lod: 0, lod_tol: 0.25, theme: &Theme::light() },
642 &mut ts,
643 &mut st,
644 );
645 assert_eq!(vis.len(), 1, "far chunk culled");
646 assert_eq!(st.tessellated_items, 1);
647 let n0 = c.chunks[vis[0]].data.as_ref().unwrap().lines.len();
648 let mut st2 = PrepareStats::default();
650 c.prepare(
651 &FrameParams { visible: view, margin: 0.0, lod: 0, lod_tol: 0.25, theme: &Theme::light() },
652 &mut ts,
653 &mut st2,
654 );
655 assert_eq!(st2.rebuilt_chunks, 0);
656 let mut st3 = PrepareStats::default();
658 c.prepare(
659 &FrameParams { visible: view, margin: 0.0, lod: 3, lod_tol: 0.25 / 8.0, theme: &Theme::light() },
660 &mut ts,
661 &mut st3,
662 );
663 assert_eq!(st3.rebuilt_chunks, 1);
664 assert!(c.chunks[vis[0]].data.as_ref().unwrap().lines.len() > n0 * 2);
665 }
666
667 #[test]
668 fn removal_and_reset() {
669 let mut c = SceneCache::default();
670 c.apply(delta(vec![line_item(1, 0.0, 0), line_item(2, 0.0, 0)], Some(vec![1, 2])));
671 c.apply(SceneDelta { revision: 2, removals: vec![1], order: Some(vec![2]), ..SceneDelta::default() });
672 assert_eq!(c.draw_order(), vec![2]);
673 assert_eq!(c.revision(), 2);
674 c.apply(SceneDelta { revision: 3, reset: true, ..SceneDelta::default() });
675 assert!(c.is_empty());
676 assert!(c.draw_order().is_empty());
677 }
678}