Soft-drain GPU during recursive drop-shadow passes

Frame drop shadows re-render every child silhouette inside a single
walker node, so the usual partial drain cadence (every N tree nodes)
lets GPU ops pile up. Flush every few shadow passes instead.
This commit is contained in:
Alejandro Alonso 2026-08-13 12:50:55 +02:00
parent 072730574d
commit f25a2c701f
3 changed files with 38 additions and 0 deletions

View File

@ -30,6 +30,9 @@
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
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.
- 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

@ -424,6 +424,8 @@ pub(crate) struct RenderState {
/// Visible tiles were already presented this pass; interest-ring fill may
/// still be running. Final Full should not re-present.
pub viewport_presented: bool,
/// Drop-shadow silhouette ops since the last shadow cadence drain (reset per tile).
shadow_gpu_ops_since_drain: i32,
}
pub struct InteractiveDragCrop {
@ -609,6 +611,7 @@ impl RenderState {
tile_atlas_flushed: false,
drop_shadows_ops_warmed: false,
viewport_presented: false,
shadow_gpu_ops_since_drain: 0,
})
}
@ -2298,6 +2301,7 @@ impl RenderState {
// reorder by distance to the center.
self.current_tile = None;
self.viewport_presented = false;
self.shadow_gpu_ops_since_drain = 0;
}
pub fn start_render_loop(
@ -2719,6 +2723,23 @@ impl RenderState {
crate::get_gpu_state().context.flush(None);
}
/// 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`]
/// alone (which runs every N tree nodes).
#[inline]
fn maybe_drain_after_shadow_gpu_op(&mut self) {
let every = self.options.partial_gpu_drain_shadow_every_n;
if every <= 0 {
return;
}
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();
}
}
/// 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).
@ -3136,6 +3157,7 @@ impl RenderState {
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.maybe_drain_after_shadow_gpu_op();
return Ok(());
}
@ -3179,6 +3201,7 @@ impl RenderState {
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.maybe_drain_after_shadow_gpu_op();
return Ok(());
}
@ -3260,6 +3283,7 @@ impl RenderState {
drop_canvas.restore();
}
self.maybe_drain_after_shadow_gpu_op();
Ok(())
}
@ -3379,6 +3403,7 @@ impl RenderState {
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
self.maybe_drain_after_shadow_gpu_op();
}
}
}
@ -3413,6 +3438,7 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT);
self.maybe_drain_after_shadow_gpu_op();
Ok(true)
}
@ -3925,6 +3951,7 @@ impl RenderState {
self.current_tile_had_shapes = false;
self.tile_atlas_flushed = false;
self.drop_shadows_ops_warmed = false;
self.shadow_gpu_ops_since_drain = 0;
let viewer_masked_pass = self.viewer_masked_pass();

View File

@ -14,6 +14,10 @@ const NODE_BATCH_THRESHOLD: i32 = 3;
/// buffers bounded when many shapes paint cheaply to Current (release packs
/// far more per budget than debug).
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;
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
const ANTIALIAS_THRESHOLD: f32 = 7.0;
#[derive(Debug, Copy, Clone, PartialEq)]
@ -35,6 +39,9 @@ pub struct RenderOptions {
/// Soft-flush GPU every N nodes during progressive tile walks (see
/// [`PARTIAL_GPU_DRAIN_EVERY_N`]).
pub partial_gpu_drain_every_n: i32,
/// Soft-flush GPU every N drop-shadow silhouette passes (see
/// [`PARTIAL_GPU_DRAIN_SHADOW_EVERY_N`]).
pub partial_gpu_drain_shadow_every_n: i32,
pub blur_downscale_threshold: f32,
pub capture_frames: i32,
}
@ -52,6 +59,7 @@ impl Default for RenderOptions {
max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
node_batch_threshold: NODE_BATCH_THRESHOLD,
partial_gpu_drain_every_n: PARTIAL_GPU_DRAIN_EVERY_N,
partial_gpu_drain_shadow_every_n: PARTIAL_GPU_DRAIN_SHADOW_EVERY_N,
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
capture_frames: 0,
}