T15: paint barrier — pump event loop until framebuffer is consistent
`webview.paint()` dispatches a render command to Servo's paint thread asynchronously, so the subsequent `read_to_image()` raced the paint thread and reliably returned cleared-white pixels on data: URLs (T10.8). Clear `has_pending_frame` before dispatching paint, then spin the Servo event loop until `notify_new_frame_ready` re-arms it or 32 ms elapse (override via `ELY_PAINT_BARRIER_MS`). Bench: software path now ships real RGBA bytes for 60 frames instead of all-white.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
env,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc,
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
@@ -52,6 +53,27 @@ static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
const SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const SCREENSHOT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
||||
|
||||
/// Default upper bound on how long `paint()` will spin the Servo event
|
||||
/// loop waiting for `notify_new_frame_ready` after dispatching
|
||||
/// `webview.paint()`. 32 ms is two 60 Hz frames — enough headroom for
|
||||
/// the paint thread to land a real framebuffer before we read it back,
|
||||
/// short enough that a stuck paint can't stall the input/render loop.
|
||||
/// Overridable via `ELY_PAINT_BARRIER_MS`; `0` disables the barrier and
|
||||
/// restores the pre-T15 "fire and read" behaviour.
|
||||
const DEFAULT_PAINT_BARRIER_MS: u64 = 32;
|
||||
const PAINT_BARRIER_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
||||
|
||||
fn paint_barrier_budget() -> Duration {
|
||||
static BUDGET: OnceLock<Duration> = OnceLock::new();
|
||||
*BUDGET.get_or_init(|| {
|
||||
let ms = env::var("ELY_PAINT_BARRIER_MS")
|
||||
.ok()
|
||||
.and_then(|raw| raw.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_PAINT_BARRIER_MS);
|
||||
Duration::from_millis(ms)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ServoSurfaceSize {
|
||||
width: u32,
|
||||
@@ -391,10 +413,22 @@ impl ServoHost for SoftwareServoHost {
|
||||
let rendering_context = self.webview(webview_id)?.rendering_context.clone();
|
||||
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
rendering_context.prepare_for_rendering();
|
||||
// `webview.paint()` dispatches a render command to Servo's paint
|
||||
// thread — it does NOT block until the framebuffer is consistent.
|
||||
// Without a barrier, `read_rendered_frame` below races the paint
|
||||
// thread and reliably reads the cleared-white state on data: URLs.
|
||||
// Clear the pending-frame flag first so we can detect the *next*
|
||||
// `notify_new_frame_ready` (the one our `paint()` triggers), then
|
||||
// pump the event loop until Servo reports the new frame is ready
|
||||
// or `paint_barrier_budget()` elapses. On timeout we fall through
|
||||
// and read anyway, preserving the pre-T15 fast path for callers
|
||||
// that explicitly disable the barrier with `ELY_PAINT_BARRIER_MS=0`.
|
||||
{
|
||||
let webview = self.webview(webview_id)?;
|
||||
webview.delegate.mark_frame_presented();
|
||||
webview.webview.paint();
|
||||
}
|
||||
self.wait_for_paint_completion(webview_id);
|
||||
let rendered_frame = Self::read_rendered_frame(rendering_context.as_ref())?;
|
||||
rendering_context.present();
|
||||
self.webview(webview_id)?.delegate.mark_frame_presented();
|
||||
@@ -552,6 +586,41 @@ impl SoftwareServoHost {
|
||||
Ok(webview)
|
||||
}
|
||||
|
||||
/// Spin Servo's event loop until the webview's delegate observes a
|
||||
/// fresh `notify_new_frame_ready` callback (i.e. the framebuffer is
|
||||
/// consistent for readback) or [`paint_barrier_budget`] elapses. The
|
||||
/// caller is responsible for clearing the pending-frame flag before
|
||||
/// dispatching `webview.paint()`; otherwise this returns immediately
|
||||
/// off the *previous* frame and the race is preserved.
|
||||
///
|
||||
/// Returns silently on timeout — `paint()` falls through to
|
||||
/// `read_rendered_frame` so callers still get whatever pixels the
|
||||
/// rendering context currently holds. That keeps the fast path open
|
||||
/// when `ELY_PAINT_BARRIER_MS=0` disables the budget entirely, and
|
||||
/// matches the pre-T15 behaviour on the (rare) case where Servo
|
||||
/// can't land a frame inside two refresh intervals.
|
||||
fn wait_for_paint_completion(&mut self, webview_id: &WebViewId) {
|
||||
let budget = paint_barrier_budget();
|
||||
if budget.is_zero() {
|
||||
return;
|
||||
}
|
||||
let started_at = Instant::now();
|
||||
loop {
|
||||
self.servo.spin_event_loop();
|
||||
let ready = self
|
||||
.webviews
|
||||
.get(webview_id)
|
||||
.is_some_and(|webview| webview.delegate.has_pending_frame());
|
||||
if ready {
|
||||
return;
|
||||
}
|
||||
if started_at.elapsed() >= budget {
|
||||
return;
|
||||
}
|
||||
thread::sleep(PAINT_BARRIER_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_rendered_frame(
|
||||
rendering_context: &dyn RenderingContext,
|
||||
) -> Result<RenderedFrame, ServoHostError> {
|
||||
|
||||
@@ -98,7 +98,7 @@ impl HostWebViewDelegate {
|
||||
self.title.borrow().clone()
|
||||
}
|
||||
|
||||
fn has_pending_frame(&self) -> bool {
|
||||
pub(super) fn has_pending_frame(&self) -> bool {
|
||||
self.has_pending_frame.get()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user