Split Servo live rendering modules
This commit is contained in:
@@ -17,10 +17,17 @@ use std::{
|
|||||||
const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT";
|
const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT";
|
||||||
|
|
||||||
use ely_domain::SitePermissionDecision;
|
use ely_domain::SitePermissionDecision;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[path = "servo_live_wire.rs"]
|
||||||
|
mod wire;
|
||||||
|
|
||||||
use super::servo_sidecar_command::{SidecarCommandError, default_sidecar_command};
|
use super::servo_sidecar_command::{SidecarCommandError, default_sidecar_command};
|
||||||
|
use wire::{
|
||||||
|
LiveFrameReport, LiveRequest, LiveResponse, LiveSurfaceHandle, log_frame_perf,
|
||||||
|
log_iosurface_current, log_iosurface_handle,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use super::iosurface_metal::IOSurfaceCache;
|
use super::iosurface_metal::IOSurfaceCache;
|
||||||
@@ -397,161 +404,6 @@ pub(crate) enum ServoLiveError {
|
|||||||
SidecarCommand(#[from] SidecarCommandError),
|
SidecarCommand(#[from] SidecarCommandError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
enum LiveRequest {
|
|
||||||
Ensure {
|
|
||||||
tab_id: String,
|
|
||||||
profile_id: String,
|
|
||||||
url: String,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
page_zoom_percent: u16,
|
|
||||||
device_pixel_ratio: f32,
|
|
||||||
scroll_delta_x: i32,
|
|
||||||
scroll_delta_y: i32,
|
|
||||||
scroll_point_x: Option<u32>,
|
|
||||||
scroll_point_y: Option<u32>,
|
|
||||||
click_x: Option<u32>,
|
|
||||||
click_y: Option<u32>,
|
|
||||||
hover_x: Option<u32>,
|
|
||||||
hover_y: Option<u32>,
|
|
||||||
typed_text: Option<String>,
|
|
||||||
site_permissions: Vec<ServoLiveSitePermission>,
|
|
||||||
},
|
|
||||||
Poll {
|
|
||||||
tab_id: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LiveResponse {
|
|
||||||
error: Option<String>,
|
|
||||||
frame: Option<LiveFrameReport>,
|
|
||||||
#[serde(default)]
|
|
||||||
perf: Option<LiveFramePerfSummary>,
|
|
||||||
/// Hardware path only: present on the first frame after a new
|
|
||||||
/// IOSurface is bound (initial paint, resize, surfman swap chain
|
|
||||||
/// rotation). T10.4 will turn this into an imported Metal texture;
|
|
||||||
/// for now we log it on the `ely::servo::iosurface` target so the
|
|
||||||
/// pipeline is observable end-to-end without yet wiring it into
|
|
||||||
/// the renderer.
|
|
||||||
#[serde(default)]
|
|
||||||
surface_handle: Option<LiveSurfaceHandle>,
|
|
||||||
/// Hardware path only: which previously-imported IOSurface to
|
|
||||||
/// sample this frame. surfman's attached swap chain rotates the
|
|
||||||
/// bound surface, so this id alternates between the values the
|
|
||||||
/// receiver has already imported via `surface_handle`.
|
|
||||||
#[serde(default)]
|
|
||||||
current_surface_id: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wire mirror of `ely_servo_host::IOSurfaceHandle`. Duplicated rather
|
|
||||||
/// than imported because `ely_app` only talks to the sidecar via
|
|
||||||
/// stdin/stdout JSON — it has no crate dependency on `ely_servo_host`
|
|
||||||
/// and adding one just to share a four-field struct would pull the
|
|
||||||
/// Servo dep tree into the renderer process.
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
|
||||||
struct LiveSurfaceHandle {
|
|
||||||
mach_port_name: u32,
|
|
||||||
surface_id: u64,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Aggregated frame-stage timings rolled up every N frames by the
|
|
||||||
/// sidecar. We accept anything matching the wire shape and let the
|
|
||||||
/// `tracing` event echo the percentiles verbatim — the sidecar is
|
|
||||||
/// the source of truth for histogram boundaries.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LiveFramePerfSummary {
|
|
||||||
window: u32,
|
|
||||||
context: String,
|
|
||||||
paint_p50_us: u64,
|
|
||||||
paint_p95_us: u64,
|
|
||||||
paint_p99_us: u64,
|
|
||||||
encode_p50_us: u64,
|
|
||||||
encode_p95_us: u64,
|
|
||||||
encode_p99_us: u64,
|
|
||||||
write_p50_us: u64,
|
|
||||||
write_p95_us: u64,
|
|
||||||
write_p99_us: u64,
|
|
||||||
total_p50_us: u64,
|
|
||||||
total_p95_us: u64,
|
|
||||||
total_p99_us: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-frame tag that tells the renderer which already-imported
|
|
||||||
/// `MTLTexture` to sample. Emitted at `trace` instead of `info` because
|
|
||||||
/// it fires every frame on the hardware path; the import event above
|
|
||||||
/// is the rare `info` and this trace is the steady-state breadcrumb.
|
|
||||||
fn log_iosurface_current(surface_id: u64) {
|
|
||||||
tracing::trace!(
|
|
||||||
target: "ely::servo::iosurface",
|
|
||||||
surface_id,
|
|
||||||
"iosurface_current",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit one structured `tracing` event per IOSurface handover, on a
|
|
||||||
/// dedicated target so `RUST_LOG=ely::servo::iosurface=info` lights up
|
|
||||||
/// the cross-process surface pipeline without pulling in everything
|
|
||||||
/// else. The renderer (T10.4) will turn the same handle into an
|
|
||||||
/// imported Metal texture; today the event is the observable contract
|
|
||||||
/// that T10.3 plumbing is alive.
|
|
||||||
fn log_iosurface_handle(handle: &LiveSurfaceHandle) {
|
|
||||||
tracing::info!(
|
|
||||||
target: "ely::servo::iosurface",
|
|
||||||
mach_port_name = handle.mach_port_name,
|
|
||||||
surface_id = handle.surface_id,
|
|
||||||
width = handle.width,
|
|
||||||
height = handle.height,
|
|
||||||
"iosurface_handle",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emit one structured `tracing` event per perf summary, on a
|
|
||||||
/// dedicated target so `RUST_LOG=ely::servo::perf=info` flips the
|
|
||||||
/// stream on without dragging the rest of the app along. Filtering
|
|
||||||
/// happens upstream in the subscriber — this call is a single
|
|
||||||
/// pointer + integer push.
|
|
||||||
fn log_frame_perf(summary: &LiveFramePerfSummary) {
|
|
||||||
tracing::info!(
|
|
||||||
target: "ely::servo::perf",
|
|
||||||
window = summary.window,
|
|
||||||
context = %summary.context,
|
|
||||||
paint_p50_us = summary.paint_p50_us,
|
|
||||||
paint_p95_us = summary.paint_p95_us,
|
|
||||||
paint_p99_us = summary.paint_p99_us,
|
|
||||||
encode_p50_us = summary.encode_p50_us,
|
|
||||||
encode_p95_us = summary.encode_p95_us,
|
|
||||||
encode_p99_us = summary.encode_p99_us,
|
|
||||||
write_p50_us = summary.write_p50_us,
|
|
||||||
write_p95_us = summary.write_p95_us,
|
|
||||||
write_p99_us = summary.write_p99_us,
|
|
||||||
total_p50_us = summary.total_p50_us,
|
|
||||||
total_p95_us = summary.total_p95_us,
|
|
||||||
total_p99_us = summary.total_p99_us,
|
|
||||||
"frame_perf",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LiveFrameReport {
|
|
||||||
loaded_url: Option<String>,
|
|
||||||
title: Option<String>,
|
|
||||||
state: String,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
rgba_byte_count: usize,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
non_white_pixel_count: u64,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
content_pixel_count: u64,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
sample_hash: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map `ELY_SERVO_RENDERING_CONTEXT` to a CLI argument value if it's
|
/// Map `ELY_SERVO_RENDERING_CONTEXT` to a CLI argument value if it's
|
||||||
/// one we recognise. Unset variable returns `None` (sidecar uses its
|
/// one we recognise. Unset variable returns `None` (sidecar uses its
|
||||||
/// own default of software); unknown value also returns `None`
|
/// own default of software); unknown value also returns `None`
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::ServoLiveSitePermission;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub(super) enum LiveRequest {
|
||||||
|
Ensure {
|
||||||
|
tab_id: String,
|
||||||
|
profile_id: String,
|
||||||
|
url: String,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
page_zoom_percent: u16,
|
||||||
|
device_pixel_ratio: f32,
|
||||||
|
scroll_delta_x: i32,
|
||||||
|
scroll_delta_y: i32,
|
||||||
|
scroll_point_x: Option<u32>,
|
||||||
|
scroll_point_y: Option<u32>,
|
||||||
|
click_x: Option<u32>,
|
||||||
|
click_y: Option<u32>,
|
||||||
|
hover_x: Option<u32>,
|
||||||
|
hover_y: Option<u32>,
|
||||||
|
typed_text: Option<String>,
|
||||||
|
site_permissions: Vec<ServoLiveSitePermission>,
|
||||||
|
},
|
||||||
|
Poll {
|
||||||
|
tab_id: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct LiveResponse {
|
||||||
|
pub(super) error: Option<String>,
|
||||||
|
pub(super) frame: Option<LiveFrameReport>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) perf: Option<LiveFramePerfSummary>,
|
||||||
|
/// Hardware path only: present on the first frame after a new
|
||||||
|
/// IOSurface is bound (initial paint, resize, surfman swap chain
|
||||||
|
/// rotation). T10.4 will turn this into an imported Metal texture;
|
||||||
|
/// for now we log it on the `ely::servo::iosurface` target so the
|
||||||
|
/// pipeline is observable end-to-end without yet wiring it into
|
||||||
|
/// the renderer.
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) surface_handle: Option<LiveSurfaceHandle>,
|
||||||
|
/// Hardware path only: which previously-imported IOSurface to
|
||||||
|
/// sample this frame. surfman's attached swap chain rotates the
|
||||||
|
/// bound surface, so this id alternates between the values the
|
||||||
|
/// receiver has already imported via `surface_handle`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) current_surface_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wire mirror of `ely_servo_host::IOSurfaceHandle`. Duplicated rather
|
||||||
|
/// than imported because `ely_app` only talks to the sidecar via
|
||||||
|
/// stdin/stdout JSON — it has no crate dependency on `ely_servo_host`
|
||||||
|
/// and adding one just to share a four-field struct would pull the
|
||||||
|
/// Servo dep tree into the renderer process.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
pub(super) struct LiveSurfaceHandle {
|
||||||
|
pub(super) mach_port_name: u32,
|
||||||
|
pub(super) surface_id: u64,
|
||||||
|
pub(super) width: u32,
|
||||||
|
pub(super) height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aggregated frame-stage timings rolled up every N frames by the
|
||||||
|
/// sidecar. We accept anything matching the wire shape and let the
|
||||||
|
/// `tracing` event echo the percentiles verbatim — the sidecar is
|
||||||
|
/// the source of truth for histogram boundaries.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct LiveFramePerfSummary {
|
||||||
|
window: u32,
|
||||||
|
context: String,
|
||||||
|
paint_p50_us: u64,
|
||||||
|
paint_p95_us: u64,
|
||||||
|
paint_p99_us: u64,
|
||||||
|
encode_p50_us: u64,
|
||||||
|
encode_p95_us: u64,
|
||||||
|
encode_p99_us: u64,
|
||||||
|
write_p50_us: u64,
|
||||||
|
write_p95_us: u64,
|
||||||
|
write_p99_us: u64,
|
||||||
|
total_p50_us: u64,
|
||||||
|
total_p95_us: u64,
|
||||||
|
total_p99_us: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct LiveFrameReport {
|
||||||
|
pub(super) loaded_url: Option<String>,
|
||||||
|
pub(super) title: Option<String>,
|
||||||
|
pub(super) state: String,
|
||||||
|
pub(super) width: u32,
|
||||||
|
pub(super) height: u32,
|
||||||
|
pub(super) rgba_byte_count: usize,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
pub(super) non_white_pixel_count: u64,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
pub(super) content_pixel_count: u64,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
pub(super) sample_hash: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-frame tag that tells the renderer which already-imported
|
||||||
|
/// `MTLTexture` to sample. Emitted at `trace` instead of `info` because
|
||||||
|
/// it fires every frame on the hardware path; the import event above
|
||||||
|
/// is the rare `info` and this trace is the steady-state breadcrumb.
|
||||||
|
pub(super) fn log_iosurface_current(surface_id: u64) {
|
||||||
|
tracing::trace!(
|
||||||
|
target: "ely::servo::iosurface",
|
||||||
|
surface_id,
|
||||||
|
"iosurface_current",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit one structured `tracing` event per IOSurface handover, on a
|
||||||
|
/// dedicated target so `RUST_LOG=ely::servo::iosurface=info` lights up
|
||||||
|
/// the cross-process surface pipeline without pulling in everything
|
||||||
|
/// else. The renderer (T10.4) will turn the same handle into an
|
||||||
|
/// imported Metal texture; today the event is the observable contract
|
||||||
|
/// that T10.3 plumbing is alive.
|
||||||
|
pub(super) fn log_iosurface_handle(handle: &LiveSurfaceHandle) {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ely::servo::iosurface",
|
||||||
|
mach_port_name = handle.mach_port_name,
|
||||||
|
surface_id = handle.surface_id,
|
||||||
|
width = handle.width,
|
||||||
|
height = handle.height,
|
||||||
|
"iosurface_handle",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit one structured `tracing` event per perf summary, on a
|
||||||
|
/// dedicated target so `RUST_LOG=ely::servo::perf=info` flips the
|
||||||
|
/// stream on without dragging the rest of the app along. Filtering
|
||||||
|
/// happens upstream in the subscriber — this call is a single
|
||||||
|
/// pointer + integer push.
|
||||||
|
pub(super) fn log_frame_perf(summary: &LiveFramePerfSummary) {
|
||||||
|
tracing::info!(
|
||||||
|
target: "ely::servo::perf",
|
||||||
|
window = summary.window,
|
||||||
|
context = %summary.context,
|
||||||
|
paint_p50_us = summary.paint_p50_us,
|
||||||
|
paint_p95_us = summary.paint_p95_us,
|
||||||
|
paint_p99_us = summary.paint_p99_us,
|
||||||
|
encode_p50_us = summary.encode_p50_us,
|
||||||
|
encode_p95_us = summary.encode_p95_us,
|
||||||
|
encode_p99_us = summary.encode_p99_us,
|
||||||
|
write_p50_us = summary.write_p50_us,
|
||||||
|
write_p95_us = summary.write_p95_us,
|
||||||
|
write_p99_us = summary.write_p99_us,
|
||||||
|
total_p50_us = summary.total_p50_us,
|
||||||
|
total_p95_us = summary.total_p95_us,
|
||||||
|
total_p99_us = summary.total_p99_us,
|
||||||
|
"frame_perf",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ use thiserror::Error;
|
|||||||
mod args;
|
mod args;
|
||||||
#[path = "ely_servo_sidecar/live.rs"]
|
#[path = "ely_servo_sidecar/live.rs"]
|
||||||
mod live;
|
mod live;
|
||||||
|
#[path = "ely_servo_sidecar/live_output.rs"]
|
||||||
|
mod live_output;
|
||||||
#[path = "ely_servo_sidecar/live_protocol.rs"]
|
#[path = "ely_servo_sidecar/live_protocol.rs"]
|
||||||
mod live_protocol;
|
mod live_protocol;
|
||||||
#[path = "ely_servo_sidecar/perf.rs"]
|
#[path = "ely_servo_sidecar/perf.rs"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead, Write},
|
io::{self, BufRead},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@@ -14,11 +14,12 @@ use ely_servo_host::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::args::LiveArgs;
|
use super::args::LiveArgs;
|
||||||
|
use super::live_output::{populate_surface_fields, write_outcome};
|
||||||
pub(super) use super::live_protocol::LiveSidecarError;
|
pub(super) use super::live_protocol::LiveSidecarError;
|
||||||
use super::live_protocol::{
|
use super::live_protocol::{
|
||||||
LiveFrameReport, LiveOutcome, LiveRequest, LiveSitePermission, PartialFrameTimings,
|
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.
|
/// Per-`Ensure` budget for Servo to paint after input dispatch.
|
||||||
/// 250 ms catches the common click + paint round trip within the
|
/// 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>(
|
fn ensure_session<'a>(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
sessions: &'a mut HashMap<String, LiveSession>,
|
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::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
env,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
Arc, OnceLock,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
@@ -14,24 +13,16 @@ use std::{
|
|||||||
|
|
||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
use euclid::Scale;
|
|
||||||
use servo::{
|
use servo::{
|
||||||
DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
|
DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint,
|
||||||
DeviceVector2D, Opts, RenderingContext, Scroll, Servo, ServoBuilder, WebViewBuilder,
|
WebViewVector,
|
||||||
WebViewPoint, WebViewVector,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Wrap an `f32` scale factor in Servo's typed `Scale<f32, DeviceIndependentPixel,
|
#[path = "runtime_context.rs"]
|
||||||
/// DevicePixel>`. The clamp guards against `NaN`/`inf` reaching Servo's
|
mod runtime_context;
|
||||||
/// layout (which assumes a positive finite scale).
|
|
||||||
fn hidpi_scale_from_factor(scale_factor: f32) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
use runtime_context::hidpi_scale_from_factor;
|
||||||
let safe = if scale_factor.is_finite() && scale_factor > 0.0 {
|
pub use runtime_context::{RenderingContextKind, ServoSurfaceSize};
|
||||||
scale_factor.clamp(0.5, 5.0)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
Scale::new(safe)
|
|
||||||
}
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -51,72 +42,6 @@ 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)]
|
|
||||||
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 {
|
pub struct SoftwareServoHost {
|
||||||
servo: Servo,
|
servo: Servo,
|
||||||
default_surface_size: ServoSurfaceSize,
|
default_surface_size: ServoSurfaceSize,
|
||||||
@@ -517,42 +442,6 @@ impl SoftwareServoHost {
|
|||||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)
|
.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> {
|
fn webview(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
|
||||||
self.webviews
|
self.webviews
|
||||||
.get(webview_id)
|
.get(webview_id)
|
||||||
@@ -578,58 +467,4 @@ impl SoftwareServoHost {
|
|||||||
webview.webview.focus();
|
webview.webview.focus();
|
||||||
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(
|
|
||||||
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