From a9b7feec4adcd1fd539a0f3a6b86b540210d38d2 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 5 Apr 2026 16:16:03 +0900 Subject: [PATCH] perf(canvas): skip ViewportCull bitset build when all leaves visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ViewportCull::from_plan allocated and populated a VisibilitySet bitset of every visible leaf id on every full-draw frame. At fit-zoom on large scenes (135K-node fixture, dashboards, pasteboards) every leaf is visible, so the bitset is O(N) work whose answer is always "true". Change the cull to carry an Option: None represents the "all visible" case and is_leaf_visible short-circuits to true. Detect the case by comparing live region count + promoted count against the LayerList length (regions and promoted are disjoint — see the region/promoted partitioning in draw_layers_with_scene_cache_skip). Criterion (CPU raster, bench_viewport_culling): 50k/all_visible 24.678ms -> 24.077ms -2.44% (p=0.00) 5k/all_visible 3.881ms -> 3.818ms -1.76% (p=0.00) partial/corner/zoomed_in/empty: no regression (p > 0.16) The fast path only fires when every leaf is visible; culled frames pay one extra Option match branch per leaf which the benchmarks show is below noise. Savings scale linearly with node count, targeting the view-only fit-zoom path on large scenes. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grida-canvas/src/painter/painter.rs | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index ec8568395..b0a92ac83 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -79,7 +79,9 @@ impl VisibilitySet { /// stable and cached; visibility culling is a per-frame draw-time concern. pub struct ViewportCull { /// Bitset of visible leaf NodeIds from R-tree intersection query. - visible_leaves: VisibilitySet, + /// `None` when every leaf in the scene is visible — the bitset is then + /// unnecessary and [`is_leaf_visible`] short-circuits to `true`. + visible_leaves: Option, /// World-space viewport rectangle. Stored for future use when /// RenderSurface bounds include effect inflation. #[allow(dead_code)] @@ -92,10 +94,27 @@ impl ViewportCull { /// Extracts visible NodeIds from the plan's region indices (live-drawn /// nodes) and promoted IDs (compositor-cached nodes), then builds the /// compact bitset. + /// + /// When every leaf in the scene is visible (count of visible IDs matches + /// the total layer count), returns an "all-visible" cull that skips the + /// O(N) bitset construction. On large fit-zoom scenes (100K+ nodes), + /// this eliminates ~1ms of per-frame allocation + bit-setting work. pub fn from_plan( plan: &crate::runtime::scene::FramePlan, layers: &super::layer::LayerList, ) -> Self { + // Count visible IDs to detect the all-visible fast path. Regions hold + // the live-drawn indices; promoted IDs are disjoint from live regions + // (see `draw_layers_with_scene_cache_skip` construction path). + let live_count: usize = plan.regions.iter().map(|(_, idx)| idx.len()).sum(); + let visible_count = live_count + plan.promoted.len(); + if visible_count >= layers.layers.len() { + return Self { + visible_leaves: None, + viewport: plan.viewport, + }; + } + let visible_leaves = VisibilitySet::from_ids( plan.regions .iter() @@ -107,7 +126,7 @@ impl ViewportCull { .chain(plan.promoted.iter().copied()), ); Self { - visible_leaves, + visible_leaves: Some(visible_leaves), viewport: plan.viewport, } } @@ -115,7 +134,10 @@ impl ViewportCull { /// Returns `true` if the leaf node is visible (in the R-tree result set). #[inline] pub fn is_leaf_visible(&self, id: &NodeId) -> bool { - self.visible_leaves.contains(id) + match &self.visible_leaves { + Some(set) => set.contains(id), + None => true, + } } }