diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9dda9ce11d..cc26d6de45 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2003,89 +2003,12 @@ impl RenderState { pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { let _start = performance::begin_timed_log!("render_from_cache"); performance::begin_measure!("render_from_cache"); - let bg_color = self.background_color; - - // During fast mode (pan/zoom), if a previous full-quality render still has pending tiles, - // always prefer the persistent atlas. The atlas is incrementally updated as tiles finish, - // and drawing from it avoids mixing a partially-updated Cache surface with missing tiles. - if self.options.is_fast_mode() && !self.surfaces.atlas.is_empty() { - self.surfaces - .draw_atlas_to_backbuffer(self.viewbox, bg_color); - - self.present_frame(shapes); - performance::end_measure!("render_from_cache"); - performance::end_timed_log!("render_from_cache", _start); - return; - } - - // Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache) - if self.cached_viewbox.area.width() > 0.0 { - // Scale and translate the target according to the cached data - let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom; - - let interest = self.options.dpr_viewport_interest_area_threshold; - let TileRect(start_tile_x, start_tile_y, _, _) = - tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest); - let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr; - let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr; - let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x; - let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y; - - // For zoom-out, prefer cache only if it fully covers the viewport. - // Otherwise, atlas will provide a more correct full-viewport preview. - let zooming_out = self.viewbox.zoom < self.cached_viewbox.zoom; - if zooming_out { - let cache_dim = self.surfaces.cache_dimensions(); - let cache_w = cache_dim.width as f32; - let cache_h = cache_dim.height as f32; - - // Viewport in target pixels. - let vw = self.viewbox.dpr_width().max(1.0); - let vh = self.viewbox.dpr_height().max(1.0); - - // Inverse-map viewport corners into cache coordinates. - // target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords). - // => cache = (target / navigate_zoom) - translate - let inv = if navigate_zoom.abs() > f32::EPSILON { - 1.0 / navigate_zoom - } else { - 0.0 - }; - - // let cx0 = (0.0 * inv) - translate_x; - // let cy0 = (0.0 * inv) - translate_y; - // NOTA: 0.0 * inv => siempre 0 - let cx0 = -translate_x; - let cy0 = -translate_y; - let cx1 = (vw * inv) - translate_x; - let cy1 = (vh * inv) - translate_y; - - let min_x = cx0.min(cx1); - let min_y = cy0.min(cy1); - let max_x = cx0.max(cx1); - let max_y = cy0.max(cy1); - - let cache_covers = - min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h; - if !cache_covers { - // Early return only if atlas exists; otherwise keep cache path. - if !self.surfaces.atlas.is_empty() { - self.surfaces - .draw_atlas_to_backbuffer(self.viewbox, bg_color); - - self.present_frame(shapes); - performance::end_measure!("render_from_cache"); - performance::end_timed_log!("render_from_cache", _start); - return; - } - } - } - - // Draw directly from cache surface, avoiding snapshot overhead - self.surfaces.draw_cache_to_backbuffer(); - - self.present_frame(shapes); - } + self.surfaces.draw_combined_atlas_to_backbuffer( + &self.viewbox, + &self.tile_viewbox, + self.background_color, + ); + self.present_frame(shapes); performance::end_measure!("render_from_cache"); performance::end_timed_log!("render_from_cache", _start); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 4f9952ca79..dc91011abc 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -590,6 +590,57 @@ impl Surfaces { canvas.restore(); } + /// Fast pan/zoom preview: draw the doc atlas as backdrop, then overlay HQ + /// cached tile textures placed via their stored document rects (pan + scale). + pub fn draw_combined_atlas_to_backbuffer( + &mut self, + viewbox: &Viewbox, + tile_viewbox: &TileViewbox, + background: skia::Color, + ) { + self.draw_atlas_to_backbuffer(*viewbox, background); + + // Tile textures are keyed by grid index but positioned in document space. + // Without `tile_doc_rects` we cannot displace/scale them correctly (e.g. + // right after zoom invalidation); the atlas backdrop alone is enough. + if self.atlas.tile_doc_rects.is_empty() { + return; + } + + let batch = self.tiles.build_atlas_draw_batch_for_doc_rects( + viewbox, + tile_viewbox, + &self.atlas.tile_doc_rects, + ); + if batch.is_empty() { + return; + } + + if self.tiles.needs_snapshot() || self.tile_atlas_image.is_none() { + self.tile_atlas_image = Some(self.tile_atlas.image_snapshot()); + self.tiles.snapshot(); + } + let Some(atlas_image) = self.tile_atlas_image.as_ref() else { + return; + }; + + let canvas = self.backbuffer.canvas(); + canvas.save(); + canvas.reset_matrix(); + + canvas.draw_atlas( + atlas_image, + &batch.transforms, + &batch.textures, + None, + skia::BlendMode::SrcOver, + self.atlas_sampling_options, + None, + None, + ); + canvas.restore(); + } + pub fn margins(&self) -> skia::ISize { self.margins } @@ -716,18 +767,6 @@ impl Surfaces { ); } - /// Draws the cache surface directly to the backbuffer canvas. - /// This avoids creating an intermediate snapshot, reducing GPU stalls. - pub fn draw_cache_to_backbuffer(&mut self) { - let sampling_options = self.sampling_options; - self.cache.draw( - self.backbuffer.canvas(), - (0.0, 0.0), - sampling_options, - Some(&skia::Paint::default()), - ); - } - pub fn cache_dimensions(&self) -> skia::ISize { skia::ISize::new(self.cache.width(), self.cache.height()) } @@ -1516,6 +1555,17 @@ pub struct TileTextureCache { removed: HashSet, } +pub struct AtlasDrawBatch { + pub transforms: Vec, + pub textures: Vec, +} + +impl AtlasDrawBatch { + pub fn is_empty(&self) -> bool { + self.transforms.is_empty() + } +} + impl TileTextureCache { pub fn new(texture_size: i32, capacity: usize) -> Self { Self { @@ -1615,6 +1665,76 @@ impl TileTextureCache { } } + pub fn build_atlas_draw_batch_for_doc_rects( + &self, + viewbox: &Viewbox, + tile_viewbox: &TileViewbox, + tile_doc_rects: &HashMap, + ) -> AtlasDrawBatch { + let mut transforms = Vec::new(); + let mut textures = Vec::new(); + + let s = viewbox.get_scale(); + let view_doc = viewbox.area; + + for y in tile_viewbox.visible_rect.top()..=tile_viewbox.visible_rect.bottom() { + for x in tile_viewbox.visible_rect.left()..=tile_viewbox.visible_rect.right() { + let tile = Tile(x, y); + + let Some(tile_ref) = self.grid.get(&tile) else { + continue; + }; + + if self.removed.contains(&tile) { + continue; + } + + let doc_rect = tile_doc_rects + .get(&tile) + .copied() + .unwrap_or_else(|| tiles::get_tile_rect(tile, s)); + if doc_rect.is_empty() || !doc_rect.intersects(view_doc) { + continue; + } + + let scos = doc_rect.width() * s / self.tile_size; + let tx = (doc_rect.left + viewbox.pan.x) * s; + let ty = (doc_rect.top + viewbox.pan.y) * s; + + transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty))); + textures.push(tile_ref.rect); + } + } + + // Cached tiles from a previous zoom level use indices outside visible_rect; + // place them via their stored document rect, not the current grid walk above. + for (&tile, tile_ref) in &self.grid { + if tile_viewbox.is_visible(&tile) || self.removed.contains(&tile) { + continue; + } + + let doc_rect = tile_doc_rects + .get(&tile) + .copied() + .unwrap_or_else(|| tiles::get_tile_rect(tile, s)); + if doc_rect.is_empty() || !doc_rect.intersects(view_doc) { + continue; + } + + let tx = (doc_rect.left + viewbox.pan.x) * s; + let ty = (doc_rect.top + viewbox.pan.y) * s; + let scos = doc_rect.width() * s / self.tile_size; + + transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty))); + textures.push(tile_ref.rect); + } + + AtlasDrawBatch { + transforms, + textures, + } + } + pub fn has(&self, tile: Tile) -> bool { self.grid.contains_key(&tile) && !self.removed.contains(&tile) }