Split recursive frame shadows across partial render frames

After pan/zoom, a full-quality tile walk could spend one rAF inside a
single frame drop shadow re-rendering every child silhouette. Yield
shadow work across partial frames when allow_stop is set, resume
in-progress passes before the walker continues, and flush Filter/
DropShadows surfaces after each heavy pass (default every 1).

AI-assisted-by: composer-2.5-fast
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alejandro Alonso 2026-08-13 13:20:49 +02:00
parent f25a2c701f
commit 94e2e26786
3 changed files with 493 additions and 32 deletions

View File

@ -31,8 +31,8 @@
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
compose+present only.
- Drop-shadow cadence: walker soft-drains every N tree nodes; recursive frame shadows
also soft-drain every M silhouette/filter passes (`partial_gpu_drain_shadow_every_n`,
default 2) because one frame node can trigger many child re-renders.
flush Filter/DropShadows/TextDropShadows every silhouette pass (default every 1)
because one frame node can trigger many child re-renders with blur filter surfaces.
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.

View File

@ -64,7 +64,7 @@ pub enum RenderFlag {
SyncTiles = 4,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct NodeRenderState {
pub id: Uuid,
// We use this bool to keep that we've traversed all the children inside this node.
@ -77,6 +77,42 @@ pub struct NodeRenderState {
mask: bool,
// True when this container was flattened (enter/exit skipped).
flattened: bool,
/// Drop shadows for this node were finished by a deferred progressive pass.
drop_shadow_done: bool,
/// Skip enter/blur when resuming after progressive shadows that yielded post-enter.
skip_shape_enter: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DropShadowPhase {
Children,
Colorize,
Composite,
}
/// Progressive drop-shadow work split across rAF partial frames.
#[derive(Debug, Clone)]
struct InProgressDropShadow {
node_render_state: NodeRenderState,
element_id: Uuid,
pending_shadows: Vec<Shadow>,
current_shadow: Shadow,
child_ids: Vec<Uuid>,
child_index: usize,
phase: DropShadowPhase,
clip_bounds: Option<ClipStack>,
target_surface: SurfaceId,
scale: f32,
element_extrect: Rect,
inherited_layer_blur: Option<Blur>,
rendered_any: bool,
/// True when progressive shadow work started after `render_shape_enter`.
shape_enter_done: bool,
}
enum DropShadowResult {
Complete(bool),
Yielded,
}
/// Get simplified children of a container, flattening nested flattened containers
@ -426,6 +462,12 @@ pub(crate) struct RenderState {
pub viewport_presented: bool,
/// Drop-shadow silhouette ops since the last shadow cadence drain (reset per tile).
shadow_gpu_ops_since_drain: i32,
/// True while `render_shape_tree_partial_uncached` runs with `allow_stop`.
progressive_render: bool,
/// Wall-clock anchor for progressive yield checks during shadow passes.
render_budget_timestamp: i32,
/// Drop-shadow pass resumed on the next partial frame.
in_progress_drop_shadow: Option<InProgressDropShadow>,
}
pub struct InteractiveDragCrop {
@ -612,6 +654,9 @@ impl RenderState {
drop_shadows_ops_warmed: false,
viewport_presented: false,
shadow_gpu_ops_since_drain: 0,
progressive_render: false,
render_budget_timestamp: 0,
in_progress_drop_shadow: None,
})
}
@ -1120,7 +1165,11 @@ impl RenderState {
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
if self.tile_atlas_flushed {
crate::get_gpu_state().context.flush_and_submit();
if self.progressive_render {
crate::get_gpu_state().context.flush(None);
} else {
crate::get_gpu_state().context.flush_and_submit();
}
}
self.surfaces.draw_current_tile_into_tile_atlas(
@ -2302,6 +2351,8 @@ impl RenderState {
self.current_tile = None;
self.viewport_presented = false;
self.shadow_gpu_ops_since_drain = 0;
self.in_progress_drop_shadow = None;
self.progressive_render = false;
}
pub fn start_render_loop(
@ -2626,6 +2677,8 @@ impl RenderState {
visited_mask: false,
mask: false,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
});
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
}
@ -2723,6 +2776,17 @@ impl RenderState {
crate::get_gpu_state().context.flush(None);
}
/// Flush the offscreen surfaces used by drop-shadow passes, then drain the
/// GrContext. Shadow blur/filter work records into Filter and DropShadows;
/// a context-only flush leaves those op chains unbounded on WebGL.
#[inline]
pub(crate) fn drain_shadow_gpu_soft(&mut self) {
self.surfaces.flush(SurfaceId::Filter);
self.surfaces.flush(SurfaceId::DropShadows);
self.surfaces.flush(SurfaceId::TextDropShadows);
Self::drain_partial_gpu_soft();
}
/// Count one expensive drop-shadow GPU pass and soft-drain on the shadow
/// cadence. Frame/group shadows re-render many children inside a single
/// walker node, so they need tighter flushing than [`Self::drain_partial_gpu_soft`]
@ -2736,10 +2800,27 @@ impl RenderState {
self.shadow_gpu_ops_since_drain += 1;
if self.shadow_gpu_ops_since_drain >= every {
self.shadow_gpu_ops_since_drain = 0;
Self::drain_partial_gpu_soft();
self.drain_shadow_gpu_soft();
}
}
/// Text silhouettes in a shadow pass run a full `render_shape`; flush
/// immediately so glyph/filter ops do not pile up behind one counter tick.
#[inline]
fn should_yield_render_budget(&self) -> bool {
if !self.progressive_render {
return false;
}
performance::get_time() - self.render_budget_timestamp
> self.options.max_blocking_time_ms
}
#[inline]
fn drain_shadow_gpu_after_text_pass(&mut self) {
self.shadow_gpu_ops_since_drain = 0;
self.drain_shadow_gpu_soft();
}
/// Skip all drop/inner shadows in fast mode, or when even a large design-space
/// shadow would be subpixel. Otherwise filter per shadow via
/// [`Shadow::is_perceptible_at_scale_for`] (stricter for recursive shapes).
@ -2918,6 +2999,8 @@ impl RenderState {
visited_mask: true,
mask: false,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
});
if let Some(&mask_id) = element.mask_id() {
self.pending_nodes.push(NodeRenderState {
@ -2927,6 +3010,8 @@ impl RenderState {
visited_mask: false,
mask: true,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
});
}
}
@ -3283,15 +3368,13 @@ impl RenderState {
drop_canvas.restore();
}
self.maybe_drain_after_shadow_gpu_op();
self.drain_shadow_gpu_soft();
Ok(())
}
/// Renders element drop shadows to DropShadows surface and composites to Current.
/// Used for both normal shadow rendering and pre-layer rendering (frame_clip_layer_blur).
/// Returns `true` when at least one visible drop shadow was composited.
/// Synchronous drop-shadow render (exports, sync tiles, non-recursive shapes).
#[allow(clippy::too_many_arguments)]
fn render_element_drop_shadows_and_composite(
fn render_element_drop_shadows_sync(
&mut self,
element: &Shape,
tree: ShapesPoolRef,
@ -3301,10 +3384,6 @@ impl RenderState {
node_render_state: &NodeRenderState,
target_surface: SurfaceId,
) -> Result<bool> {
// Avoid a blank DropShadows→Current blit + clear when nothing will paint
// (no shadows, fast/overview skip, or all footprints subpixel). Callers
// must still touch DropShadows once per tile when this returns false
// (see `drop_shadows_ops_warmed`).
if self.should_skip_drop_shadows()
|| !element
.drop_shadows_visible()
@ -3403,7 +3482,7 @@ impl RenderState {
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.maybe_drain_after_shadow_gpu_op();
self.drain_shadow_gpu_after_text_pass();
}
}
}
@ -3416,12 +3495,24 @@ impl RenderState {
.draw_paint(&paint);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.drain_shadow_gpu_soft();
}
if !rendered_any {
return Ok(false);
}
self.composite_drop_shadows_to_target(element, clip_bounds, scale, target_surface);
Ok(true)
}
fn composite_drop_shadows_to_target(
&mut self,
element: &Shape,
clip_bounds: Option<ClipStack>,
scale: f32,
target_surface: SurfaceId,
) {
if let Some(clips) = clip_bounds.as_ref() {
let antialias = !self.options.is_fast_mode()
&& element.should_use_antialias(scale, self.options.antialias_threshold);
@ -3438,8 +3529,339 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT);
self.maybe_drain_after_shadow_gpu_op();
Ok(true)
self.drain_shadow_gpu_soft();
}
fn render_drop_shadow_child_silhouette(
&mut self,
element: &Shape,
shadow_shape_id: &Uuid,
shadow: &Shadow,
node_render_state: &NodeRenderState,
scale: f32,
inherited_layer_blur: Option<Blur>,
target_surface: SurfaceId,
tree: ShapesPoolRef,
) -> Result<()> {
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
return Ok(());
};
if shadow_shape.hidden {
return Ok(());
}
let nested_clip_bounds = node_render_state.get_nested_shadow_clip_bounds(element, shadow);
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
self.render_drop_black_shadow(
shadow_shape,
&shadow_shape.extrect(tree, scale),
shadow,
nested_clip_bounds,
scale,
inherited_layer_blur,
target_surface,
)
} else {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().color = skia::Color::BLACK;
transformed_shadow.to_mut().blur = transformed_shadow.blur;
transformed_shadow.to_mut().spread = transformed_shadow.spread;
let mut new_shadow_paint = skia::Paint::default();
new_shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter());
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
self.with_nested_blurs_suppressed(|state| {
state.render_shape(
shadow_shape,
nested_clip_bounds,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
true,
None,
Some(vec![new_shadow_paint.clone()]),
None,
target_surface,
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.drain_shadow_gpu_after_text_pass();
Ok(())
}
}
fn begin_drop_shadow_progressive(
&mut self,
element: &Shape,
tree: ShapesPoolRef,
extrect: &mut Option<Rect>,
clip_bounds: Option<ClipStack>,
scale: f32,
node_render_state: &NodeRenderState,
target_surface: SurfaceId,
shape_enter_done: bool,
) -> Result<DropShadowResult> {
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
let inherited_layer_blur = match element.shape_type {
Type::Frame(_) | Type::Group(_) => element.blur,
_ => None,
};
let mut shadows: Vec<Shadow> = element
.drop_shadows_visible()
.filter(|s| s.is_perceptible_at_scale_for(scale, element.is_recursive()))
.cloned()
.collect();
if shadows.is_empty() {
return Ok(DropShadowResult::Complete(false));
}
let current_shadow = shadows.remove(0);
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
self.render_drop_black_shadow(
element,
element_extrect,
&current_shadow,
clip_bounds.clone(),
scale,
None,
target_surface,
)?;
self.drain_shadow_gpu_soft();
let child_ids = get_simplified_children(tree, element);
let progress = InProgressDropShadow {
node_render_state: node_render_state.clone(),
element_id: element.id,
pending_shadows: shadows,
current_shadow,
child_ids,
child_index: 0,
phase: DropShadowPhase::Children,
clip_bounds,
target_surface,
scale,
element_extrect: *element_extrect,
inherited_layer_blur,
rendered_any: true,
shape_enter_done,
};
self.in_progress_drop_shadow = Some(progress);
self.advance_drop_shadow_progressive(element, tree, false)
}
fn advance_drop_shadow_progressive(
&mut self,
element: &Shape,
tree: ShapesPoolRef,
requeue_on_complete: bool,
) -> Result<DropShadowResult> {
let Some(mut progress) = self.in_progress_drop_shadow.take() else {
return Ok(DropShadowResult::Complete(false));
};
let node_render_state = progress.node_render_state.clone();
let result = self.advance_drop_shadow_progressive_inner(
element,
tree,
&node_render_state,
&mut progress,
);
match result {
Ok(DropShadowResult::Yielded) => {
self.in_progress_drop_shadow = Some(progress);
Ok(DropShadowResult::Yielded)
}
Ok(DropShadowResult::Complete(rendered)) => {
self.in_progress_drop_shadow = None;
if requeue_on_complete {
let mut ns = progress.node_render_state;
ns.drop_shadow_done = true;
ns.skip_shape_enter = progress.shape_enter_done;
self.pending_nodes.push(ns);
}
Ok(DropShadowResult::Complete(rendered))
}
Err(e) => {
self.in_progress_drop_shadow = Some(progress);
Err(e)
}
}
}
fn advance_drop_shadow_progressive_inner(
&mut self,
element: &Shape,
tree: ShapesPoolRef,
node_render_state: &NodeRenderState,
progress: &mut InProgressDropShadow,
) -> Result<DropShadowResult> {
loop {
match progress.phase {
DropShadowPhase::Children => {
while progress.child_index < progress.child_ids.len() {
let child_id = progress.child_ids[progress.child_index];
progress.child_index += 1;
self.render_drop_shadow_child_silhouette(
element,
&child_id,
&progress.current_shadow,
node_render_state,
progress.scale,
progress.inherited_layer_blur,
progress.target_surface,
tree,
)?;
self.drain_shadow_gpu_soft();
if self.should_yield_render_budget() {
return Ok(DropShadowResult::Yielded);
}
}
progress.phase = DropShadowPhase::Colorize;
}
DropShadowPhase::Colorize => {
let mut paint = skia::Paint::default();
paint.set_color(progress.current_shadow.color);
paint.set_blend_mode(skia::BlendMode::SrcIn);
self.surfaces
.canvas(SurfaceId::DropShadows)
.draw_paint(&paint);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.drain_shadow_gpu_soft();
if progress.pending_shadows.is_empty() {
progress.phase = DropShadowPhase::Composite;
} else {
progress.current_shadow = progress.pending_shadows.remove(0);
progress.child_index = 0;
progress.phase = DropShadowPhase::Children;
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
self.render_drop_black_shadow(
element,
&progress.element_extrect,
&progress.current_shadow,
progress.clip_bounds.clone(),
progress.scale,
None,
progress.target_surface,
)?;
self.drain_shadow_gpu_soft();
if self.should_yield_render_budget() {
return Ok(DropShadowResult::Yielded);
}
}
}
DropShadowPhase::Composite => {
self.composite_drop_shadows_to_target(
element,
progress.clip_bounds.clone(),
progress.scale,
progress.target_surface,
);
return Ok(DropShadowResult::Complete(progress.rendered_any));
}
}
}
}
fn resume_drop_shadow_progressive(
&mut self,
tree: ShapesPoolRef,
) -> Result<DropShadowResult> {
let element_id = self
.in_progress_drop_shadow
.as_ref()
.map(|p| p.element_id)
.ok_or_else(|| Error::CriticalError("Missing in-progress drop shadow".into()))?;
let Some(element) = tree.get(&element_id) else {
self.in_progress_drop_shadow = None;
return Ok(DropShadowResult::Complete(false));
};
self.advance_drop_shadow_progressive(element, tree, true)
}
/// Renders element drop shadows to DropShadows surface and composites to Current.
/// Used for both normal shadow rendering and pre-layer rendering (frame_clip_layer_blur).
#[allow(clippy::too_many_arguments)]
fn render_element_drop_shadows_and_composite(
&mut self,
element: &Shape,
tree: ShapesPoolRef,
extrect: &mut Option<Rect>,
clip_bounds: Option<ClipStack>,
scale: f32,
node_render_state: &NodeRenderState,
target_surface: SurfaceId,
shape_enter_done: bool,
) -> Result<DropShadowResult> {
if self.should_skip_drop_shadows()
|| !element
.drop_shadows_visible()
.any(|s| s.is_perceptible_at_scale_for(scale, element.is_recursive()))
{
return Ok(DropShadowResult::Complete(false));
}
if node_render_state.drop_shadow_done {
return Ok(DropShadowResult::Complete(false));
}
let use_progressive = self.progressive_render
&& element.is_recursive()
&& !matches!(element.shape_type, Type::Bool(_))
&& !get_simplified_children(tree, element).is_empty();
if use_progressive {
if self
.in_progress_drop_shadow
.as_ref()
.is_some_and(|p| p.element_id == element.id)
{
return self.advance_drop_shadow_progressive(element, tree, false);
}
return self.begin_drop_shadow_progressive(
element,
tree,
extrect,
clip_bounds,
scale,
node_render_state,
target_surface,
shape_enter_done,
);
}
let rendered = self.render_element_drop_shadows_sync(
element,
tree,
extrect,
clip_bounds,
scale,
node_render_state,
target_surface,
)?;
Ok(DropShadowResult::Complete(rendered))
}
pub fn render_shape_tree_partial_uncached(
@ -3452,6 +3874,9 @@ impl RenderState {
let mut iteration = 0;
let mut is_empty = true;
self.progressive_render = allow_stop;
self.render_budget_timestamp = performance::get_time();
let mut target_surface = SurfaceId::Current;
if export {
target_surface = SurfaceId::Export;
@ -3499,6 +3924,16 @@ impl RenderState {
None
};
if self.in_progress_drop_shadow.is_some() {
match self.resume_drop_shadow_progressive(tree)? {
DropShadowResult::Yielded => return Ok((false, true)),
DropShadowResult::Complete(true) => {
self.drop_shadows_ops_warmed = true;
}
DropShadowResult::Complete(false) => {}
}
}
while let Some(node_render_state) = self.pending_nodes.pop() {
let node_id = node_render_state.id;
let visited_children = node_render_state.visited_children;
@ -3557,6 +3992,8 @@ impl RenderState {
visited_mask: false,
mask: false,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
});
}
}
@ -3681,8 +4118,8 @@ impl RenderState {
.drop_shadows_visible()
.any(|s| s.is_perceptible_at_scale_for(scale, element.is_recursive()));
if shadow_before_layer
&& self.render_element_drop_shadows_and_composite(
if shadow_before_layer && !node_render_state.drop_shadow_done {
match self.render_element_drop_shadows_and_composite(
element,
tree,
&mut extrect,
@ -3690,18 +4127,28 @@ impl RenderState {
scale,
&node_render_state,
target_surface,
)?
{
self.drop_shadows_ops_warmed = true;
false,
)? {
DropShadowResult::Yielded => return Ok((is_empty, true)),
DropShadowResult::Complete(true) => {
self.drop_shadows_ops_warmed = true;
}
DropShadowResult::Complete(false) => {}
}
}
// Render background blur BEFORE save_layer so it modifies
// the backdrop independently of the shape's opacity.
if !node_render_state.is_root() && self.focus_mode.is_active() {
if !node_render_state.skip_shape_enter
&& !node_render_state.is_root()
&& self.focus_mode.is_active()
{
self.render_background_blur(element, target_surface);
}
self.render_shape_enter(element, mask, clip_bounds.as_ref(), target_surface);
if !node_render_state.skip_shape_enter {
self.render_shape_enter(element, mask, clip_bounds.as_ref(), target_surface);
}
}
if !node_render_state.is_root() && self.focus_mode.is_active() {
@ -3709,13 +4156,16 @@ impl RenderState {
let skip_shadows = self.should_skip_drop_shadows();
// Skip shadow block when already rendered before the layer (frame_clip_layer_blur)
let shadows_already_rendered = Self::frame_clip_layer_blur(element).is_some();
// or when a deferred progressive pass finished shadows for this node.
let shadows_already_rendered = node_render_state.drop_shadow_done
|| Self::frame_clip_layer_blur(element).is_some();
// For text shapes, render drop shadow using text rendering logic
if !skip_shadows
&& !shadows_already_rendered
&& !matches!(element.shape_type, Type::Text(_))
&& self.render_element_drop_shadows_and_composite(
{
match self.render_element_drop_shadows_and_composite(
element,
tree,
&mut extrect,
@ -3723,10 +4173,14 @@ impl RenderState {
scale,
&node_render_state,
target_surface,
)?
{
// Real shadow composite already clears DropShadows.
self.drop_shadows_ops_warmed = true;
true,
)? {
DropShadowResult::Yielded => return Ok((is_empty, true)),
DropShadowResult::Complete(true) => {
self.drop_shadows_ops_warmed = true;
}
DropShadowResult::Complete(false) => {}
}
}
if !self.drop_shadows_ops_warmed {
@ -3807,6 +4261,8 @@ impl RenderState {
visited_mask: false,
mask,
flattened: can_flatten,
drop_shadow_done: false,
skip_shape_enter: false,
});
if element.is_recursive() {
@ -3841,6 +4297,8 @@ impl RenderState {
visited_mask: false,
mask: false,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
});
}
}
@ -3952,6 +4410,7 @@ impl RenderState {
self.tile_atlas_flushed = false;
self.drop_shadows_ops_warmed = false;
self.shadow_gpu_ops_since_drain = 0;
self.in_progress_drop_shadow = None;
let viewer_masked_pass = self.viewer_masked_pass();
@ -4006,6 +4465,8 @@ impl RenderState {
visited_mask: false,
mask: false,
flattened: false,
drop_shadow_done: false,
skip_shape_enter: false,
}));
} else {
// Visible tiles finished. Promote deferred interest-ring work

View File

@ -16,8 +16,8 @@ const NODE_BATCH_THRESHOLD: i32 = 3;
const PARTIAL_GPU_DRAIN_EVERY_N: i32 = 64;
/// Soft-drain during recursive drop-shadow passes (each silhouette or filter
/// surface). The walker counts tree nodes, not shadow children re-rendered
/// inside one frame, so frame shadows need a tighter cadence.
const PARTIAL_GPU_DRAIN_SHADOW_EVERY_N: i32 = 2;
/// inside one frame, so frame shadows drain after every heavy pass by default.
const PARTIAL_GPU_DRAIN_SHADOW_EVERY_N: i32 = 1;
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
const ANTIALIAS_THRESHOLD: f32 = 7.0;
#[derive(Debug, Copy, Clone, PartialEq)]