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::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
env,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc, OnceLock,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
@@ -52,6 +53,27 @@ static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
|||||||
const SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(20);
|
const SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
const SCREENSHOT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
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)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct ServoSurfaceSize {
|
pub struct ServoSurfaceSize {
|
||||||
width: u32,
|
width: u32,
|
||||||
@@ -391,10 +413,22 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
let rendering_context = self.webview(webview_id)?.rendering_context.clone();
|
let rendering_context = self.webview(webview_id)?.rendering_context.clone();
|
||||||
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||||
rendering_context.prepare_for_rendering();
|
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)?;
|
let webview = self.webview(webview_id)?;
|
||||||
|
webview.delegate.mark_frame_presented();
|
||||||
webview.webview.paint();
|
webview.webview.paint();
|
||||||
}
|
}
|
||||||
|
self.wait_for_paint_completion(webview_id);
|
||||||
let rendered_frame = Self::read_rendered_frame(rendering_context.as_ref())?;
|
let rendered_frame = Self::read_rendered_frame(rendering_context.as_ref())?;
|
||||||
rendering_context.present();
|
rendering_context.present();
|
||||||
self.webview(webview_id)?.delegate.mark_frame_presented();
|
self.webview(webview_id)?.delegate.mark_frame_presented();
|
||||||
@@ -552,6 +586,41 @@ impl SoftwareServoHost {
|
|||||||
Ok(webview)
|
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(
|
fn read_rendered_frame(
|
||||||
rendering_context: &dyn RenderingContext,
|
rendering_context: &dyn RenderingContext,
|
||||||
) -> Result<RenderedFrame, ServoHostError> {
|
) -> Result<RenderedFrame, ServoHostError> {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ impl HostWebViewDelegate {
|
|||||||
self.title.borrow().clone()
|
self.title.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_pending_frame(&self) -> bool {
|
pub(super) fn has_pending_frame(&self) -> bool {
|
||||||
self.has_pending_frame.get()
|
self.has_pending_frame.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user