diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index a1db36b48..3ee2e7ce1 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -1566,5 +1566,16 @@ impl Scene { self.camera_generation += 1; } - pub fn update(&mut self, _dt: Duration) {} + pub fn update(&mut self, _dt: Duration) { + // The glyph atlas grew or re-baked since this scene last built its + // caches (a CJK sheet set bakes thousands of tiles on first sight): + // every quad laid out before that samples the wrong tile — scrambled + // text in the affected blocks / viewports. One structural bump + // re-lays-out all text against the settled atlas. + let atlas_gen = crate::scene::text::sdf_atlas::generation(); + if self.last_atlas_generation.get() != atlas_gen { + self.last_atlas_generation.set(atlas_gen); + self.bump_geometry(); + } + } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e2b373127..4fc66e527 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2348,6 +2348,11 @@ pub struct Scene { /// (re)built, otherwise the held value. `build_primitive` reads it right /// after the call to gate GPU wire re-upload. 0 = none yet. pub(crate) last_model_wire_gen: std::cell::Cell, + /// SDF glyph-atlas generation this scene's caches were built against. + /// When the atlas grows or re-bakes, every cached glyph quad (resident + /// sets, block caches, projected viewport copies) addresses the wrong + /// tile; `update` compares against the live generation and rebuilds. + pub(crate) last_atlas_generation: std::cell::Cell, /// Interaction-LOD state: `camera_generation` seen on the previous frame and /// the wall time it last changed. Used to detect "the view is actively being /// panned / zoomed / orbited" so the expensive per-pixel hatch pass can be @@ -2638,6 +2643,7 @@ impl Scene { last_tess_ms: std::cell::Cell::new(0.0), last_tess_wires: std::cell::Cell::new(0), last_model_wire_gen: std::cell::Cell::new(0), + last_atlas_generation: std::cell::Cell::new(crate::scene::text::sdf_atlas::generation()), nav_last_gen: std::cell::Cell::new(0), nav_changed_at: std::cell::Cell::new(None), nav_perf_pending: std::cell::Cell::new(None), @@ -6105,6 +6111,7 @@ impl Scene { // Build once: full tessellation, no cull, no zoom LOD — the resident // set is zoom-independent (GPU analytical circles/arcs/ellipses). let t_tess = iced::time::Instant::now(); + let mut atlas_gen = crate::scene::text::sdf_atlas::generation(); let mut wires = self.wires_for_block_culled( block, None, @@ -6115,6 +6122,34 @@ impl Scene { all_visible, style_viewport, ); + // Baking a drawing's glyphs for the first time can grow the SDF atlas + // part-way through this pass (a CJK sheet set bakes thousands of + // tiles). Text tessellated before the growth carries UVs that now + // address the wrong tiles — visible as scrambled glyphs — and this + // set is cached by geometry epoch, so nothing would ever re-lay it + // out. The memo guard already folds the generation in, so rebuilding + // once the atlas has settled re-tessellates only the stale text. + for _ in 0..3 { + let now = crate::scene::text::sdf_atlas::generation(); + if now == atlas_gen { + break; + } + atlas_gen = now; + // Block definitions tessellated earlier in this pass hold the + // stale quads too, and their cache is keyed by block epoch (which + // has not moved), so drop them or the rebuild would reuse them. + self.block_defn_cache.borrow_mut().clear(); + wires = self.wires_for_block_culled( + block, + None, + None, + frozen_layers, + anno_scale_override, + annotation_scale_handle, + all_visible, + style_viewport, + ); + } let perf = crate::perf::enabled(); let t_post = perf.then(iced::time::Instant::now); // Synthesized nonprint markers (geo-location daisy) live in model space