🔧 Split the tile rebuild into a chunked async loop

This commit is contained in:
Elena Torro 2026-03-09 11:59:15 +01:00
parent c76985abee
commit 2b16f9631e
4 changed files with 129 additions and 4 deletions

View File

@ -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

View File

@ -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<i32> {
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<()> {

View File

@ -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<Uuid>,
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");

View File

@ -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);
}