From 06a52b7951569db295fb0e13e0c2bb9fb48a12f6 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Tue, 19 May 2026 11:57:27 +0200 Subject: [PATCH] :bug: Fix minor glitches --- frontend/src/debug.cljs | 33 +--- render-wasm/src/main.rs | 2 +- render-wasm/src/render.rs | 236 ++++++++++++++--------------- render-wasm/src/render/debug.rs | 10 +- render-wasm/src/render/options.rs | 6 + render-wasm/src/render/surfaces.rs | 121 +++++++++++---- render-wasm/src/tiles.rs | 86 +++++++---- 7 files changed, 283 insertions(+), 211 deletions(-) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 985a936bcc..b4322a593f 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -135,6 +135,14 @@ (wasm.mem/free) text))) +(defn ^:export wasmCaptureFrames + [amount] + (let [module wasm/internal-module + f (when module (unchecked-get module "_capture_frames"))] + (if (fn? f) + (wasm.h/call module "_capture_frames" amount) + (js/console.warn "[debug] render-wasm module not ready or missing _render_stats")))) + (defn ^:export wasmRenderStats [] (let [module wasm/internal-module @@ -213,31 +221,6 @@ opacity: 0.5; ") -(defn ^:export fps - "Adds a widget to keep track of the average FPS's" - [] - (let [last (volatile! (.now js/performance)) - avg (volatile! 0) - node (-> (.createElement js/document "div") - (obj/set! "id" "fps") - (obj/set! "style" widget-style)) - body (obj/get js/document "body") - - do-thing (fn do-thing [] - (timers/raf - (fn [] - (let [cur (.now js/performance) - ts (/ 1000 (* (- cur @last))) - val (+ @avg (* (- ts @avg) 0.1))] - - (obj/set! node "innerText" val) - (vreset! last cur) - (vreset! avg val) - (do-thing)))))] - - (.appendChild body node) - (do-thing))) - (defn ^:export dump-state [] (logjs "state" @st/state) nil) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index df3e17495b..5563fc1330 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -302,7 +302,7 @@ pub extern "C" fn set_view_end() -> Result<()> { let render_state = get_render_state(); render_state.options.set_fast_mode(false); render_state.cancel_animation_frame(); - render_state.tile_viewbox.update(render_state.viewbox); + render_state.tile_viewbox.update(&render_state.viewbox); if render_state.options.is_profile_rebuild_tiles() { state.rebuild_tiles(); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 794a0bcfab..fdefa22ed2 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -441,22 +441,6 @@ fn drag_crop_snapshot_window_px( (ox, oy, win_w, win_h) } -pub fn get_cache_size(viewbox: Viewbox, interest: i32) -> skia::ISize { - // First we retrieve the extended area of the viewport that we could render. - let TileRect(isx, isy, iex, iey) = - tiles::get_tiles_for_viewbox_with_interest(viewbox, interest); - - let dx = if isx.signum() != iex.signum() { 1 } else { 0 }; - let dy = if isy.signum() != iey.signum() { 1 } else { 0 }; - - let tile_size = tiles::TILE_SIZE; - ( - ((iex - isx).abs() + dx) * tile_size as i32, - ((iey - isy).abs() + dy) * tile_size as i32, - ) - .into() -} - impl RenderState { /// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an /// interactive transform (drag/resize/rotate). @@ -563,7 +547,7 @@ impl RenderState { render_area_with_margins: Rect::new_empty(), tiles, tile_viewbox: tiles::TileViewbox::new_with_interest( - viewbox, + &viewbox, options.dpr_viewport_interest_area_threshold, ), pending_tiles: PendingTiles::new(), @@ -837,7 +821,7 @@ impl RenderState { let dpr_height = (height as f32 * self.options.dpr).floor() as i32; self.surfaces.resize(dpr_width, dpr_height)?; self.viewbox.set_wh(width as f32, height as f32); - self.tile_viewbox.update(self.viewbox); + self.tile_viewbox.update(&self.viewbox); Ok(()) } @@ -918,7 +902,7 @@ impl RenderState { // the interaction ends. if self.options.is_interactive_transform() { let tile_rect = self.get_current_aligned_tile_bounds()?; - self.surfaces.draw_current_tile_direct( + self.surfaces.draw_current_tile_into_backbuffer( &tile_rect, self.background_color, surfaces::DrawOnCache::No, @@ -943,7 +927,7 @@ impl RenderState { .as_ref() .ok_or(Error::CriticalError("Current tile not found".to_string()))?; - self.surfaces.cache_current_tile_texture( + self.surfaces.draw_current_tile_into_tile_atlas( &self.tile_viewbox, ¤t_tile, &tile_rect, @@ -952,11 +936,13 @@ impl RenderState { ); self.surfaces - .draw_cached_tile_surface(current_tile, rect, self.background_color); + .draw_cached_tile_into_backbuffer(current_tile, rect, self.background_color); + Ok(()) } - pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) { + /// This function draws the "surface stack" into the specified "target" surface. + pub fn draw_shape_surface_stack_into(&mut self, shape: Option<&Shape>, target: SurfaceId) { performance::begin_measure!("apply_drawing_to_render_canvas"); let paint = skia::Paint::default(); @@ -1664,7 +1650,7 @@ impl RenderState { } if apply_to_current_surface { - self.apply_drawing_to_render_canvas(Some(&shape), target_surface); + self.draw_shape_surface_stack_into(Some(&shape), target_surface); } // Only restore if we saved (optimization for simple shapes) @@ -1925,7 +1911,7 @@ impl RenderState { 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, + &self.cached_viewbox, interest, ); let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr; @@ -2005,20 +1991,15 @@ impl RenderState { // would be at wrong positions — skip them and let the full // render after set_view_end handle it. if !self.zoom_changed() { - let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox); + let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox); let offset = self.viewbox.get_offset(); for tx in visible_rect.x1()..=visible_rect.x2() { for ty in visible_rect.y1()..=visible_rect.y2() { let tile = tiles::Tile::from(tx, ty); if self.surfaces.has_cached_tile_surface(tile) { - let tile_rect = skia::Rect::from_xywh( - tx as f32 * tiles::TILE_SIZE - offset.x, - ty as f32 * tiles::TILE_SIZE - offset.y, - tiles::TILE_SIZE, - tiles::TILE_SIZE, - ); + let tile_rect = tile.get_rect_with_offset(&offset); self.surfaces - .draw_cached_tile_surface(tile, tile_rect, bg_color); + .draw_cached_tile_into_backbuffer(tile, tile_rect, bg_color); } } } @@ -2057,6 +2038,26 @@ impl RenderState { Ok(()) } + fn gc(&mut self, tree: ShapesPoolRef) { + #[cfg(feature = "stats")] + self.stats.clear(); + + self.surfaces.gc(); + + self.pending_nodes.clear(); + if self.pending_nodes.capacity() < tree.len() { + self.pending_nodes + .reserve(tree.len() - self.pending_nodes.capacity()); + } + // Clear nested state stacks to avoid residual fills/blurs from previous renders + // being incorrectly applied to new frames + self.nested_fills.clear(); + self.nested_blurs.clear(); + self.nested_shadows.clear(); + // reorder by distance to the center. + self.current_tile = None; + } + pub fn start_render_loop( &mut self, base_object: Option<&Uuid>, @@ -2064,13 +2065,12 @@ impl RenderState { timestamp: i32, sync_render: bool, ) -> Result<()> { - #[cfg(feature = "stats")] - self.stats.clear(); + self.gc(tree); let _start = performance::begin_timed_log!("start_render_loop"); let scale = self.get_scale(); - self.tile_viewbox.update(self.viewbox); + self.tile_viewbox.update(&self.viewbox); self.focus_mode.reset(); performance::begin_measure!("render"); @@ -2103,61 +2103,44 @@ impl RenderState { | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32 | SurfaceId::TextDropShadows as u32; + self.surfaces.apply_mut(surface_ids, |s| { s.canvas().scale((scale, scale)); }); - let viewbox_cache_size = get_cache_size( - self.viewbox, - self.options.dpr_viewport_interest_area_threshold, - ); - let cached_viewbox_cache_size = get_cache_size( - self.cached_viewbox, - self.options.dpr_viewport_interest_area_threshold, - ); - // Only resize cache if the new size is larger than the cached size - // This avoids unnecessary surface recreations when the cache size decreases - if viewbox_cache_size.width > cached_viewbox_cache_size.width - || viewbox_cache_size.height > cached_viewbox_cache_size.height - { - self.surfaces.resize_cache( - viewbox_cache_size, - self.options.dpr_viewport_interest_area_threshold, - )?; - } + self.surfaces.resize_cache_from_viewbox( + &self.viewbox, + &self.cached_viewbox, + self.options.dpr_viewport_interest_area_threshold + )?; // FIXME - review debug // debug::render_debug_tiles_for_viewbox(self); let _tile_start = performance::begin_timed_log!("tile_cache_update"); + performance::begin_measure!("tile_cache"); let only_visible = self.options.is_interactive_transform(); self.pending_tiles .update(&self.tile_viewbox, &self.surfaces, only_visible); performance::end_measure!("tile_cache"); - performance::end_timed_log!("tile_cache_update", _tile_start); - self.pending_nodes.clear(); - if self.pending_nodes.capacity() < tree.len() { - self.pending_nodes - .reserve(tree.len() - self.pending_nodes.capacity()); - } - // Clear nested state stacks to avoid residual fills/blurs from previous renders - // being incorrectly applied to new frames - self.nested_fills.clear(); - self.nested_blurs.clear(); - self.nested_shadows.clear(); - // reorder by distance to the center. - self.current_tile = None; + performance::end_timed_log!("tile_cache_update", _tile_start); self.render_in_progress = true; - self.apply_drawing_to_render_canvas(None, SurfaceId::Current); + self.draw_shape_surface_stack_into(None, SurfaceId::Current); if sync_render { self.render_shape_tree_sync(base_object, tree, timestamp)?; } else { self.process_animation_frame(base_object, tree, timestamp)?; + + // This is an option to debug frames. + if self.options.capture_frames > 0 { + self.options.capture_frames -= 1; + } + // Update cached_viewbox after visible tiles render // synchronously so that render_from_cache uses the correct // zoom ratio even if interest-area tiles are still rendering @@ -2215,8 +2198,9 @@ impl RenderState { performance::begin_measure!("process_animation_frame"); self.render_shape_tree_partial(base_object, tree, timestamp, true)?; - self.surfaces.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox); - self.flush_and_submit(); + if !self.options.is_interactive_transform() { + self.surfaces.draw_tile_atlas_to_backbuffer(&self.viewbox, &self.tile_viewbox); + } if self.render_in_progress { // Partial frame: just flush GPU work. The display shows the last @@ -3286,7 +3270,7 @@ impl RenderState { .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); } else if visited_children { - self.apply_drawing_to_render_canvas(Some(element), target_surface); + self.draw_shape_surface_stack_into(Some(element), target_surface); } // Skip nested state updates for flattened containers @@ -3355,6 +3339,7 @@ impl RenderState { } iteration += 1; } + Ok((is_empty, false)) } @@ -3379,13 +3364,18 @@ impl RenderState { while !should_stop { if let Some(current_tile) = self.current_tile { - // NOTA: Ahora no tenemos que cubrir el caso en el que el tile - // no está cacheado porque se hará todo desde el draw_atlas. + // NOTE: For now we don't need to cover the case where the tile + // is not cached because everything will be handled from draw_atlas. if !self.surfaces.has_cached_tile_surface(current_tile) { performance::begin_measure!("render_shape_tree::uncached"); let (is_empty, early_return) = self .render_shape_tree_partial_uncached(tree, timestamp, allow_stop, false)?; + #[cfg(target_arch = "wasm32")] + if self.options.capture_frames > 0 { + debug::console_debug_surface(self, SurfaceId::Backbuffer); + } + if early_return { return Ok(()); } @@ -3400,7 +3390,7 @@ impl RenderState { if self.options.is_interactive_transform() { // During drag, avoid snapshot-based caching. Draw Current directly // into Target (and Cache) to reduce stalls. - self.surfaces.draw_current_tile_direct( + self.surfaces.draw_current_tile_into_backbuffer( &tile_rect, self.background_color, surfaces::DrawOnCache::Yes, @@ -3417,9 +3407,6 @@ impl RenderState { tile_rect, ); } - } else { - // Tile is uncached and has no shapes to render - self.apply_render_to_final_canvas(tile_rect)?; } } else { if self.tiles.is_empty_at(current_tile) { @@ -3442,61 +3429,68 @@ impl RenderState { // empty tile. self.current_tile_had_shapes = false; - if !self.surfaces.has_cached_tile_surface(next_tile) { - if let Some(ids) = self.tiles.get_shapes_at(next_tile) { - // Check if any shape on this tile has a background blur. - // If so, we need ALL root shapes rendered (not just those - // assigned to this tile) because the blur snapshots Current - // which must contain the shapes behind it. - let tile_has_bg_blur = ids.iter().any(|id| { - tree.get(id).is_some_and(|s| { - s.blur.is_some_and(|b| { - !b.hidden && b.blur_type == BlurType::BackgroundBlur - }) - }) - }); + let Some(ids) = self.tiles.get_shapes_at(next_tile) else { + // If the tile is empty we do not need to render it. + continue; + }; - // We only need first level shapes, in the same order as the parent node. - // - // During interactive transforms we may invalidate only the modified shapes - // (to avoid massive ancestor eviction). However, we still composite full - // tiles (we clear the tile rect before drawing Current), so we must render - // all root shapes that can contribute to this tile; otherwise, unchanged - // siblings inside the same tile would disappear. - let mut valid_ids = Vec::with_capacity(ids.len()); - if self.options.is_interactive_transform() || tile_has_bg_blur { - valid_ids.extend(root_ids.iter().copied()); - } else { - for root_id in root_ids.iter() { - if ids.contains(root_id) { - valid_ids.push(*root_id); - } - } + if self.surfaces.has_cached_tile_surface(next_tile) { + // If the tile is cached, then we do not need to + // render it. + continue; + } + + // Check if any shape on this tile has a background blur. + // If so, we need ALL root shapes rendered (not just those + // assigned to this tile) because the blur snapshots Current + // which must contain the shapes behind it. + let tile_has_bg_blur = ids.iter().any(|id| { + tree.get(id).is_some_and(|s| { + s.blur.is_some_and(|b| { + !b.hidden && b.blur_type == BlurType::BackgroundBlur + }) + }) + }); + + // We only need first level shapes, in the same order as the parent node. + // + // During interactive transforms we may invalidate only the modified shapes + // (to avoid massive ancestor eviction). However, we still composite full + // tiles (we clear the tile rect before drawing Current), so we must render + // all root shapes that can contribute to this tile; otherwise, unchanged + // siblings inside the same tile would disappear. + let mut valid_ids = Vec::with_capacity(ids.len()); + if self.options.is_interactive_transform() || tile_has_bg_blur { + valid_ids.extend(root_ids.iter().copied()); + } else { + for root_id in root_ids.iter() { + if ids.contains(root_id) { + valid_ids.push(*root_id); } - - if !valid_ids.is_empty() { - self.current_tile_had_shapes = true; - } - - self.pending_nodes.extend(valid_ids.into_iter().map(|id| { - NodeRenderState { - id, - visited_children: false, - clip_bounds: None, - visited_mask: false, - mask: false, - flattened: false, - } - })); } } + + if !valid_ids.is_empty() { + self.current_tile_had_shapes = true; + } + + self.pending_nodes.extend(valid_ids.into_iter().map(|id| { + NodeRenderState { + id, + visited_children: false, + clip_bounds: None, + visited_mask: false, + mask: false, + flattened: false, + } + })); } else { + // If there are no more pending tiles, stop. should_stop = true; } } self.render_in_progress = false; - self.surfaces.gc(); // Mark cache as valid for render_from_cache. // Only update for full-quality renders (non-fast mode). diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index cddf371851..1646b0a466 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -4,7 +4,7 @@ use super::{tiles, RenderState, SurfaceId}; use macros::wasm_error; #[cfg(target_arch = "wasm32")] -use crate::get_render_state; +use crate::{get_render_state}; use skia_safe::{self as skia, Rect}; @@ -273,6 +273,14 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, } } +#[no_mangle] +#[wasm_error] +#[cfg(target_arch = "wasm32")] +pub extern "C" fn capture_frames(capture_frames: i32) -> Result<()> { + get_render_state().options.set_capture_frames(capture_frames); + Ok(()) +} + #[no_mangle] #[wasm_error] #[cfg(target_arch = "wasm32")] diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 8142f1f52c..6effbd900e 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -29,6 +29,7 @@ pub struct RenderOptions { pub max_blocking_time_ms: i32, pub node_batch_threshold: i32, pub blur_downscale_threshold: f32, + pub capture_frames: i32, } impl Default for RenderOptions { @@ -44,6 +45,7 @@ impl Default for RenderOptions { max_blocking_time_ms: MAX_BLOCKING_TIME_MS, node_batch_threshold: NODE_BATCH_THRESHOLD, blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD, + capture_frames: 0, } } } @@ -66,6 +68,10 @@ impl RenderOptions { self.fast_mode = enabled; } + pub fn set_capture_frames(&mut self, capture_frames: i32) { + self.capture_frames = capture_frames; + } + /// Updates the dpr viewport interest area threshold. /// This function is updated when the dpr or the /// viewport_interest_area_threshold is changed diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index ef3d6e6de2..19d66dd341 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -5,7 +5,7 @@ use crate::{get_gpu_state, performance}; use skia_safe::{self as skia, IRect, Paint, RRect, Rect}; -use super::{gpu_state::GpuState, tiles, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE}; +use super::{gpu_state::GpuState, tiles, tiles::Tile, tiles::TileRect, tiles::TileViewbox}; use crate::math::Point; use base64::{engine::general_purpose, Engine as _}; @@ -13,9 +13,18 @@ use std::collections::{HashMap, HashSet}; const TEXTURES_CACHE_CAPACITY: usize = 1024; const TEXTURES_BATCH_DELETE: usize = 256; + // This is the amount of extra space we're going to give to all the surfaces to render shapes. // If it's too big it could affect performance. +const TILE_SIZE: i32 = tiles::TILE_SIZE as i32; const TILE_SIZE_MULTIPLIER: i32 = 2; +const TILE_MARGIN_SIZE: i32 = TILE_SIZE * TILE_SIZE_MULTIPLIER / 4; +const TILE_DRAWABLE_RECT: IRect = IRect { + left: TILE_MARGIN_SIZE, + top: TILE_MARGIN_SIZE, + right: TILE_MARGIN_SIZE + TILE_SIZE, + bottom: TILE_MARGIN_SIZE + TILE_SIZE, +}; /// Atlas texture size limits (px per side). /// @@ -24,9 +33,24 @@ const TILE_SIZE_MULTIPLIER: i32 = 2; /// [`Surfaces::set_max_atlas_texture_size`]. /// - `MAX_ATLAS_TEXTURE_SIZE` is a hard upper bound to clamp the runtime value /// (defensive cap to avoid accidentally creating oversized GPU textures). -const MAX_ATLAS_TEXTURE_SIZE: i32 = 4096; +const MAX_ATLAS_TEXTURE_SIZE: i32 = 8192; const DEFAULT_MAX_ATLAS_TEXTURE_SIZE: i32 = 1024; +pub fn get_cache_size(viewbox: &Viewbox, interest: i32) -> skia::ISize { + // First we retrieve the extended area of the viewport that we could render. + let TileRect(isx, isy, iex, iey) = + tiles::get_tiles_for_viewbox_with_interest(viewbox, interest); + + let dx = if isx.signum() != iex.signum() { 1 } else { 0 }; + let dy = if isy.signum() != iey.signum() { 1 } else { 0 }; + + ( + ((iex - isx).abs() + dx) * TILE_SIZE, + ((iey - isy).abs() + dy) * TILE_SIZE, + ) + .into() +} + #[derive(Debug, PartialEq)] pub enum DrawOnCache { Yes, @@ -202,7 +226,10 @@ impl Surfaces { /// WebGL `MAX_TEXTURE_SIZE` reported by the browser. Values are clamped to /// a small minimum so the atlas logic stays well-defined. pub fn set_max_atlas_texture_size(&mut self, max_px: i32) { - self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE); + self.max_atlas_texture_size = max_px.clamp( + TILE_SIZE, + MAX_ATLAS_TEXTURE_SIZE + ); } #[inline] @@ -268,7 +295,7 @@ impl Surfaces { } // Add padding to reduce realloc frequency. - let pad = TILE_SIZE; + let pad = tiles::TILE_SIZE; new_left -= pad; new_top -= pad; new_right += pad; @@ -279,7 +306,7 @@ impl Surfaces { // Compute atlas scale needed to fit within the fixed texture cap. // Keep the highest possible scale (closest to 1.0) that still fits. - let cap = self.max_atlas_texture_size.max(TILE_SIZE as i32) as f32; + let cap = self.max_atlas_texture_size.max(TILE_SIZE) as f32; let required_scale = (cap / doc_w).min(cap / doc_h).clamp(0.01, 1.0); // Never upscale the atlas (it would add blur and churn). @@ -876,12 +903,39 @@ impl Surfaces { .ok_or(Error::CriticalError("Failed to create surface".to_string()))?; self.cache.canvas().reset_matrix(); self.cache.canvas().translate(( - (interest_area_threshold as f32 * TILE_SIZE), - (interest_area_threshold as f32 * TILE_SIZE), + (interest_area_threshold * TILE_SIZE) as f32, + (interest_area_threshold * TILE_SIZE) as f32, )); Ok(()) } + pub fn resize_cache_from_viewbox( + &mut self, + viewbox: &Viewbox, + cached_viewbox: &Viewbox, + interest_area_threshold: i32, + ) -> Result<()> { + let viewbox_cache_size = get_cache_size( + viewbox, + interest_area_threshold, + ); + let cached_viewbox_cache_size = get_cache_size( + cached_viewbox, + interest_area_threshold, + ); + // Only resize cache if the new size is larger than the cached size + // This avoids unnecessary surface recreations when the cache size decreases + if viewbox_cache_size.width > cached_viewbox_cache_size.width + || viewbox_cache_size.height > cached_viewbox_cache_size.height + { + return self.resize_cache( + viewbox_cache_size, + interest_area_threshold, + ); + } + Ok(()) + } + pub fn draw_rect_to( &mut self, id: SurfaceId, @@ -1038,6 +1092,7 @@ impl Surfaces { self.canvas(SurfaceId::Debug) .clear(skia::Color::TRANSPARENT) .reset_matrix(); + self.canvas(SurfaceId::UI) .clear(skia::Color::TRANSPARENT) .reset_matrix(); @@ -1054,7 +1109,7 @@ impl Surfaces { canvas.restore(); } - pub fn cache_current_tile_texture( + pub fn draw_current_tile_into_tile_atlas( &mut self, tile_viewbox: &TileViewbox, tile: &Tile, @@ -1063,15 +1118,9 @@ impl Surfaces { tile_doc_rect: skia::Rect, ) { let gpu_state = get_gpu_state(); - let rect = IRect::from_xywh( - self.margins.width, - self.margins.height, - self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width, - self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height, - ); + let rect = TILE_DRAWABLE_RECT; let tile_image_opt = self.current.image_snapshot_with_bounds(rect); - if let Some(tile_image) = tile_image_opt { if !skip_cache_surface { // Draw to cache surface for render_from_cache @@ -1088,6 +1137,7 @@ impl Surfaces { let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect); self.atlas_tile_doc_rects.insert(*tile, tile_doc_rect); + // Draws current tile into tile atlas let tile_ref = self.tiles.add(tile_viewbox, tile); self.tile_atlas.canvas().draw_image_rect( &tile_image, @@ -1249,13 +1299,17 @@ impl Surfaces { self.tile_atlas.image_snapshot_with_bounds(rect) } - pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) { + pub fn draw_cached_tile_into_backbuffer(&mut self, tile: Tile, rect: skia::Rect, _color: skia::Color) { if let Some(image) = self.get_tile_image_from_tile_atlas(tile) { - let mut paint = skia::Paint::default(); - paint.set_color(color); - self.backbuffer.canvas().draw_rect(rect, &paint); - self.backbuffer - .canvas() + let backbuffer_canvas = self.backbuffer.canvas(); + + // if color != skia::Color::TRANSPARENT { + // let mut paint = skia::Paint::default(); + // paint.set_color(color); + // backbuffer_canvas.draw_rect(rect, &paint); + // } + + backbuffer_canvas .draw_image_rect(&image, None, rect, &skia::Paint::default()); } } @@ -1264,16 +1318,16 @@ impl Surfaces { /// cache-aligned rect. This keeps the Cache surface in sync with /// Backbuffer so that `render_from_cache` (used during pan) has the /// full scene including tiles served from the texture cache. - pub fn draw_cached_tile_to_cache( + pub fn draw_cached_tile_into_cache( &mut self, tile: Tile, aligned_rect: &skia::Rect, - color: skia::Color, + _color: skia::Color, ) { if let Some(image) = self.get_tile_image_from_tile_atlas(tile) { - let mut bg = skia::Paint::default(); - bg.set_color(color); - self.cache.canvas().draw_rect(aligned_rect, &bg); + // let mut bg = skia::Paint::default(); + // bg.set_color(color); + // self.cache.canvas().draw_rect(aligned_rect, &bg); self.cache.canvas().draw_image_rect( &image, None, @@ -1286,10 +1340,10 @@ impl Surfaces { /// Draws the current tile directly to the backbuffer and cache surfaces without /// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't /// populate the tile texture cache (suitable for one-shot renders like tests). - pub fn draw_current_tile_direct( + pub fn draw_current_tile_into_backbuffer( &mut self, tile_rect: &skia::Rect, - color: skia::Color, + _color: skia::Color, draw_on_cache: DrawOnCache, ) { let sampling_options = self.sampling_options; @@ -1302,10 +1356,11 @@ impl Surfaces { let src_rect_f = skia::Rect::from(src_rect); let backbuffer_canvas = self.backbuffer.canvas(); + // Draw background - let mut paint = skia::Paint::default(); - paint.set_color(color); - backbuffer_canvas.draw_rect(tile_rect, &paint); + // let mut paint = skia::Paint::default(); + // paint.set_color(color); + // backbuffer_canvas.draw_rect(tile_rect, &paint); // Draw current surface directly to target (no snapshot) self.current.draw( @@ -1487,8 +1542,8 @@ pub struct TileTextureCache { impl TileTextureCache { pub fn new(texture_size: i32, capacity: usize) -> Self { Self { - tile_size: TILE_SIZE, - provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE as i32), + tile_size: tiles::TILE_SIZE, + provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE), transforms: Vec::with_capacity(capacity), textures: Vec::with_capacity(capacity), grid: HashMap::with_capacity(capacity), diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 676a75249c..bc500c308a 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -1,8 +1,8 @@ +use std::collections::{HashMap, HashSet}; +use skia_safe as skia; use crate::render::Surfaces; use crate::uuid::Uuid; use crate::view::Viewbox; -use skia_safe as skia; -use std::collections::{HashMap, HashSet}; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub struct Tile(pub i32, pub i32); @@ -10,12 +10,26 @@ impl Tile { pub fn from(x: i32, y: i32) -> Self { Tile(x, y) } + + #[inline(always)] pub fn x(&self) -> i32 { self.0 } + + #[inline(always)] pub fn y(&self) -> i32 { self.1 } + + #[inline(always)] + pub fn get_rect_with_offset(&self, offset: &skia::Point) -> skia::Rect { + skia::Rect::from_xywh( + self.0 as f32 * TILE_SIZE - offset.x, + self.1 as f32 * TILE_SIZE - offset.y, + TILE_SIZE, + TILE_SIZE, + ) + } } #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -134,7 +148,7 @@ pub struct TileViewbox { } impl TileViewbox { - pub fn new_with_interest(viewbox: Viewbox, interest: i32) -> Self { + pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self { Self { visible_rect: get_tiles_for_viewbox(viewbox), interest_rect: get_tiles_for_viewbox_with_interest(viewbox, interest), @@ -143,7 +157,7 @@ impl TileViewbox { } } - pub fn update(&mut self, viewbox: Viewbox) { + pub fn update(&mut self, viewbox: &Viewbox) { self.visible_rect = get_tiles_for_viewbox(viewbox); self.interest_rect = get_tiles_for_viewbox_with_interest(viewbox, self.interest); self.center = get_tile_center_for_viewbox(viewbox); @@ -161,6 +175,7 @@ impl TileViewbox { pub const TILE_SIZE: f32 = 512.; +#[inline(always)] pub fn get_tile_dimensions() -> skia::ISize { (TILE_SIZE as i32, TILE_SIZE as i32).into() } @@ -175,17 +190,17 @@ pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> TileRect { TileRect(sx, sy, ex, ey) } -pub fn get_tiles_for_viewbox(viewbox: Viewbox) -> TileRect { +pub fn get_tiles_for_viewbox(viewbox: &Viewbox) -> TileRect { let tile_size = get_tile_size(viewbox.get_scale()); get_tiles_for_rect(viewbox.area, tile_size) } -pub fn get_tiles_for_viewbox_with_interest(viewbox: Viewbox, interest: i32) -> TileRect { +pub fn get_tiles_for_viewbox_with_interest(viewbox: &Viewbox, interest: i32) -> TileRect { let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox); TileRect(sx - interest, sy - interest, ex + interest, ey + interest) } -pub fn get_tile_center_for_viewbox(viewbox: Viewbox) -> Tile { +pub fn get_tile_center_for_viewbox(viewbox: &Viewbox) -> Tile { let TileRect(sx, sy, ex, ey) = get_tiles_for_viewbox(viewbox); Tile((ex - sx) / 2, (ey - sy) / 2) } @@ -207,7 +222,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect { skia::Rect::from_xywh(tx, ty, ts, ts) } -// This structure is usseful to keep all the shape uuids by shape id. +// This structure is useful to keep all the shape uuids by shape id. pub struct TileHashMap { grid: HashMap>, index: HashMap>, @@ -261,12 +276,19 @@ impl TileHashMap { } const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12; -const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = 64; +const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = VIEWPORT_DEFAULT_CAPACITY; // This structure keeps the list of tiles that are in the pending list, the // ones that are going to be rendered. pub struct PendingTiles { pub list: Vec, + + // This reduces memory allocation. + visible_cached: Vec, + visible_uncached: Vec, + interest_cached: Vec, + interest_uncached: Vec, + pub spiral: Vec, pub spiral_rect: TileRect, } @@ -275,15 +297,21 @@ impl PendingTiles { pub fn new() -> Self { Self { list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + + visible_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + visible_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + interest_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + interest_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY), + spiral: Vec::with_capacity(VIEWPORT_SPIRAL_DEFAULT_CAPACITY), spiral_rect: TileRect::empty(), } } // Generate tiles in spiral order from center - fn generate_spiral(columns: usize, rows: usize) -> Vec { + fn generate_spiral(&mut self, columns: usize, rows: usize) { let total = columns * rows; - let mut result = Vec::with_capacity(total); + let mut cx = 0; let mut cy = 0; @@ -294,8 +322,8 @@ impl PendingTiles { let mut direction_total_y = 1; let mut direction = 0; - result.push(Tile(cx, cy)); - while result.len() < total { + self.spiral.push(Tile(cx, cy)); + while self.spiral.len() < total { match direction { 0 => cx += 1, 1 => cy += 1, @@ -304,7 +332,7 @@ impl PendingTiles { _ => unreachable!("Invalid direction"), } - result.push(Tile(cx, cy)); + self.spiral.push(Tile(cx, cy)); direction_current += 1; let direction_total = if direction % 2 == 0 { @@ -323,8 +351,7 @@ impl PendingTiles { direction_current = 0; } } - result.reverse(); - result + self.spiral.reverse(); } pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) { @@ -349,8 +376,7 @@ impl PendingTiles { // the spiral should not change. let total = (spiral_rect.width() * spiral_rect.height()) as usize; if self.spiral.len() < total { - self.spiral = - Self::generate_spiral(spiral_rect.width() as usize, spiral_rect.height() as usize); + self.generate_spiral(spiral_rect.width() as usize, spiral_rect.height() as usize); } // Partition tiles into 4 priority groups (highest priority = processed last due to pop()): @@ -358,10 +384,10 @@ impl PendingTiles { // 2. visible + uncached (user sees these, render next) // 3. interest + cached (pre-rendered area, blit from cache) // 4. interest + uncached (lowest priority - background pre-render) - let mut visible_cached = Vec::new(); - let mut visible_uncached = Vec::new(); - let mut interest_cached = Vec::new(); - let mut interest_uncached = Vec::new(); + self.visible_cached.clear(); + self.visible_uncached.clear(); + self.interest_cached.clear(); + self.interest_uncached.clear(); let center_tile = Tile(spiral_rect.center_x(), spiral_rect.center_y()); for spiral_tile in self.spiral.iter() { @@ -370,19 +396,19 @@ impl PendingTiles { let is_cached = surfaces.has_cached_tile_surface(tile); match (is_visible, is_cached) { - (true, true) => visible_cached.push(tile), - (true, false) => visible_uncached.push(tile), - (false, true) => interest_cached.push(tile), - (false, false) => interest_uncached.push(tile), + (true, true) => self.visible_cached.push(tile), + (true, false) => self.visible_uncached.push(tile), + (false, true) => self.interest_cached.push(tile), + (false, false) => self.interest_uncached.push(tile), } } // Build final list with lowest priority first (they get popped last) // Order: interest_uncached, interest_cached, visible_uncached, visible_cached - self.list.extend(interest_uncached); - self.list.extend(interest_cached); - self.list.extend(visible_uncached); - self.list.extend(visible_cached); + self.list.extend(self.interest_uncached.iter()); + self.list.extend(self.interest_cached.iter()); + self.list.extend(self.visible_uncached.iter()); + self.list.extend(self.visible_cached.iter()); } pub fn pop(&mut self) -> Option {