iris: a cleared glyph atlas must un-cache every RenderedText, not just empty itself
Iris's phone, 2026-09-06 22:16: after leaving the app and returning, every glyph drawn *before* the resume came back as fragments of other letters, while the diagnostics text drawn after it was perfect. The renderer rebuild does force a full redraw -- `surface_changed` calls `render.resize(...)`, which sets `UiRenderState::resized`, which makes the next `update` take `redraw_all`. What survives that is one cache further in: `TextView::render` returns its cached `RenderedText` whenever the wrap width, buffer and attrs are unchanged, so `TextData::place` is never reached, nothing is re-rasterised into the fresh atlas, and the *previous* atlas's uv_min/uv_max/layer go straight back to the GPU. Only text whose content changed after the resume re-shapes -- exactly the split in the screenshot. One mechanism rather than a per-holder invalidation path: `GlyphAtlas` carries a `generation`, bumped by `clear`; a `RenderedText` records the one it was placed against; and `TextView::render`'s cache key includes it, so clearing the atlas makes every cached render un-reusable at once. `Painter::glyphs` debug-asserts that a submitted quad's generation is the live one, catching the fault at the submission instead of on screen. Test `clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it` (iris/src/widget/text/mod.rs): draw, clear the atlas, resize, draw again, and assert the atlas holds the same glyph count. Confirmed to fail without the cache-key line -- it trips the new debug_assert with "glyphs placed against atlas generation 0 submitted against 1". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
10267dec27
commit
ba2afbaedb
4 files changed
+108
No files matched your search
@@ -601,6 +601,11 @@ pub struct RenderedText {
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
|
||||
/// A holder must re-render rather than re-emit these quads once the
|
||||
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens
|
||||
/// otherwise); `Painter::glyphs` debug-asserts it.
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
@@ -619,6 +624,7 @@ impl TextData {
|
||||
glyphs: std::sync::Arc::new(glyphs),
|
||||
size: buffer.size(),
|
||||
color: attrs.color,
|
||||
generation: self.atlas.generation(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,10 @@ struct Page {
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// Bumped by [`GlyphAtlas::clear`], so anything holding placed glyphs
|
||||
/// from an earlier atlas can tell that its coordinates are stale --
|
||||
/// see that method's doc for what goes wrong without it.
|
||||
generation: u64,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
@@ -166,6 +170,13 @@ impl GlyphAtlas {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
/// Which atlas the entries handed out right now belong to. A
|
||||
/// [`crate::RenderedText`] records this when it is built and is only
|
||||
/// reusable while it still matches.
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pages.len()
|
||||
}
|
||||
@@ -188,9 +199,20 @@ impl GlyphAtlas {
|
||||
/// new. Dropping `pages` also drops its `TextureHandle`s, which send a
|
||||
/// free message back through their `Textures`; see `Textures::reset`'s
|
||||
/// doc for why that is harmless here.
|
||||
/// Bumping `generation` here is the other half of the same
|
||||
/// invalidation: emptying this atlas does nothing about the
|
||||
/// `RenderedText`s widgets are *already holding*
|
||||
/// (`iris::widget::TextView`'s `tex` cache), whose `PlacedGlyph`s carry
|
||||
/// `uv_min`/`uv_max`/`layer` into the atlas that has just been thrown
|
||||
/// away. Those redraw perfectly happily and sample whatever now sits at
|
||||
/// those coordinates -- the fragments-of-other-glyphs Iris photographed
|
||||
/// after resuming the app on 2026-09-06. One counter, checked where the
|
||||
/// cache is read, is what makes a cached render un-reusable across a
|
||||
/// renderer rebuild.
|
||||
pub fn clear(&mut self) {
|
||||
self.pages.clear();
|
||||
self.entries.clear();
|
||||
self.generation += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,12 +200,31 @@ impl<'a> Painter<'a> {
|
||||
.render(buffer, attrs, width, &mut ui.textures, density)
|
||||
}
|
||||
|
||||
/// Which glyph atlas the glyphs handed out right now belong to --
|
||||
/// what a widget caching a [`RenderedText`] across frames has to
|
||||
/// compare against before re-emitting it (`GlyphAtlas::clear`).
|
||||
pub fn atlas_generation(&mut self) -> u64 {
|
||||
self.rsc.ui_mut().text.atlas.generation()
|
||||
}
|
||||
|
||||
/// Draw a laid-out string: one quad per glyph, all sampling the atlas.
|
||||
///
|
||||
/// `origin` is where the text's top-left goes; every glyph is placed at an
|
||||
/// absolute pixel offset from it, so re-drawing after a resize is this loop
|
||||
/// and nothing else.
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
// A caller re-emitting quads placed against an atlas that has since
|
||||
// been cleared draws every glyph from coordinates now holding
|
||||
// something else. Caught at the submission rather than on screen,
|
||||
// where it reads as fragments of unrelated letters.
|
||||
debug_assert_eq!(
|
||||
text.generation,
|
||||
self.atlas_generation(),
|
||||
"glyphs placed against atlas generation {} submitted against {}: the holder did not \
|
||||
re-render after the atlas was cleared",
|
||||
text.generation,
|
||||
self.atlas_generation(),
|
||||
);
|
||||
let flags_for = |is_color| {
|
||||
if is_color {
|
||||
GlyphPrimitive::IS_COLOR
|
||||
|
||||
Reference in new issue
Block a user