From d9a1d55f2eb6e68acdb3f2dfd326aab0688866a6 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:14:02 +0300 Subject: [PATCH] perf: don't resolve viewport cameras for a grid nobody draws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `grid-detail` came back `views=1095.2ms params=0.0ms bg=0.0ms n=0`. Zero grid views — every millisecond of it thrown away. `grid_views` asked `active_viewports` for every viewport, which resolves a camera for each, and `camera_for_viewport` reaches `model_space_extents`, which outside the Model layout tessellates the whole drawing. Only then did it filter on `grid_on`, which removed all of them. The parallel walk from the previous commit had already cut this from 7826.9 ms; it was 7.8 seconds of work for an empty vector. `grid_views` now asks first. `any_viewport_grid_on` reads the same fields `active_viewports` copies into `ViewportInstance::grid_on`, and skips the visibility filters that function also applies: answering "maybe" costs only the work that would have happened anyway, while answering "no" wrongly would drop a grid the user asked for. Also reports the tessellation memo's guard and the inputs that can move it. Two consecutive identical resident builds each missed all 186 455 entries — `memo_hit=0` twice in a row — which a stable guard cannot explain, so something is clearing the memo between them and reading the code has not said what. `guard=`, `guard_stale=`, `avp=`, `anno=`, `anno_h=`, `all_vis=` and `sdf_gen=` will. Co-Authored-By: Claude Opus 5 --- src/scene/mod.rs | 19 +++++++++++++++++-- src/scene/paper.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9a3c58ac1..6e8298c8f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -9080,6 +9080,11 @@ impl Scene { let mut materialize_ms = 0.0f64; let mut memo_hits = 0usize; let memo_misses; + // Two consecutive identical resident builds both missed every entry, + // which a stable guard cannot explain — the memo is being cleared + // between them. Report the guard and the inputs that can move it. + let mut guard_dbg = 0u64; + let mut guard_was_stale = false; let t_build = perf.then(iced::time::Instant::now); let mut wires: Vec = if memo_active { // Guard hash of everything tessellate_entity output depends on @@ -9114,7 +9119,9 @@ impl Scene { } else { (&self.tess_memo, &self.tess_memo_guard) }; - if guard_cell.get() != guard { + guard_dbg = guard; + guard_was_stale = guard_cell.get() != guard; + if guard_was_stale { memo_cell.borrow_mut().clear(); guard_cell.set(guard); } @@ -9228,7 +9235,8 @@ impl Scene { crate::perf_record!( "[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} \ build={:.1} [classify={:.1} hits={:.1} tess={:.1} materialize={:.1}] sort={:.1}({}) \ -entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={}", +entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={} \ +guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", crate::perf::elapsed_ms(t_fn), sort_cache_ms, visible_ms, @@ -9247,6 +9255,13 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates if memo_active { "on" } else { "off" }, visible_path, visible_candidates, + guard_dbg, + guard_was_stale, + avp.map(|h| h.value()).unwrap_or(0), + anno, + annotation_scale_handle.map(|h| h.value()).unwrap_or(0), + all_visible, + crate::scene::text::sdf_atlas::generation(), ); } wires diff --git a/src/scene/paper.rs b/src/scene/paper.rs index d7a1d9285..450074122 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -69,7 +69,36 @@ impl Scene { (layout_block, sheet, content) } + /// Could any viewport in this layout draw a grid? + /// + /// Read from the same fields `active_viewports` copies into + /// `ViewportInstance::grid_on`, and deliberately without the visibility + /// filters that function also applies: answering "maybe" only costs the + /// work that would have happened anyway, while answering "no" wrongly + /// would drop a grid the user asked for. + fn any_viewport_grid_on(&self) -> bool { + if self.current_layout == "Model" { + return self.model_tiles.borrow().iter().any(|tile| tile.grid_on); + } + let (_, sheet, content) = self.paper_viewport_handles(); + let grid_on = |handle: Handle| { + matches!( + self.document.get_entity(handle), + Some(EntityType::Viewport(vp)) if vp.status.grid_on + ) + }; + grid_on(sheet) || content.iter().copied().any(grid_on) + } + pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera, Handle)> { + // `active_viewports` resolves a camera for every viewport, and + // `camera_for_viewport` reaches `model_space_extents`, which outside the + // Model layout tessellates the whole drawing. Filtering on `grid_on` + // afterwards threw all of that away: on the reproducer the first paper + // frame spent 7.8 s here to produce `n=0` views. Ask first. + if !self.any_viewport_grid_on() { + return Vec::new(); + } self.active_viewports(vw, vh, acadrust::entities::ViewportRenderMode::Wireframe2D) .into_iter() .filter(|inst| inst.grid_on)