fix(servo): stop forcing paint+present on every ensure

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.
This commit is contained in:
2026-05-18 15:39:14 -04:00
parent d1e811b724
commit 0a97c4af13
+12 -1
View File
@@ -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))