diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 82fb16ebaa..0b65bbec69 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -946,15 +946,26 @@ (:y position))] (= result 1))) +(defn- tile-rebuild-loop + "Process tile rebuild in chunks via requestAnimationFrame, then render." + [] + (js/requestAnimationFrame + (fn [ts] + (when wasm/context-initialized? + (let [has-more (h/call wasm/internal-module "_tile_rebuild_step" ts)] + (if (pos? has-more) + (tile-rebuild-loop) + (render ts))))))) + (def render-finish - (letfn [(do-render [ts] + (letfn [(do-render [_ts] ;; Check if context is still initialized before executing ;; to prevent errors when navigating quickly (when wasm/context-initialized? (perf/begin-measure "render-finish") - (h/call wasm/internal-module "_set_view_end") - (render ts) - (perf/end-measure "render-finish")))] + (h/call wasm/internal-module "_set_view_end_async") + (perf/end-measure "render-finish") + (tile-rebuild-loop)))] (fns/debounce do-render DEBOUNCE_DELAY_MS))) (def render-pan diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6e519cc249..7ccf53dace 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -346,6 +346,50 @@ pub extern "C" fn set_view_end() -> Result<()> { Ok(()) } +/// Like set_view_end but uses chunked tile rebuild to avoid blocking +/// the main thread. Prepares the view state and starts the async +/// tile rebuild process. Call `tile_rebuild_step` in a rAF loop after this. +#[no_mangle] +#[wasm_error] +pub extern "C" fn set_view_end_async() -> Result<()> { + with_state_mut!(state, { + performance::begin_measure!("set_view_end_async"); + state.render_state.options.set_fast_mode(false); + state.render_state.cancel_animation_frame(); + + let scale = state.render_state.get_scale(); + state + .render_state + .tile_viewbox + .update(state.render_state.viewbox, scale); + + if state.render_state.options.is_profile_rebuild_tiles() { + // Profile mode still uses sync rebuild + state.rebuild_tiles(); + } else { + state.start_tile_rebuild(); + } + + state.render_state.sync_cached_viewbox(); + performance::end_measure!("set_view_end_async"); + }); + Ok(()) +} + +/// Process a chunk of the tile rebuild. Returns 1 if more work remains, 0 if done. +#[no_mangle] +#[wasm_error] +pub extern "C" fn tile_rebuild_step(timestamp: i32) -> Result { + let result = with_state_mut!(state, { + if state.process_tile_rebuild_step(timestamp) { + 1 + } else { + 0 + } + }); + Ok(result) +} + #[no_mangle] #[wasm_error] pub extern "C" fn clear_focus_mode() -> Result<()> { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ce5781ca63..28697700e1 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -300,6 +300,10 @@ pub(crate) struct RenderState { pub ignore_nested_blurs: bool, /// Preview render mode - when true, uses simplified rendering for progressive loading pub preview_mode: bool, + /// State for chunked tile rebuild across animation frames + tile_rebuild_pending_nodes: Vec, + tile_rebuild_zoom_changed: bool, + tile_rebuild_in_progress: bool, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { @@ -372,6 +376,9 @@ impl RenderState { touched_ids: HashSet::default(), ignore_nested_blurs: false, preview_mode: false, + tile_rebuild_pending_nodes: vec![], + tile_rebuild_zoom_changed: false, + tile_rebuild_in_progress: false, } } @@ -2541,6 +2548,60 @@ impl RenderState { performance::end_measure!("rebuild_tiles_shallow"); } + /// Initialize a chunked tile rebuild that can be spread across multiple + /// animation frames to avoid blocking the main thread. + pub fn start_tile_rebuild(&mut self, tree: ShapesPoolRef) { + self.tile_rebuild_zoom_changed = self.zoom_changed(); + + // Collect top-level shape ids (children of the root node) + let mut nodes = Vec::new(); + if let Some(root) = tree.get(&Uuid::nil()) { + for child_id in root.children_ids_iter(false) { + nodes.push(*child_id); + } + } + + self.tile_rebuild_pending_nodes = nodes; + self.tile_rebuild_in_progress = !self.tile_rebuild_pending_nodes.is_empty(); + } + + /// Process a batch of shapes for tile rebuild within a time budget. + /// Returns true if there is more work remaining. + pub fn process_tile_rebuild_step(&mut self, tree: ShapesPoolRef, timestamp: i32) -> bool { + if !self.tile_rebuild_in_progress { + return false; + } + + performance::begin_measure!("process_tile_rebuild_step"); + + while let Some(shape_id) = self.tile_rebuild_pending_nodes.pop() { + if let Some(shape) = tree.get(&shape_id) { + if self.tile_rebuild_zoom_changed { + let _ = self.update_shape_tiles(shape, tree); + } else { + let _ = self.update_shape_tiles_incremental(shape, tree); + } + } + + // Check time budget every few shapes + if performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS { + performance::end_measure!("process_tile_rebuild_step"); + return true; + } + } + + // All shapes processed — finalize + self.tile_rebuild_in_progress = false; + self.surfaces.remove_cached_tiles(self.background_color); + + performance::end_measure!("process_tile_rebuild_step"); + false + } + + pub fn is_tile_rebuild_in_progress(&self) -> bool { + self.tile_rebuild_in_progress + } + pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) { performance::begin_measure!("rebuild_tiles"); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b99b768334..45863a7d7b 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -211,6 +211,15 @@ impl State { self.render_state.rebuild_tiles_shallow(&self.shapes); } + pub fn start_tile_rebuild(&mut self) { + self.render_state.start_tile_rebuild(&self.shapes); + } + + pub fn process_tile_rebuild_step(&mut self, timestamp: i32) -> bool { + self.render_state + .process_tile_rebuild_step(&self.shapes, timestamp) + } + pub fn rebuild_tiles(&mut self) { self.render_state.rebuild_tiles_from(&self.shapes, None); }