From 42e89b347d7a356cca8f633d5a23194ef2cdb3ea Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 22 Oct 2026 19:20:07 +0500 Subject: [PATCH] Thumbnails: count the renders, not only the thumbnails held (#969) Follow-up to #1011, as asked on #1038: a counter of thumbnails actually rendered, and tests that use it. `Textures::thumb_count` is the number of thumbnails *held*. It settles at the rows in view whether the pane rendered them once or a thousand times, so it cannot see a pane that renders the same rows over and over -- which is what #969 was: the flashing white was the rendering, not the holding. `Textures::thumb_renders` counts the renders, and a settled pane is one that has stopped asking for them. The two new tests assert what the held count cannot: - every thumbnail held was rendered, and not many more than that were rendered and thrown away (measured: 8 held, 8 rendered at rest); - ten more frames of the same view render nothing at all; - after a 40,000 pt scroll of an 800-slide deck, 24 renders in total -- the rows it landed on and the ones it passed over, not the deck -- and then it settles again. With the cache made to thrash (its 600-entry limit cut to 4, so the rows in view are evicted and re-rendered every frame), the merged test for the Slide Sorter still passes -- the held count settles at 4 -- while this one fails with "255 renders for 4 thumbnails". That is the hole the counter closes. --- crates/ui-egui/src/huge_deck_tests.rs | 57 +++++++++++++++++++++++++++ crates/ui-egui/src/textures.rs | 11 ++++++ 2 files changed, 68 insertions(+) diff --git a/crates/ui-egui/src/huge_deck_tests.rs b/crates/ui-egui/src/huge_deck_tests.rs index 2ff81bdf..6652de77 100644 --- a/crates/ui-egui/src/huge_deck_tests.rs +++ b/crates/ui-egui/src/huge_deck_tests.rs @@ -74,3 +74,60 @@ fn the_sorter_of_a_huge_deck_renders_only_the_slides_in_view() { h.run_steps(10); assert_eq!(h.state().textures.thumb_count(), settled); } + +/// What the count of thumbnails *held* cannot see: a pane that renders the same rows over and +/// over. The cache settles at the rows in view either way, so `thumb_count` is the same whether +/// the pane rendered them once or a thousand times — and the flashing white of #969 was the +/// rendering, not the holding. `thumb_renders` counts the renders, so a settled pane is one that +/// has stopped asking for them. +/// +/// A follow-up to #1011, as skylartaylor asked on #1038. +#[test] +fn a_settled_thumbnail_pane_renders_nothing_more() { + let mut h = harness(800); + h.run_steps(60); + let (held, rendered) = (h.state().textures.thumb_count(), h.state().textures.thumb_renders()); + // Every thumbnail held was rendered, and the pane did not render many it then threw away: + // this is the assertion a count of held thumbnails cannot make, and it comes first so that a + // thrashing pane is reported as thrashing rather than as holding too few. + assert!(rendered >= held as u64, "{rendered} renders for {held} thumbnails"); + assert!(rendered < held as u64 * 2, "{rendered} renders for {held} thumbnails: the pane is re-rendering"); + assert!(held > 3 && held < 60, "{held} thumbnails held"); + + // Ten more frames of the same view: not one more render. + h.run_steps(10); + assert_eq!(h.state().textures.thumb_renders(), rendered, "a settled pane renders nothing more"); + assert_eq!(h.state().textures.thumb_count(), held); + + // Scrolled far down: the slides now in view are rendered, and only those -- the renders go up + // by about a screen's worth, not by the whole deck. + h.hover_at(egui::pos2(100.0, 450.0)); + h.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(0.0, -40_000.0), + modifiers: egui::Modifiers::NONE, + phase: egui::TouchPhase::Move, + }); + h.run_steps(60); + let after = h.state().textures.thumb_renders(); + // Measured: 8 held and 8 rendered at rest, 24 rendered after the scroll -- the rows it + // landed on plus the ones it passed over, not the 800 slides of the deck. + assert!(after > rendered, "the slides now in view are rendered: {after} vs {rendered}"); + assert!(after < 100, "{after} renders after one scroll of an 800-slide deck (was {rendered})"); + // And it settles again. + h.run_steps(10); + assert_eq!(h.state().textures.thumb_renders(), after, "settled again"); +} + +/// The same for the Slide Sorter: it settles, and stays settled. +#[test] +fn a_settled_sorter_renders_nothing_more() { + let mut h = harness(800); + h.state_mut().ui.view = crate::ViewMode::Sorter; + h.run_steps(60); + let (held, rendered) = (h.state().textures.thumb_count(), h.state().textures.thumb_renders()); + assert!(rendered >= held as u64 && rendered < held as u64 * 2, "{rendered} renders for {held} thumbnails"); + assert!(held > 3 && held < 80, "{held} thumbnails held"); + h.run_steps(10); + assert_eq!(h.state().textures.thumb_renders(), rendered, "a settled sorter renders nothing more"); +} diff --git a/crates/ui-egui/src/textures.rs b/crates/ui-egui/src/textures.rs index 49df4144..c6643c20 100644 --- a/crates/ui-egui/src/textures.rs +++ b/crates/ui-egui/src/textures.rs @@ -47,6 +47,10 @@ pub struct Textures { /// Canvas renders so far (tests, performance checks). pub canvas_renders: u64, thumbs: HashMap, + /// Thumbnails actually rendered, over the life of this cache. The number *held* + /// ([`Textures::thumb_count`]) cannot see a pane that renders the same rows over and over -- + /// the cache thrash behind #969 -- because the count settles while the work does not. + thumb_renders: u64, /// SmartArt gallery pictures by layout, style and colours (and, for hover previews, the /// outline's hash). smartart: HashMap, @@ -345,6 +349,12 @@ impl Textures { self.thumbs.len() } + /// How many thumbnails this cache has rendered, ever. A pane that is done asks for no more: + /// the number stops climbing, which is what tells a thrashing pane from a settled one. + pub fn thumb_renders(&self) -> u64 { + self.thumb_renders + } + /// Force the canvas to re-render next time (fonts loaded, theme changed in place…). pub fn invalidate(&mut self) { self.canvas = None; @@ -384,6 +394,7 @@ impl Textures { let (tw, th) = fit_size((width_px, h), max_side(ctx)); let scale = tw as f64 / p.slide_size.width.max(1.0); let img = deckcraft_render::render_slide(p, index, &RenderOpts { scale, size: Some((tw, th)), threads: 0, ..Default::default() }); + self.thumb_renders += 1; let tex = upload_editor(ctx, format!("thumb-{index}"), to_color_image(&img, false)); // Drop the old entry for this slide address; entries for slides that no longer exist age out. let region = key.want;