From 43b12bc4b91c7e3ed9c3ee88b5bc92f5cdffa8c2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 7 Aug 2026 09:45:39 +0200 Subject: [PATCH] :zap: Soft-drain GPU mid-walk on progressive Partials (#11127) Release packs far more cheap Current draws (e.g. fills_none paths) into one Partial than debug; a single end-of-Partial flush_and_submit then stalls the browser. Soft-flush every N walker nodes (and on Partial yield) keeps ops buffers bounded while Full still submits via present_frame. --- render-wasm/src/render.rs | 28 ++++++++++++++++++++-------- render-wasm/src/render/options.rs | 8 ++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 5403eb98b6..8aa067dae2 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1386,8 +1386,8 @@ impl RenderState { // the layered path without Fills/Strokes blits. // Non-SrcOver blend, frame clip blur, and masked groups stay layered. // Stroke-only (fills_none) can go direct: empty fills are a no-op and - // strokes paint into Current. Requires Partial GPU drain (dc1ab) so - // large SVG-icon files do not backlog commands until Full present. + // strokes paint into Current. Large files need mid-walk GPU drains so + // release builds do not backlog a huge ops buffer in one Partial. let can_render_directly = apply_to_current_surface && offset.is_none() && parent_shadows.is_none() @@ -2461,12 +2461,9 @@ impl RenderState { panic!("FrameType::None"); } FrameType::Partial => { - // Drain tile GPU work (Current / tile atlas / cache) without - // presenting Target and without re-snapshotting the tile atlas — - // composition stays deferred to Full. A Backbuffer flush alone - // left commands queued until present_frame's flush_and_submit, - // which stalled the browser on large files. - crate::get_gpu_state().context.flush_and_submit(); + // Final soft drain for this yield (mid-walk also drains; see + // `drain_partial_gpu_soft`). Full still submits via present_frame. + Self::drain_partial_gpu_soft(); } FrameType::Full => { // A full-quality frame is now complete. Rebuild the per-shape crop @@ -2669,6 +2666,15 @@ impl RenderState { true } + /// Soft-drain GPU command buffers during progressive tile walks. + /// Release packs far more cheap Current draws (e.g. fills_none paths) into + /// one Partial than debug; flushing only at Partial end then stalls. Call + /// periodically so each flush stays small. Full present still submits. + #[inline] + fn drain_partial_gpu_soft() { + crate::get_gpu_state().context.flush(None); + } + /// 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). @@ -3775,6 +3781,12 @@ impl RenderState { if allow_stop && self.should_stop_rendering(iteration, timestamp) { return Ok((is_empty, true)); } + // Keep GPU ops buffers bounded when many shapes paint cheaply to + // Current (release packs far more per Partial than debug). + let drain_every = self.options.partial_gpu_drain_every_n; + if allow_stop && drain_every > 0 && iteration > 0 && iteration % drain_every == 0 { + Self::drain_partial_gpu_soft(); + } iteration += 1; } diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index fed66505fe..5dfe0ac2fb 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -11,6 +11,10 @@ const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1; const MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 2; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 3; +/// Soft-drain GPU every N walker nodes on progressive Partials. Keeps ops +/// 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; const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0; const ANTIALIAS_THRESHOLD: f32 = 7.0; #[derive(Debug, Copy, Clone, PartialEq)] @@ -29,6 +33,9 @@ pub struct RenderOptions { pub dpr_viewport_interest_area_threshold: i32, pub max_blocking_time_ms: i32, pub node_batch_threshold: i32, + /// Soft-flush GPU every N nodes during progressive tile walks (see + /// [`PARTIAL_GPU_DRAIN_EVERY_N`]). + pub partial_gpu_drain_every_n: i32, pub blur_downscale_threshold: f32, pub capture_frames: i32, } @@ -45,6 +52,7 @@ impl Default for RenderOptions { dpr_viewport_interest_area_threshold: VIEWPORT_INTEREST_AREA_THRESHOLD, max_blocking_time_ms: MAX_BLOCKING_TIME_MS, node_batch_threshold: NODE_BATCH_THRESHOLD, + partial_gpu_drain_every_n: PARTIAL_GPU_DRAIN_EVERY_N, blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD, capture_frames: 0, }