diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index f2f04ed8fa..44b0c7e674 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -15,7 +15,7 @@ mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use gpu_state::GpuState; @@ -295,6 +295,12 @@ fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec) - } } +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +struct ShapeScaleKey { + shape_id: Uuid, + scale_bits: u32, +} + pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, @@ -330,6 +336,7 @@ pub(crate) struct RenderState { pub show_grid: Option, pub focus_mode: FocusMode, pub touched_ids: HashSet, + shape_last_extrect_by_scale: HashMap, /// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.) /// where we must render shapes without inheriting ancestor layer blurs. Toggle it through /// `with_nested_blurs_suppressed` to ensure it's always restored. @@ -408,12 +415,53 @@ impl RenderState { show_grid: None, focus_mode: FocusMode::new(), touched_ids: HashSet::default(), + shape_last_extrect_by_scale: HashMap::new(), ignore_nested_blurs: false, preview_mode: false, export_context: None, }) } + fn rect_union(a: Rect, b: Rect) -> Rect { + Rect::from_ltrb( + a.left().min(b.left()), + a.top().min(b.top()), + a.right().max(b.right()), + a.bottom().max(b.bottom()), + ) + } + + fn invalidate_shape_across_cached_scales(&mut self, shape: &Shape, tree: ShapesPoolRef) { + let scale_bits_list = self.surfaces.cached_scale_bits(); + for scale_bits in scale_bits_list { + let scale = f32::from_bits(scale_bits); + if !scale.is_finite() || scale <= 0.0 { + continue; + } + let new_extrect = shape.extrect(tree, scale); + let key = ShapeScaleKey { + shape_id: shape.id, + scale_bits, + }; + let rect = if let Some(old) = self.shape_last_extrect_by_scale.get(&key).copied() { + Self::rect_union(old, new_extrect) + } else { + new_extrect + }; + + let tile_size = tiles::get_tile_size(scale); + let TileRect(sx, sy, ex, ey) = tiles::get_tiles_for_rect(rect, tile_size); + for x in sx..=ex { + for y in sy..=ey { + self.surfaces + .remove_cached_tile_surface(tiles::Tile::from(x, y), scale_bits); + } + } + + self.shape_last_extrect_by_scale.insert(key, new_extrect); + } + } + /// Combines every visible layer blur currently active (ancestors + shape) /// into a single equivalent blur. Layer blur radii compound by adding their /// variances (σ² = radius²), so we: @@ -2933,23 +2981,19 @@ impl RenderState { pub fn rebuild_touched_tiles(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_touched_tiles"); - let mut all_tiles = HashSet::::new(); - let ids = std::mem::take(&mut self.touched_ids); for shape_id in ids.iter() { if let Some(shape) = tree.get(shape_id) { if shape_id != &Uuid::nil() { - all_tiles.extend(self.update_shape_tiles(shape, tree)); + // Keep the shape->tile index updated for the current viewport. + let _ = self.update_shape_tiles(shape, tree); + // Invalidate cached tiles for this shape across all cached zoom levels. + self.invalidate_shape_across_cached_scales(shape, tree); } } } - // Update the changed tiles - for tile in all_tiles { - self.remove_cached_tile(tile); - } - performance::end_measure!("rebuild_touched_tiles"); } @@ -2967,15 +3011,12 @@ impl RenderState { tree: ShapesPoolMutRef<'_>, ) -> Result<()> { performance::begin_measure!("invalidate_and_update_tiles"); - let mut all_tiles = HashSet::::new(); for shape_id in shape_ids { if let Some(shape) = tree.get(shape_id) { - all_tiles.extend(self.update_shape_tiles(shape, tree)); + let _ = self.update_shape_tiles(shape, tree); + self.invalidate_shape_across_cached_scales(shape, tree); } } - for tile in all_tiles { - self.remove_cached_tile(tile); - } performance::end_measure!("invalidate_and_update_tiles"); Ok(()) } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index edb8152d64..d7dbc4c0cc 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -566,6 +566,10 @@ impl Surfaces { self.tiles.has(tile, scale_bits) } + pub fn cached_scale_bits(&self) -> Vec { + self.tiles.scale_bits().collect() + } + pub fn remove_cached_tile_surface(&mut self, tile: Tile, scale_bits: u32) { // Mark tile as invalid // Old content stays visible until new tile overwrites it atomically, @@ -914,6 +918,10 @@ impl TileTextureCache { self.grid.len() } + pub fn scale_bits(&self) -> impl Iterator + '_ { + self.scales.iter().copied() + } + pub fn best_fallback_scale_bits( &self, target_scale: f32,