diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 44b0c7e674..5ae705a7bd 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -422,6 +422,21 @@ impl RenderState { }) } + /// Device scale when workspace zoom is 100% (`viewbox.zoom == 1`): `1.0 * dpr`. + fn base_zoom_placeholder_scale_bits(&self) -> u32 { + (1.0_f32 * self.options.dpr()).to_bits() + } + + /// Scale bits used to look up tile textures in the cache. + /// In `fast_mode`, always use the 100% zoom cache; otherwise use current scale. + fn tile_texture_cache_lookup_scale_bits(&self) -> u32 { + if self.options.is_fast_mode() { + self.base_zoom_placeholder_scale_bits() + } else { + self.get_scale().to_bits() + } + } + fn rect_union(a: Rect, b: Rect) -> Rect { Rect::from_ltrb( a.left().min(b.left()), @@ -1489,6 +1504,9 @@ impl RenderState { target_world_rect, current_scale, current_scale_bits, + self.options + .is_fast_mode() + .then_some(self.base_zoom_placeholder_scale_bits()), true, ); } @@ -1576,9 +1594,14 @@ impl RenderState { let _tile_start = performance::begin_timed_log!("tile_cache_update"); performance::begin_measure!("tile_cache"); - let scale_bits = scale.to_bits(); - self.pending_tiles - .update(&self.tile_viewbox, &self.surfaces, scale_bits); + let lookup_bits = self.tile_texture_cache_lookup_scale_bits(); + self.pending_tiles.update( + &self.tile_viewbox, + &self.surfaces, + scale, + self.options.is_fast_mode(), + lookup_bits, + ); performance::end_measure!("tile_cache"); performance::end_timed_log!("tile_cache_update", _tile_start); @@ -2612,16 +2635,14 @@ impl RenderState { while !should_stop { if let Some(current_tile) = self.current_tile { let scale = self.get_scale(); - let scale_bits = scale.to_bits(); - if self - .surfaces - .has_cached_tile_surface(current_tile, scale_bits) - { + let lookup_bits = self.tile_texture_cache_lookup_scale_bits(); + let fast_mode = self.options.is_fast_mode(); + if !fast_mode && self.surfaces.has_cached_tile_surface(current_tile, lookup_bits) { performance::begin_measure!("render_shape_tree::cached"); let tile_rect = self.get_current_tile_bounds()?; self.surfaces.draw_cached_tile_surface( current_tile, - scale_bits, + lookup_bits, tile_rect, self.background_color, ); @@ -2646,7 +2667,8 @@ impl RenderState { self.background_color, target_world_rect, scale, - scale_bits, + lookup_bits, + fast_mode.then_some(self.base_zoom_placeholder_scale_bits()), true, ); performance::begin_measure!("render_shape_tree::uncached"); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index d7dbc4c0cc..dd0067edff 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -566,6 +566,24 @@ impl Surfaces { self.tiles.has(tile, scale_bits) } + pub fn world_rect_has_any_tile_at_scale_bits(&self, world_rect: Rect, scale_bits: u32) -> bool { + let scale = f32::from_bits(scale_bits); + if !scale.is_finite() || scale <= 0.0 { + return false; + } + let tile_size_world = super::tiles::get_tile_size(scale); + let super::tiles::TileRect(sx, sy, ex, ey) = + super::tiles::get_tiles_for_rect(world_rect, tile_size_world); + for x in sx..=ex { + for y in sy..=ey { + if self.tiles.has_stale(Tile::from(x, y), scale_bits) { + return true; + } + } + } + false + } + pub fn cached_scale_bits(&self) -> Vec { self.tiles.scale_bits().collect() } @@ -670,6 +688,7 @@ impl Surfaces { } /// Draw a placeholder for a missing tile using cached tiles from other zoom levels. + /// If `forced_src_scale_bits` is set, only that scale is used as the source (used by fast_mode). pub fn draw_tile_fallback_cross_zoom( &mut self, tile_viewbox: &TileViewbox, @@ -678,12 +697,15 @@ impl Surfaces { target_world_rect: Rect, target_scale: f32, target_scale_bits: u32, + forced_src_scale_bits: Option, debug_trace: bool, ) -> usize { - let Some(candidate_scale_bits) = self - .tiles - .best_fallback_scale_bits(target_scale, target_scale_bits) - else { + let Some(candidate_scale_bits) = (if let Some(bits) = forced_src_scale_bits { + Some(bits) + } else { + self.tiles + .best_fallback_scale_bits(target_scale, target_scale_bits) + }) else { if debug_trace { } return 0; @@ -706,7 +728,12 @@ impl Surfaces { for x in sx..=ex { for y in sy..=ey { let src_tile = Tile::from(x, y); - let Some(src_image) = self.tiles.get(src_tile, candidate_scale_bits) else { + let src_image_opt = if forced_src_scale_bits.is_some() { + self.tiles.get_stale(src_tile, candidate_scale_bits) + } else { + self.tiles.get(src_tile, candidate_scale_bits) + }; + let Some(src_image) = src_image_opt else { continue; }; @@ -833,6 +860,12 @@ impl TileTextureCache { self.grid.contains_key(&key) && !self.removed.contains(&key) } + /// Like `has` but ignores the `removed` tombstone (stale placeholder). + pub fn has_stale(&self, tile: Tile, scale_bits: u32) -> bool { + let key = TileCacheKey { tile, scale_bits }; + self.grid.contains_key(&key) + } + fn gc(&mut self) { println!("gc"); // Make a real remove @@ -896,6 +929,12 @@ impl TileTextureCache { self.grid.get(&key) } + /// Like `get` but returns the image even if it is marked as `removed`. + pub fn get_stale(&self, tile: Tile, scale_bits: u32) -> Option<&skia::Image> { + let key = TileCacheKey { tile, scale_bits }; + self.grid.get(&key) + } + pub fn remove(&mut self, tile: Tile, scale_bits: u32) { self.removed.insert(TileCacheKey { tile, scale_bits }); } diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 318b91b04c..8bde91668c 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -261,7 +261,14 @@ impl PendingTiles { result } - pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, scale_bits: u32) { + pub fn update( + &mut self, + tile_viewbox: &TileViewbox, + surfaces: &Surfaces, + current_scale: f32, + fast_mode: bool, + lookup_scale_bits: u32, + ) { self.list.clear(); // Generate spiral for the interest area (viewport + margin) @@ -279,7 +286,14 @@ impl PendingTiles { for tile in spiral { let is_visible = tile_viewbox.visible_rect.contains(&tile); - let is_cached = surfaces.has_cached_tile_surface(tile, scale_bits); + let is_cached = if fast_mode { + // fast_mode reads only 100% zoom cache; tile indices differ across scales, + // so decide cacheability by world rect coverage at the lookup scale. + let world_rect = super::tiles::get_tile_rect(tile, current_scale); + surfaces.world_rect_has_any_tile_at_scale_bits(world_rect, lookup_scale_bits) + } else { + surfaces.has_cached_tile_surface(tile, lookup_scale_bits) + }; match (is_visible, is_cached) { (true, true) => visible_cached.push(tile),