mirror of
https://github.com/penpot/penpot.git
synced 2026-05-10 10:38:17 +00:00
🔧 Split the tile rebuild into a chunked async loop
This commit is contained in:
parent
c76985abee
commit
996f973317
@ -946,15 +946,32 @@
|
||||
(:y position))]
|
||||
(= result 1)))
|
||||
|
||||
(defn- tile-rebuild-loop
|
||||
"Process tile rebuild in chunks via requestAnimationFrame, rendering
|
||||
on each frame so partial results are visible. The generation counter
|
||||
ensures stale loops (from a previous zoom/pan) stop immediately."
|
||||
[generation]
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(when wasm/context-initialized?
|
||||
(let [has-more (h/call wasm/internal-module "_tile_rebuild_step" ts generation)]
|
||||
(render ts)
|
||||
(when (pos? has-more)
|
||||
(tile-rebuild-loop generation)))))))
|
||||
|
||||
(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")))]
|
||||
(let [generation (h/call wasm/internal-module "_set_view_end")]
|
||||
(perf/end-measure "render-finish")
|
||||
(if (pos? generation)
|
||||
;; Async rebuild started — drive it with rAF loop
|
||||
(tile-rebuild-loop generation)
|
||||
;; Profile mode did sync rebuild — just render once
|
||||
(render (js/performance.now))))))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(def render-pan
|
||||
|
||||
@ -305,45 +305,55 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Returns a generation counter (> 0) when async rebuild was started,
|
||||
/// or 0 when profile mode used sync rebuild (no loop needed).
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let _end_start = performance::begin_timed_log!("set_view_end");
|
||||
pub extern "C" fn set_view_end() -> Result<i32> {
|
||||
let generation = with_state_mut!(state, {
|
||||
performance::begin_measure!("set_view_end");
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
|
||||
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
|
||||
// This is critical because we limit tiles to the interest area for optimization
|
||||
let scale = state.render_state.get_scale();
|
||||
state
|
||||
.render_state
|
||||
.tile_viewbox
|
||||
.update(state.render_state.viewbox, scale);
|
||||
|
||||
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
|
||||
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
let gen = if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
// Profile mode uses sync rebuild — no async loop needed
|
||||
state.rebuild_tiles();
|
||||
0i32
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
state.start_tile_rebuild() as i32
|
||||
};
|
||||
|
||||
state.render_state.sync_cached_viewbox();
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
#[cfg(feature = "profile-macros")]
|
||||
{
|
||||
let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START };
|
||||
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
|
||||
gen
|
||||
});
|
||||
Ok(generation)
|
||||
}
|
||||
|
||||
/// Process a chunk of the tile rebuild. Returns 1 if more work remains, 0 if done.
|
||||
/// `generation` must match the value returned by `set_view_end`; stale
|
||||
/// loops from a previous rebuild are stopped immediately.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn tile_rebuild_step(timestamp: i32, generation: i32) -> Result<i32> {
|
||||
let result = with_state_mut!(state, {
|
||||
if state.process_tile_rebuild_step(timestamp, generation as u32) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@ -300,6 +300,11 @@ 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,
|
||||
/// Generation counter to detect stale rebuild loops after re-entrant zoom/pan
|
||||
tile_rebuild_generation: u32,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@ -372,6 +377,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_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2514,17 +2522,14 @@ impl RenderState {
|
||||
// because tiles are rendered at specific zoom levels
|
||||
let zoom_changed = self.zoom_changed();
|
||||
|
||||
let mut tiles_to_invalidate = HashSet::<tiles::Tile>::new();
|
||||
let mut nodes = vec![Uuid::nil()];
|
||||
while let Some(shape_id) = nodes.pop() {
|
||||
if let Some(shape) = tree.get(&shape_id) {
|
||||
if shape_id != Uuid::nil() {
|
||||
if zoom_changed {
|
||||
// Zoom changed: use full update that tracks all affected tiles
|
||||
tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree));
|
||||
let _ = self.update_shape_tiles(shape, tree);
|
||||
} else {
|
||||
// Pan only: use incremental update that preserves valid cached tiles
|
||||
self.update_shape_tiles_incremental(shape, tree);
|
||||
let _ = self.update_shape_tiles_incremental(shape, tree);
|
||||
}
|
||||
} else {
|
||||
// We only need to rebuild tiles from the first level.
|
||||
@ -2541,6 +2546,72 @@ 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.
|
||||
/// Returns the generation counter so the caller can detect stale loops.
|
||||
pub fn start_tile_rebuild(&mut self, tree: ShapesPoolRef) -> u32 {
|
||||
self.tile_rebuild_zoom_changed = self.zoom_changed();
|
||||
self.tile_rebuild_generation = self.tile_rebuild_generation.wrapping_add(1);
|
||||
// Skip 0 — the CLJS caller uses 0 to mean "sync rebuild, no loop needed"
|
||||
if self.tile_rebuild_generation == 0 {
|
||||
self.tile_rebuild_generation = 1;
|
||||
}
|
||||
|
||||
// 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_generation
|
||||
}
|
||||
|
||||
/// Process a batch of shapes for tile rebuild within a time budget.
|
||||
/// Returns true if there is more work remaining. Returns false if done
|
||||
/// or if `generation` doesn't match (stale loop from a previous rebuild).
|
||||
pub fn process_tile_rebuild_step(
|
||||
&mut self,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
generation: u32,
|
||||
) -> bool {
|
||||
if generation != self.tile_rebuild_generation {
|
||||
return false;
|
||||
}
|
||||
|
||||
performance::begin_measure!("process_tile_rebuild_step");
|
||||
|
||||
let mut iteration = 0i32;
|
||||
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 {
|
||||
// Return value intentionally discarded — invalidation happens
|
||||
// in bulk via remove_cached_tiles once all shapes are processed.
|
||||
let _ = self.update_shape_tiles(shape, tree);
|
||||
} else {
|
||||
let _ = self.update_shape_tiles_incremental(shape, tree);
|
||||
}
|
||||
}
|
||||
|
||||
iteration += 1;
|
||||
if iteration % NODE_BATCH_THRESHOLD == 0
|
||||
&& performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS
|
||||
{
|
||||
performance::end_measure!("process_tile_rebuild_step");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// All shapes processed — finalize
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
|
||||
performance::end_measure!("process_tile_rebuild_step");
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
|
||||
performance::begin_measure!("rebuild_tiles");
|
||||
|
||||
|
||||
@ -211,6 +211,15 @@ impl State {
|
||||
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn start_tile_rebuild(&mut self) -> u32 {
|
||||
self.render_state.start_tile_rebuild(&self.shapes)
|
||||
}
|
||||
|
||||
pub fn process_tile_rebuild_step(&mut self, timestamp: i32, generation: u32) -> bool {
|
||||
self.render_state
|
||||
.process_tile_rebuild_step(&self.shapes, timestamp, generation)
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles(&mut self) {
|
||||
self.render_state.rebuild_tiles_from(&self.shapes, None);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user