From 0a97c4af13f0dc43e6d2b1519f076d36a11f04e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 18 May 2026 15:39:14 -0400 Subject: [PATCH] fix(servo): stop forcing paint+present on every ensure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Servo's reference embedder (\`examples/winit_minimal.rs\`) is reactive: - \`user_event\` only calls \`servo.spin_event_loop()\` — never paints - \`notify_new_frame_ready\` is what flips the embedder into the redraw path (calls \`window.request_redraw()\`) - \`RedrawRequested\` is the **single** site that runs \`webview.paint() + rendering_context.present()\` Painting is therefore at-most-once-per-real-frame: a paint happens only when Servo has actually composited new content. Our \`ServoLiveClient::ensure\` was forcing \`paint_without_readback_with_completion\` after every setup pass. \`ensure\` runs on every \`ensure_surface\` invocation — viewport debounce settle, URL redirect (google.com → / → /?zx=…), permission update, native-surface re-attach, page-zoom change. Each invocation called Servo's painter while it had no new composited frame ready: WebRender's \`clear_background\` wiped the surface to the configured shell background colour, the empty scene rendered, \`present\` swapped that blank surface to the NSView's CALayer — and the user saw a white flash. A chain of redirects produced a chain of flashes. Drop the unconditional \`paint\` from \`ensure\`. Keep the \`host.tick()\` (Servo's spin) so the navigate / viewport / input messages reach the constellation in this turn, but defer painting to \`poll\`, which already gates \`paint_without_readback_with_completion\` on \`snapshot.has_pending_frame()\` — exactly mirroring the reference embedder's reactive model. \`ServoLiveFrame\` returned from \`ensure\` only carries snapshot metadata, so callers that depend on the frame value (worker queue, UI state) are unaffected. --- crates/ely_app/src/services/servo_live.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/ely_app/src/services/servo_live.rs b/crates/ely_app/src/services/servo_live.rs index ac6eb5a..0ac8c0b 100644 --- a/crates/ely_app/src/services/servo_live.rs +++ b/crates/ely_app/src/services/servo_live.rs @@ -53,11 +53,22 @@ impl ServoLiveClient { self.apply_permissions(&request, &webview_id, &profile_id)?; self.apply_navigation(&request, &webview_id, tab_id, requested_url)?; self.apply_input(&request, &webview_id)?; + // Match Servo's `examples/winit_minimal.rs`: spin the event loop on + // the embedder-side hot path, never paint. Painting is reactive in + // Servo — `notify_new_frame_ready` on the delegate flags the + // session, and the next `poll` (gated on `has_pending_frame`) + // performs the single paint + present for that frame. Forcing a + // paint here would clear the surface to the WebRender background + // and present it *before* Servo has composited the navigated + // page, which is what produced the per-redirect white-flash on + // sites that perform a chain of redirects (google.com → / + // → /?zx=…). The `ServoLiveFrame` we return only carries the + // snapshot metadata; the on-screen surface is owned by Servo via + // the native NSView and updated through `poll`. self.host.tick(); if !self.session_uses_native_surface(&request.tab_id) { return Err(ServoLiveError::NativeSurfaceUnavailable); } - self.host.paint_without_readback_with_completion(&webview_id, false)?; let frame = self.frame_from_session(&request.tab_id, &webview_id)?; Ok(Some(frame))