diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 4a6fa5c2b8..f0bd78b351 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1417,7 +1417,7 @@ ;; this implicitly (`zoom_changed`); this extends it to pan/resize-triggered ;; ends (e.g. selecting a shape opens the options panel and resizes the ;; viewport), which previously blanked. - (internal-render 0 RENDER-FLAG-SYNC-TILES) + (internal-render (js/performance.now) RENDER-FLAG-SYNC-TILES) ;; The direct render above bypasses the rAF `render` loop, so repaint the ;; editor overlay explicitly. Only when this was a full frame: a progressive ;; render keeps painting through the rAF loop and its partial frames must not @@ -1432,7 +1432,7 @@ (if (view-gesture-active?) ;; Pan/zoom pause: render without ending the interaction. (do - (internal-render 0 RENDER-FLAG-SYNC-TILES) + (internal-render (js/performance.now) RENDER-FLAG-SYNC-TILES) (render-text-editor-overlay-after-frame!)) (finalize-view-interaction!))))] (fns/debounce do-render DEBOUNCE_DELAY_MS))) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index e389378777..2630de271f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2368,6 +2368,7 @@ impl RenderState { allow_stop: bool, ) -> Result { performance::begin_measure!("continue_render_loop"); + let timestamp = self.render_budget_start(timestamp); let frame_type = self.render_shape_tree_partial(base_object, tree, timestamp, allow_stop)?; @@ -2433,6 +2434,7 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, ) -> Result { + let timestamp = self.render_budget_start(timestamp); self.render_shape_tree_partial(base_object, tree, timestamp, false)?; // Same composition as `continue_render_loop` for full frames: snapshot only the @@ -2567,6 +2569,24 @@ impl RenderState { Ok((data.as_bytes().to_vec(), width, height)) } + /// Anchor the progressive render budget to wall-clock now when the + /// caller-provided timestamp is unusable: + /// - Frontend sometimes passes `0` (finalize-view / debounced zoom-end). + /// - rAF may hand a timestamp that is already older than the budget when + /// the handler runs late. Using that stamp made `should_stop_rendering` + /// yield after a few nodes with ~0ms of real work. + #[inline] + fn render_budget_start(&self, timestamp: i32) -> i32 { + let now = performance::get_time(); + if timestamp <= 0 { + return now; + } + if now - timestamp > self.options.max_blocking_time_ms { + return now; + } + timestamp + } + #[inline] pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool { if iteration % self.options.node_batch_threshold != 0 {