From f25a2c701fc2c28242c5aeb44ea3a31121bc2e93 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 13 Aug 2026 12:50:55 +0200 Subject: [PATCH] :zap: 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. --- .../render-wasm/ffi-rendering-subtleties.md | 3 +++ render-wasm/src/render.rs | 27 +++++++++++++++++++ render-wasm/src/render/options.rs | 8 ++++++ 3 files changed, 38 insertions(+) diff --git a/.serena/memories/render-wasm/ffi-rendering-subtleties.md b/.serena/memories/render-wasm/ffi-rendering-subtleties.md index 62d8ee152e..a7d760f2a1 100644 --- a/.serena/memories/render-wasm/ffi-rendering-subtleties.md +++ b/.serena/memories/render-wasm/ffi-rendering-subtleties.md @@ -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. diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index cd9e2aa122..9e6bac299a 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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(); diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index b071a0ae90..2d90be49e1 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -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, }