Split Servo live rendering modules
This commit is contained in:
@@ -16,6 +16,8 @@ use thiserror::Error;
|
||||
mod args;
|
||||
#[path = "ely_servo_sidecar/live.rs"]
|
||||
mod live;
|
||||
#[path = "ely_servo_sidecar/live_output.rs"]
|
||||
mod live_output;
|
||||
#[path = "ely_servo_sidecar/live_protocol.rs"]
|
||||
mod live_protocol;
|
||||
#[path = "ely_servo_sidecar/perf.rs"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
io::{self, BufRead, Write},
|
||||
io::{self, BufRead},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -14,11 +14,12 @@ use ely_servo_host::{
|
||||
};
|
||||
|
||||
use super::args::LiveArgs;
|
||||
use super::live_output::{populate_surface_fields, write_outcome};
|
||||
pub(super) use super::live_protocol::LiveSidecarError;
|
||||
use super::live_protocol::{
|
||||
LiveFrameReport, LiveOutcome, LiveRequest, LiveSitePermission, PartialFrameTimings,
|
||||
};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
|
||||
|
||||
/// Per-`Ensure` budget for Servo to paint after input dispatch.
|
||||
/// 250 ms catches the common click + paint round trip within the
|
||||
@@ -179,115 +180,6 @@ fn handle_request(
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate the hardware surface protocol fields on `outcome`. Two
|
||||
/// pieces of state ride out together:
|
||||
///
|
||||
/// * `current_surface_id` — set on every payload-bearing hardware
|
||||
/// frame so the receiver knows which previously-imported
|
||||
/// `MTLTexture` to sample THIS frame. surfman's attached swap
|
||||
/// chain rotates front/back surfaces, so this alternates between
|
||||
/// a small set of ids.
|
||||
/// * `surface_handle` — populated only the first time the sidecar
|
||||
/// sees a given `surface_id`; the receiver imports the IOSurface
|
||||
/// once and caches the resulting Metal texture. Minting a fresh
|
||||
/// mach port per frame would leak ports — `IOSurfaceCreateMachPort`
|
||||
/// hands out a new send right each call and they don't free
|
||||
/// automatically until the receiver `mach_port_deallocate`s.
|
||||
fn populate_surface_fields(
|
||||
host: &SoftwareServoHost,
|
||||
webview_id: &ely_domain::WebViewId,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &mut HashMap<String, HashSet<u64>>,
|
||||
outcome: &mut LiveOutcome,
|
||||
) {
|
||||
if outcome.frame.is_none() {
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
{
|
||||
let Ok(Some(identity)) = host.peek_iosurface_identity(webview_id) else {
|
||||
return;
|
||||
};
|
||||
outcome.response.current_surface_id = Some(identity.surface_id);
|
||||
let seen = published_surface_ids.entry(tab_id.to_string()).or_default();
|
||||
if seen.contains(&identity.surface_id) {
|
||||
return;
|
||||
}
|
||||
let Ok(Some(handle)) = host.current_iosurface_handle(webview_id) else {
|
||||
return;
|
||||
};
|
||||
seen.insert(handle.surface_id);
|
||||
outcome.response.surface_handle = Some(handle);
|
||||
}
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
{
|
||||
let _ = (host, webview_id, tab_id, published_surface_ids);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialise the response then stream the optional raw RGBA frame on
|
||||
/// the same stdout pipe. The client reads the JSON line, takes
|
||||
/// `rgba_byte_count` from the report, then reads that many bytes
|
||||
/// from the same stream — no temp file round-trip.
|
||||
///
|
||||
/// After the bytes hit the pipe we fold paint/encode/write/total
|
||||
/// timings into the aggregator. `total_ns` is the wall-clock span
|
||||
/// from `frame_started_at` (request arrival) to the stdout flush
|
||||
/// returning, so it captures every per-frame cost outside the three
|
||||
/// measured stages. Any summary the aggregator emits is stashed on
|
||||
/// `pending_summary` and rides out on the *next* response, because
|
||||
/// the protocol is one-line-per-response and an unsolicited summary
|
||||
/// line would desync the main process's read loop.
|
||||
fn write_outcome(
|
||||
stdout: &mut impl Write,
|
||||
perf: &mut FramePerfAggregator,
|
||||
pending_summary: &mut Option<FramePerfSummary>,
|
||||
outcome: Result<LiveOutcome, LiveSidecarError>,
|
||||
frame_started_at: Instant,
|
||||
) -> Result<(), LiveSidecarError> {
|
||||
let mut outcome = outcome.unwrap_or_else(|error| LiveOutcome::error(error.to_string()));
|
||||
let partial_timings = outcome.partial_timings.take();
|
||||
let frame_present = outcome.frame.is_some();
|
||||
if let Some(summary) = pending_summary.take() {
|
||||
outcome.response.perf = Some(summary);
|
||||
}
|
||||
// Hardware path: receiver samples the IOSurface directly through
|
||||
// its CVPixelBuffer cache, so the raw RGBA payload is dead
|
||||
// weight. Drop it from the wire (and zero the byte count in the
|
||||
// header so the client knows nothing follows). At 1080p × 60 fps
|
||||
// that's 8 MB × 60 = ~480 MB/s of pipe traffic eliminated.
|
||||
let drop_rgba_payload = outcome.response.current_surface_id.is_some();
|
||||
if drop_rgba_payload
|
||||
&& let Some(report) = outcome.response.frame.as_mut()
|
||||
{
|
||||
report.rgba_byte_count = 0;
|
||||
}
|
||||
let write_started_at = Instant::now();
|
||||
serde_json::to_writer(&mut *stdout, &outcome.response)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
if !drop_rgba_payload
|
||||
&& let Some(frame) = outcome.frame.as_ref()
|
||||
{
|
||||
stdout.write_all(frame.rgba_bytes())?;
|
||||
}
|
||||
stdout.flush()?;
|
||||
if frame_present {
|
||||
let write_ns = elapsed_ns(write_started_at);
|
||||
let total_ns = elapsed_ns(frame_started_at);
|
||||
let partial = partial_timings.unwrap_or(PartialFrameTimings { paint_ns: 0, encode_ns: 0 });
|
||||
let timings = FrameStageTimings::from_durations(
|
||||
Duration::from_nanos(partial.paint_ns),
|
||||
Duration::from_nanos(partial.encode_ns),
|
||||
Duration::from_nanos(write_ns),
|
||||
Duration::from_nanos(total_ns),
|
||||
);
|
||||
if let Some(summary) = perf.record(timings) {
|
||||
*pending_summary = Some(summary);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_session<'a>(
|
||||
host: &mut SoftwareServoHost,
|
||||
sessions: &'a mut HashMap<String, LiveSession>,
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io::Write,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ely_servo_host::SoftwareServoHost;
|
||||
|
||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||
|
||||
/// Populate the hardware surface protocol fields on `outcome`. Two
|
||||
/// pieces of state ride out together:
|
||||
///
|
||||
/// * `current_surface_id` — set on every payload-bearing hardware
|
||||
/// frame so the receiver knows which previously-imported
|
||||
/// `MTLTexture` to sample THIS frame. surfman's attached swap
|
||||
/// chain rotates front/back surfaces, so this alternates between
|
||||
/// a small set of ids.
|
||||
/// * `surface_handle` — populated only the first time the sidecar
|
||||
/// sees a given `surface_id`; the receiver imports the IOSurface
|
||||
/// once and caches the resulting Metal texture. Minting a fresh
|
||||
/// mach port per frame would leak ports — `IOSurfaceCreateMachPort`
|
||||
/// hands out a new send right each call and they don't free
|
||||
/// automatically until the receiver `mach_port_deallocate`s.
|
||||
pub(super) fn populate_surface_fields(
|
||||
host: &SoftwareServoHost,
|
||||
webview_id: &ely_domain::WebViewId,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &mut HashMap<String, HashSet<u64>>,
|
||||
outcome: &mut LiveOutcome,
|
||||
) {
|
||||
if outcome.frame.is_none() {
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
{
|
||||
let Ok(Some(identity)) = host.peek_iosurface_identity(webview_id) else {
|
||||
return;
|
||||
};
|
||||
outcome.response.current_surface_id = Some(identity.surface_id);
|
||||
let seen = published_surface_ids.entry(tab_id.to_string()).or_default();
|
||||
if seen.contains(&identity.surface_id) {
|
||||
return;
|
||||
}
|
||||
let Ok(Some(handle)) = host.current_iosurface_handle(webview_id) else {
|
||||
return;
|
||||
};
|
||||
seen.insert(handle.surface_id);
|
||||
outcome.response.surface_handle = Some(handle);
|
||||
}
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
{
|
||||
let _ = (host, webview_id, tab_id, published_surface_ids);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialise the response then stream the optional raw RGBA frame on
|
||||
/// the same stdout pipe. The client reads the JSON line, takes
|
||||
/// `rgba_byte_count` from the report, then reads that many bytes
|
||||
/// from the same stream — no temp file round-trip.
|
||||
///
|
||||
/// After the bytes hit the pipe we fold paint/encode/write/total
|
||||
/// timings into the aggregator. `total_ns` is the wall-clock span
|
||||
/// from `frame_started_at` (request arrival) to the stdout flush
|
||||
/// returning, so it captures every per-frame cost outside the three
|
||||
/// measured stages. Any summary the aggregator emits is stashed on
|
||||
/// `pending_summary` and rides out on the *next* response, because
|
||||
/// the protocol is one-line-per-response and an unsolicited summary
|
||||
/// line would desync the main process's read loop.
|
||||
pub(super) fn write_outcome(
|
||||
stdout: &mut impl Write,
|
||||
perf: &mut FramePerfAggregator,
|
||||
pending_summary: &mut Option<FramePerfSummary>,
|
||||
outcome: Result<LiveOutcome, LiveSidecarError>,
|
||||
frame_started_at: Instant,
|
||||
) -> Result<(), LiveSidecarError> {
|
||||
let mut outcome = outcome.unwrap_or_else(|error| LiveOutcome::error(error.to_string()));
|
||||
let partial_timings = outcome.partial_timings.take();
|
||||
let frame_present = outcome.frame.is_some();
|
||||
if let Some(summary) = pending_summary.take() {
|
||||
outcome.response.perf = Some(summary);
|
||||
}
|
||||
// Hardware path: receiver samples the IOSurface directly through
|
||||
// its CVPixelBuffer cache, so the raw RGBA payload is dead
|
||||
// weight. Drop it from the wire (and zero the byte count in the
|
||||
// header so the client knows nothing follows). At 1080p × 60 fps
|
||||
// that's 8 MB × 60 = ~480 MB/s of pipe traffic eliminated.
|
||||
let drop_rgba_payload = outcome.response.current_surface_id.is_some();
|
||||
if drop_rgba_payload && let Some(report) = outcome.response.frame.as_mut() {
|
||||
report.rgba_byte_count = 0;
|
||||
}
|
||||
let write_started_at = Instant::now();
|
||||
serde_json::to_writer(&mut *stdout, &outcome.response)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
if !drop_rgba_payload && let Some(frame) = outcome.frame.as_ref() {
|
||||
stdout.write_all(frame.rgba_bytes())?;
|
||||
}
|
||||
stdout.flush()?;
|
||||
if frame_present {
|
||||
let write_ns = elapsed_ns(write_started_at);
|
||||
let total_ns = elapsed_ns(frame_started_at);
|
||||
let partial = partial_timings.unwrap_or(PartialFrameTimings { paint_ns: 0, encode_ns: 0 });
|
||||
let timings = FrameStageTimings::from_durations(
|
||||
Duration::from_nanos(partial.paint_ns),
|
||||
Duration::from_nanos(partial.encode_ns),
|
||||
Duration::from_nanos(write_ns),
|
||||
Duration::from_nanos(total_ns),
|
||||
);
|
||||
if let Some(summary) = perf.record(timings) {
|
||||
*pending_summary = Some(summary);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
env,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
@@ -14,24 +13,16 @@ use std::{
|
||||
|
||||
use dpi::PhysicalSize;
|
||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||
use euclid::Scale;
|
||||
use servo::{
|
||||
DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
|
||||
DeviceVector2D, Opts, RenderingContext, Scroll, Servo, ServoBuilder, WebViewBuilder,
|
||||
WebViewPoint, WebViewVector,
|
||||
DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint,
|
||||
WebViewVector,
|
||||
};
|
||||
|
||||
/// Wrap an `f32` scale factor in Servo's typed `Scale<f32, DeviceIndependentPixel,
|
||||
/// DevicePixel>`. The clamp guards against `NaN`/`inf` reaching Servo's
|
||||
/// layout (which assumes a positive finite scale).
|
||||
fn hidpi_scale_from_factor(scale_factor: f32) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
let safe = if scale_factor.is_finite() && scale_factor > 0.0 {
|
||||
scale_factor.clamp(0.5, 5.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Scale::new(safe)
|
||||
}
|
||||
#[path = "runtime_context.rs"]
|
||||
mod runtime_context;
|
||||
|
||||
use runtime_context::hidpi_scale_from_factor;
|
||||
pub use runtime_context::{RenderingContextKind, ServoSurfaceSize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
@@ -51,72 +42,6 @@ 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,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ServoSurfaceSize {
|
||||
#[must_use]
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width: width.max(1), height: height.max(1) }
|
||||
}
|
||||
|
||||
fn physical(self) -> PhysicalSize<u32> {
|
||||
PhysicalSize { width: self.width, height: self.height }
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the `RenderingContext` implementation each webview gets.
|
||||
///
|
||||
/// `Software` uses Servo's built-in `SoftwareRenderingContext`, which
|
||||
/// rasterises on the CPU. `Hardware` uses the vendored
|
||||
/// [`HardwareOffscreenContext`](crate::HardwareOffscreenContext),
|
||||
/// which rasterises through the real GPU adapter against a
|
||||
/// `SurfaceType::Generic` offscreen surface. The `Hardware` variant
|
||||
/// is only available when the `hardware-render` feature is enabled;
|
||||
/// requesting it without the feature is a configuration error
|
||||
/// surfaced via `ServoHostError::HardwareRenderUnavailable`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RenderingContextKind {
|
||||
#[default]
|
||||
Software,
|
||||
Hardware,
|
||||
}
|
||||
|
||||
/// Pair of rendering-context handles produced by
|
||||
/// [`SoftwareServoHost::new_rendering_context`]. The trait-object
|
||||
/// handle drives Servo's compositor; the concrete hardware handle is
|
||||
/// kept on the side so the host can call macOS-specific methods
|
||||
/// (IOSurface mach port extraction) without downcasting.
|
||||
struct RenderingContextHandles {
|
||||
rendering_context: Rc<dyn RenderingContext>,
|
||||
#[cfg(feature = "hardware-render")]
|
||||
hardware_context: Option<Rc<crate::HardwareOffscreenContext>>,
|
||||
}
|
||||
|
||||
pub struct SoftwareServoHost {
|
||||
servo: Servo,
|
||||
default_surface_size: ServoSurfaceSize,
|
||||
@@ -517,42 +442,6 @@ impl SoftwareServoHost {
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)
|
||||
}
|
||||
|
||||
fn new_rendering_context(
|
||||
&self,
|
||||
size: ServoSurfaceSize,
|
||||
) -> Result<RenderingContextHandles, ServoHostError> {
|
||||
match self.rendering_context_kind {
|
||||
RenderingContextKind::Software => {
|
||||
let rendering_context = Rc::new(
|
||||
servo::SoftwareRenderingContext::new(size.physical())
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
||||
);
|
||||
rendering_context
|
||||
.make_current()
|
||||
.map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
Ok(RenderingContextHandles {
|
||||
rendering_context,
|
||||
#[cfg(feature = "hardware-render")]
|
||||
hardware_context: None,
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "hardware-render")]
|
||||
RenderingContextKind::Hardware => {
|
||||
let hardware = Rc::new(
|
||||
crate::HardwareOffscreenContext::new(size.physical())
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
||||
);
|
||||
hardware.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
Ok(RenderingContextHandles {
|
||||
rendering_context: hardware.clone(),
|
||||
hardware_context: Some(hardware),
|
||||
})
|
||||
}
|
||||
#[cfg(not(feature = "hardware-render"))]
|
||||
RenderingContextKind::Hardware => Err(ServoHostError::HardwareRenderUnavailable),
|
||||
}
|
||||
}
|
||||
|
||||
fn webview(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
|
||||
self.webviews
|
||||
.get(webview_id)
|
||||
@@ -578,58 +467,4 @@ impl SoftwareServoHost {
|
||||
webview.webview.focus();
|
||||
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> {
|
||||
let size = rendering_context.size();
|
||||
let width =
|
||||
i32::try_from(size.width).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||
let height =
|
||||
i32::try_from(size.height).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||
let frame_rect = DeviceIntRect::from_origin_and_size(
|
||||
DeviceIntPoint::new(0, 0),
|
||||
DeviceIntSize::new(width, height),
|
||||
);
|
||||
let image = rendering_context
|
||||
.read_to_image(frame_rect)
|
||||
.ok_or(ServoHostError::RenderedFrameUnavailable)?;
|
||||
|
||||
Ok(RenderedFrame::from_rgba_bytes(size.width, size.height, image.into_raw()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::{
|
||||
env,
|
||||
rc::Rc,
|
||||
sync::OnceLock,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use dpi::PhysicalSize;
|
||||
use euclid::Scale;
|
||||
use servo::{
|
||||
DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel,
|
||||
RenderingContext,
|
||||
};
|
||||
|
||||
use super::SoftwareServoHost;
|
||||
use crate::{RenderedFrame, ServoHostError};
|
||||
|
||||
const DEFAULT_PAINT_BARRIER_MS: u64 = 32;
|
||||
const PAINT_BARRIER_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
||||
|
||||
/// Wrap an `f32` scale factor in Servo's typed `Scale<f32, DeviceIndependentPixel,
|
||||
/// DevicePixel>`. The clamp guards against `NaN`/`inf` reaching Servo's
|
||||
/// layout (which assumes a positive finite scale).
|
||||
pub(super) fn hidpi_scale_from_factor(
|
||||
scale_factor: f32,
|
||||
) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
let safe = if scale_factor.is_finite() && scale_factor > 0.0 {
|
||||
scale_factor.clamp(0.5, 5.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Scale::new(safe)
|
||||
}
|
||||
|
||||
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,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ServoSurfaceSize {
|
||||
#[must_use]
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width: width.max(1), height: height.max(1) }
|
||||
}
|
||||
|
||||
pub(super) fn physical(self) -> PhysicalSize<u32> {
|
||||
PhysicalSize { width: self.width, height: self.height }
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the `RenderingContext` implementation each webview gets.
|
||||
///
|
||||
/// `Software` uses Servo's built-in `SoftwareRenderingContext`, which
|
||||
/// rasterises on the CPU. `Hardware` uses the vendored
|
||||
/// [`HardwareOffscreenContext`](crate::HardwareOffscreenContext),
|
||||
/// which rasterises through the real GPU adapter against a
|
||||
/// `SurfaceType::Generic` offscreen surface. The `Hardware` variant
|
||||
/// is only available when the `hardware-render` feature is enabled;
|
||||
/// requesting it without the feature is a configuration error
|
||||
/// surfaced via `ServoHostError::HardwareRenderUnavailable`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RenderingContextKind {
|
||||
#[default]
|
||||
Software,
|
||||
Hardware,
|
||||
}
|
||||
|
||||
/// Pair of rendering-context handles produced by
|
||||
/// [`SoftwareServoHost::new_rendering_context`]. The trait-object
|
||||
/// handle drives Servo's compositor; the concrete hardware handle is
|
||||
/// kept on the side so the host can call macOS-specific methods
|
||||
/// (IOSurface mach port extraction) without downcasting.
|
||||
pub(super) struct RenderingContextHandles {
|
||||
pub(super) rendering_context: Rc<dyn RenderingContext>,
|
||||
#[cfg(feature = "hardware-render")]
|
||||
pub(super) hardware_context: Option<Rc<crate::HardwareOffscreenContext>>,
|
||||
}
|
||||
|
||||
impl SoftwareServoHost {
|
||||
pub(super) fn new_rendering_context(
|
||||
&self,
|
||||
size: ServoSurfaceSize,
|
||||
) -> Result<RenderingContextHandles, ServoHostError> {
|
||||
match self.rendering_context_kind {
|
||||
RenderingContextKind::Software => {
|
||||
let rendering_context = Rc::new(
|
||||
servo::SoftwareRenderingContext::new(size.physical())
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
||||
);
|
||||
rendering_context
|
||||
.make_current()
|
||||
.map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
Ok(RenderingContextHandles {
|
||||
rendering_context,
|
||||
#[cfg(feature = "hardware-render")]
|
||||
hardware_context: None,
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "hardware-render")]
|
||||
RenderingContextKind::Hardware => {
|
||||
let hardware = Rc::new(
|
||||
crate::HardwareOffscreenContext::new(size.physical())
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
||||
);
|
||||
hardware.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
Ok(RenderingContextHandles {
|
||||
rendering_context: hardware.clone(),
|
||||
hardware_context: Some(hardware),
|
||||
})
|
||||
}
|
||||
#[cfg(not(feature = "hardware-render"))]
|
||||
RenderingContextKind::Hardware => Err(ServoHostError::HardwareRenderUnavailable),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(super) fn wait_for_paint_completion(&mut self, webview_id: &ely_domain::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);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn read_rendered_frame(
|
||||
rendering_context: &dyn RenderingContext,
|
||||
) -> Result<RenderedFrame, ServoHostError> {
|
||||
let size = rendering_context.size();
|
||||
let width =
|
||||
i32::try_from(size.width).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||
let height =
|
||||
i32::try_from(size.height).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||
let frame_rect = DeviceIntRect::from_origin_and_size(
|
||||
DeviceIntPoint::new(0, 0),
|
||||
DeviceIntSize::new(width, height),
|
||||
);
|
||||
let image = rendering_context
|
||||
.read_to_image(frame_rect)
|
||||
.ok_or(ServoHostError::RenderedFrameUnavailable)?;
|
||||
|
||||
Ok(RenderedFrame::from_rgba_bytes(size.width, size.height, image.into_raw()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user