T10.3: publish IOSurfaceHandle once per surface, current_surface_id per frame
This commit is contained in:
@@ -99,6 +99,13 @@ impl ServoLiveClient {
|
|||||||
log_frame_perf(perf);
|
log_frame_perf(perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(handle) = response.surface_handle.as_ref() {
|
||||||
|
log_iosurface_handle(handle);
|
||||||
|
}
|
||||||
|
if let Some(surface_id) = response.current_surface_id {
|
||||||
|
log_iosurface_current(surface_id);
|
||||||
|
}
|
||||||
|
|
||||||
let Some(report) = response.frame else {
|
let Some(report) = response.frame else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
@@ -338,6 +345,33 @@ struct LiveResponse {
|
|||||||
frame: Option<LiveFrameReport>,
|
frame: Option<LiveFrameReport>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
perf: Option<LiveFramePerfSummary>,
|
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
|
/// Aggregated frame-stage timings rolled up every N frames by the
|
||||||
@@ -362,6 +396,35 @@ struct LiveFramePerfSummary {
|
|||||||
total_p99_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
|
/// Emit one structured `tracing` event per perf summary, on a
|
||||||
/// dedicated target so `RUST_LOG=ely::servo::perf=info` flips the
|
/// dedicated target so `RUST_LOG=ely::servo::perf=info` flips the
|
||||||
/// stream on without dragging the rest of the app along. Filtering
|
/// stream on without dragging the rest of the app along. Filtering
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead, Write},
|
io::{self, BufRead, Write},
|
||||||
thread,
|
thread,
|
||||||
@@ -38,6 +38,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
let mut perf =
|
let mut perf =
|
||||||
FramePerfAggregator::new(context_label, FramePerfAggregator::DEFAULT_WINDOW_SIZE);
|
FramePerfAggregator::new(context_label, FramePerfAggregator::DEFAULT_WINDOW_SIZE);
|
||||||
let mut pending_summary: Option<FramePerfSummary> = None;
|
let mut pending_summary: Option<FramePerfSummary> = None;
|
||||||
|
let mut published_surface_ids: HashMap<String, HashSet<u64>> = HashMap::new();
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
||||||
@@ -54,7 +55,9 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
// `write_outcome`.
|
// `write_outcome`.
|
||||||
let frame_started_at = Instant::now();
|
let frame_started_at = Instant::now();
|
||||||
let outcome = match serde_json::from_str::<LiveRequest>(&line) {
|
let outcome = match serde_json::from_str::<LiveRequest>(&line) {
|
||||||
Ok(request) => handle_request(&mut host, &mut sessions, request),
|
Ok(request) => {
|
||||||
|
handle_request(&mut host, &mut sessions, &mut published_surface_ids, request)
|
||||||
|
}
|
||||||
Err(error) => Err(LiveSidecarError::Json(error)),
|
Err(error) => Err(LiveSidecarError::Json(error)),
|
||||||
};
|
};
|
||||||
write_outcome(
|
write_outcome(
|
||||||
@@ -79,6 +82,7 @@ const fn rendering_context_label(kind: RenderingContextKind) -> &'static str {
|
|||||||
fn handle_request(
|
fn handle_request(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
sessions: &mut HashMap<String, LiveSession>,
|
sessions: &mut HashMap<String, LiveSession>,
|
||||||
|
published_surface_ids: &mut HashMap<String, HashSet<u64>>,
|
||||||
request: LiveRequest,
|
request: LiveRequest,
|
||||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||||
match request {
|
match request {
|
||||||
@@ -101,7 +105,8 @@ fn handle_request(
|
|||||||
let tab = TabId::parse(tab_id.clone())?;
|
let tab = TabId::parse(tab_id.clone())?;
|
||||||
let profile = ProfileId::parse(profile_id)?;
|
let profile = ProfileId::parse(profile_id)?;
|
||||||
let url = UrlText::parse(url)?;
|
let url = UrlText::parse(url)?;
|
||||||
let session = ensure_session(host, sessions, tab_id, &tab, &profile, width, height)?;
|
let session =
|
||||||
|
ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?;
|
||||||
|
|
||||||
if apply_layout(host, session, width, height, page_zoom_percent)? {
|
if apply_layout(host, session, width, height, page_zoom_percent)? {
|
||||||
session.awaiting_visible_frame = true;
|
session.awaiting_visible_frame = true;
|
||||||
@@ -131,17 +136,69 @@ fn handle_request(
|
|||||||
)? {
|
)? {
|
||||||
session.awaiting_visible_frame = true;
|
session.awaiting_visible_frame = true;
|
||||||
}
|
}
|
||||||
poll_frame(host, session)
|
let webview_id = session.webview_id.clone();
|
||||||
|
let mut outcome = poll_frame(host, session)?;
|
||||||
|
populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome);
|
||||||
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
LiveRequest::Poll { tab_id } => {
|
LiveRequest::Poll { tab_id } => {
|
||||||
let Some(session) = sessions.get_mut(&tab_id) else {
|
let Some(session) = sessions.get_mut(&tab_id) else {
|
||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
};
|
};
|
||||||
poll_frame(host, session)
|
let webview_id = session.webview_id.clone();
|
||||||
|
let mut outcome = poll_frame(host, session)?;
|
||||||
|
populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome);
|
||||||
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// Serialise the response then stream the optional raw RGBA frame on
|
||||||
/// the same stdout pipe. The client reads the JSON line, takes
|
/// the same stdout pipe. The client reads the JSON line, takes
|
||||||
/// `rgba_byte_count` from the report, then reads that many bytes
|
/// `rgba_byte_count` from the report, then reads that many bytes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use ely_servo_host::{RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState};
|
use ely_servo_host::{IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -89,19 +89,53 @@ pub(super) struct LiveResponse {
|
|||||||
pub frame: Option<LiveFrameReport>,
|
pub frame: Option<LiveFrameReport>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub perf: Option<FramePerfSummary>,
|
pub perf: Option<FramePerfSummary>,
|
||||||
|
/// Populated on the first frame the sidecar emits for a given
|
||||||
|
/// surface — initial paint, after a resize, or whenever surfman
|
||||||
|
/// rotates its swap chain to a surface we haven't seen yet. The
|
||||||
|
/// receiver imports the IOSurface (via
|
||||||
|
/// `IOSurfaceLookupFromMachPort`) once per `surface_id` and caches
|
||||||
|
/// the resulting Metal texture. Always `None` on the software
|
||||||
|
/// path.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub surface_handle: Option<IOSurfaceHandle>,
|
||||||
|
/// Populated on every hardware paint frame. Tells the receiver
|
||||||
|
/// which previously-imported IOSurface to sample THIS frame. The
|
||||||
|
/// surfman attached swap chain rotates between front/back
|
||||||
|
/// surfaces, so this id alternates between the values the receiver
|
||||||
|
/// has already imported. Always `None` on the software path.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_surface_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LiveResponse {
|
impl LiveResponse {
|
||||||
fn empty() -> Self {
|
fn empty() -> Self {
|
||||||
Self { error: None, frame: None, perf: None }
|
Self {
|
||||||
|
error: None,
|
||||||
|
frame: None,
|
||||||
|
perf: None,
|
||||||
|
surface_handle: None,
|
||||||
|
current_surface_id: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn frame(frame: LiveFrameReport) -> Self {
|
fn frame(frame: LiveFrameReport) -> Self {
|
||||||
Self { error: None, frame: Some(frame), perf: None }
|
Self {
|
||||||
|
error: None,
|
||||||
|
frame: Some(frame),
|
||||||
|
perf: None,
|
||||||
|
surface_handle: None,
|
||||||
|
current_surface_id: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error(message: String) -> Self {
|
fn error(message: String) -> Self {
|
||||||
Self { error: Some(message), frame: None, perf: None }
|
Self {
|
||||||
|
error: Some(message),
|
||||||
|
frame: None,
|
||||||
|
perf: None,
|
||||||
|
surface_handle: None,
|
||||||
|
current_surface_id: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,28 +147,32 @@ impl RenderingContext for HardwareOffscreenContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-process handle to a hardware surface: the receiving process
|
|
||||||
/// can rebuild an `IOSurfaceRef` from `mach_port_name` and import it as
|
|
||||||
/// a Metal texture without ever copying the pixels.
|
|
||||||
///
|
|
||||||
/// `width`/`height` are reported in surface pixels (post-DPR), matching
|
|
||||||
/// what surfman handed out at construction time. The receiver should
|
|
||||||
/// scale layout coordinates by its own backing scale factor.
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
use crate::iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||||
pub struct IOSurfaceHandle {
|
|
||||||
pub mach_port_name: u32,
|
|
||||||
pub width: u32,
|
|
||||||
pub height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
impl HardwareOffscreenContext {
|
impl HardwareOffscreenContext {
|
||||||
|
/// Cheap, non-mutating identity probe of the currently bound
|
||||||
|
/// surface. Reads `Device::context_surface_info` (no unbind, no
|
||||||
|
/// mach port creation) so callers can dedup before paying the
|
||||||
|
/// price of `current_iosurface_mach_port`.
|
||||||
|
pub fn peek_iosurface_identity(&self) -> Result<IOSurfaceIdentity, SurfmanError> {
|
||||||
|
let device = self.inner.device.borrow();
|
||||||
|
let context = self.inner.context.borrow();
|
||||||
|
let info = device.context_surface_info(&context)?.ok_or(SurfmanError::Failed)?;
|
||||||
|
Ok(IOSurfaceIdentity {
|
||||||
|
surface_id: info.id.0 as u64,
|
||||||
|
width: u32::try_from(info.size.width).unwrap_or(0),
|
||||||
|
height: u32::try_from(info.size.height).unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot the IOSurface currently bound to the context and
|
/// Snapshot the IOSurface currently bound to the context and
|
||||||
/// return its mach port name plus dimensions. Increments the
|
/// return its mach port name plus dimensions and stable surface
|
||||||
/// IOSurface's mach-port use count; the receiving process holds it
|
/// id. Increments the IOSurface's mach-port use count; the
|
||||||
/// via `IOSurfaceLookupFromMachPort` and is responsible for
|
/// receiving process holds it via `IOSurfaceLookupFromMachPort` and
|
||||||
/// `mach_port_deallocate` once the import is finished.
|
/// is responsible for `mach_port_deallocate` once the import is
|
||||||
|
/// finished.
|
||||||
///
|
///
|
||||||
/// Implementation note: surfman's CGL backend keeps the bound
|
/// Implementation note: surfman's CGL backend keeps the bound
|
||||||
/// surface inside the GL context. To inspect it we temporarily
|
/// surface inside the GL context. To inspect it we temporarily
|
||||||
@@ -189,6 +193,7 @@ impl HardwareOffscreenContext {
|
|||||||
let info = device.surface_info(&surface);
|
let info = device.surface_info(&surface);
|
||||||
let handle = IOSurfaceHandle {
|
let handle = IOSurfaceHandle {
|
||||||
mach_port_name: mach_port,
|
mach_port_name: mach_port,
|
||||||
|
surface_id: info.id.0 as u64,
|
||||||
width: u32::try_from(info.size.width).unwrap_or(0),
|
width: u32::try_from(info.size.width).unwrap_or(0),
|
||||||
height: u32::try_from(info.size.height).unwrap_or(0),
|
height: u32::try_from(info.size.height).unwrap_or(0),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//! Cross-process IOSurface descriptor types.
|
||||||
|
//!
|
||||||
|
//! These wire types live outside `hardware_rendering_context` (which
|
||||||
|
//! is hardware-render + macOS gated) so the sidecar's JSON protocol
|
||||||
|
//! can carry an `Option<IOSurfaceHandle>` regardless of feature
|
||||||
|
//! flags. The receiver always knows how to parse the field; if no
|
||||||
|
//! sender ever populates it (software-only build), it's just `None`
|
||||||
|
//! on every frame.
|
||||||
|
//!
|
||||||
|
//! Minting an [`IOSurfaceHandle`] requires a hardware surfman context
|
||||||
|
//! and a macOS host. That part lives in
|
||||||
|
//! [`crate::hardware_rendering_context`].
|
||||||
|
|
||||||
|
/// Cross-process handle to a hardware surface: the receiving process
|
||||||
|
/// rebuilds an `IOSurfaceRef` from `mach_port_name` and imports it as
|
||||||
|
/// a Metal texture without copying pixels.
|
||||||
|
///
|
||||||
|
/// `surface_id` is the stable surfman `SurfaceID` (a pointer-shaped
|
||||||
|
/// `usize` widened to `u64` for the wire). It lets the receiver dedup:
|
||||||
|
/// when two consecutive frames carry the same `surface_id` the
|
||||||
|
/// imported `MTLTexture` is reused without re-importing. `width` and
|
||||||
|
/// `height` are reported in surface pixels (post-DPR).
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "servo-engine", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct IOSurfaceHandle {
|
||||||
|
pub mach_port_name: u32,
|
||||||
|
pub surface_id: u64,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identity-only peek of the currently bound IOSurface. Distinguishes
|
||||||
|
/// "same surface as last frame" from "resize/swap rotated to a new
|
||||||
|
/// surface" without minting a fresh mach port (mach ports are a scarce
|
||||||
|
/// kernel resource and `IOSurfaceCreateMachPort` is not cheap).
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct IOSurfaceIdentity {
|
||||||
|
pub surface_id: u64,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod error;
|
|||||||
#[cfg(feature = "hardware-render")]
|
#[cfg(feature = "hardware-render")]
|
||||||
mod hardware_rendering_context;
|
mod hardware_rendering_context;
|
||||||
mod host;
|
mod host;
|
||||||
|
mod iosurface_handle;
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
@@ -18,8 +19,7 @@ mod runtime_webview;
|
|||||||
pub use error::ServoHostError;
|
pub use error::ServoHostError;
|
||||||
#[cfg(feature = "hardware-render")]
|
#[cfg(feature = "hardware-render")]
|
||||||
pub use hardware_rendering_context::HardwareOffscreenContext;
|
pub use hardware_rendering_context::HardwareOffscreenContext;
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||||
pub use hardware_rendering_context::IOSurfaceHandle;
|
|
||||||
pub use host::{
|
pub use host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
||||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ pub enum RenderingContextKind {
|
|||||||
Hardware,
|
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,
|
||||||
@@ -377,10 +388,10 @@ impl SoftwareServoHost {
|
|||||||
size: ServoSurfaceSize,
|
size: ServoSurfaceSize,
|
||||||
) -> Result<WebViewId, ServoHostError> {
|
) -> Result<WebViewId, ServoHostError> {
|
||||||
let webview_id = WebViewId::new();
|
let webview_id = WebViewId::new();
|
||||||
let rendering_context = self.new_rendering_context(size)?;
|
let handles = self.new_rendering_context(size)?;
|
||||||
let delegate =
|
let delegate =
|
||||||
Rc::new(HostWebViewDelegate::new(profile_id.clone(), self.permissions.clone()));
|
Rc::new(HostWebViewDelegate::new(profile_id.clone(), self.permissions.clone()));
|
||||||
let webview = WebViewBuilder::new(&self.servo, rendering_context.clone())
|
let webview = WebViewBuilder::new(&self.servo, handles.rendering_context.clone())
|
||||||
.delegate(delegate.clone())
|
.delegate(delegate.clone())
|
||||||
.build();
|
.build();
|
||||||
// Cosmetic: makes the first frame paint into the rendering
|
// Cosmetic: makes the first frame paint into the rendering
|
||||||
@@ -396,7 +407,9 @@ impl SoftwareServoHost {
|
|||||||
HostWebView {
|
HostWebView {
|
||||||
tab_id,
|
tab_id,
|
||||||
profile_id,
|
profile_id,
|
||||||
rendering_context,
|
rendering_context: handles.rendering_context,
|
||||||
|
#[cfg(feature = "hardware-render")]
|
||||||
|
hardware_context: handles.hardware_context,
|
||||||
webview,
|
webview,
|
||||||
delegate,
|
delegate,
|
||||||
requested_url: None,
|
requested_url: None,
|
||||||
@@ -406,27 +419,81 @@ impl SoftwareServoHost {
|
|||||||
Ok(webview_id)
|
Ok(webview_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cheap peek at the IOSurface identity bound to this webview's
|
||||||
|
/// hardware context. Returns `None` for software webviews and on
|
||||||
|
/// non-macOS hosts; otherwise the surfman `SurfaceID`-derived
|
||||||
|
/// identity plus dimensions. Used by the sidecar's live loop to
|
||||||
|
/// dedup mach port creation.
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
pub fn peek_iosurface_identity(
|
||||||
|
&self,
|
||||||
|
webview_id: &WebViewId,
|
||||||
|
) -> Result<Option<crate::IOSurfaceIdentity>, ServoHostError> {
|
||||||
|
let webview = self.webview(webview_id)?;
|
||||||
|
let Some(hardware) = webview.hardware_context.as_ref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
hardware
|
||||||
|
.peek_iosurface_identity()
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| ServoHostError::RenderingContextUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint a fresh mach port for the IOSurface bound to this
|
||||||
|
/// webview's hardware context. The caller is responsible for
|
||||||
|
/// transferring the port to the receiving process; if no transfer
|
||||||
|
/// happens, the port leaks. Software webviews return `None`.
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
pub fn current_iosurface_handle(
|
||||||
|
&self,
|
||||||
|
webview_id: &WebViewId,
|
||||||
|
) -> Result<Option<crate::IOSurfaceHandle>, ServoHostError> {
|
||||||
|
let webview = self.webview(webview_id)?;
|
||||||
|
let Some(hardware) = webview.hardware_context.as_ref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
hardware
|
||||||
|
.current_iosurface_mach_port()
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| ServoHostError::RenderingContextUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
fn new_rendering_context(
|
fn new_rendering_context(
|
||||||
&self,
|
&self,
|
||||||
size: ServoSurfaceSize,
|
size: ServoSurfaceSize,
|
||||||
) -> Result<Rc<dyn RenderingContext>, ServoHostError> {
|
) -> Result<RenderingContextHandles, ServoHostError> {
|
||||||
let rendering_context: Rc<dyn RenderingContext> = match self.rendering_context_kind {
|
match self.rendering_context_kind {
|
||||||
RenderingContextKind::Software => Rc::new(
|
RenderingContextKind::Software => {
|
||||||
|
let rendering_context = Rc::new(
|
||||||
servo::SoftwareRenderingContext::new(size.physical())
|
servo::SoftwareRenderingContext::new(size.physical())
|
||||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
||||||
),
|
);
|
||||||
|
rendering_context
|
||||||
|
.make_current()
|
||||||
|
.map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||||
|
Ok(RenderingContextHandles {
|
||||||
|
rendering_context,
|
||||||
#[cfg(feature = "hardware-render")]
|
#[cfg(feature = "hardware-render")]
|
||||||
RenderingContextKind::Hardware => Rc::new(
|
hardware_context: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(feature = "hardware-render")]
|
||||||
|
RenderingContextKind::Hardware => {
|
||||||
|
let hardware = Rc::new(
|
||||||
crate::HardwareOffscreenContext::new(size.physical())
|
crate::HardwareOffscreenContext::new(size.physical())
|
||||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
|
.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"))]
|
#[cfg(not(feature = "hardware-render"))]
|
||||||
RenderingContextKind::Hardware => {
|
RenderingContextKind::Hardware => Err(ServoHostError::HardwareRenderUnavailable),
|
||||||
return Err(ServoHostError::HardwareRenderUnavailable);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
|
||||||
Ok(rendering_context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn webview(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
|
fn webview(&self, webview_id: &WebViewId) -> Result<&HostWebView, ServoHostError> {
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ pub(super) struct HostWebView {
|
|||||||
pub(super) tab_id: TabId,
|
pub(super) tab_id: TabId,
|
||||||
pub(super) profile_id: ProfileId,
|
pub(super) profile_id: ProfileId,
|
||||||
pub(super) rendering_context: Rc<dyn RenderingContext>,
|
pub(super) rendering_context: Rc<dyn RenderingContext>,
|
||||||
|
/// Parallel concrete handle when the rendering context is the
|
||||||
|
/// vendored hardware path. `None` for software webviews. Lets the
|
||||||
|
/// host call macOS-specific methods (IOSurface mach port
|
||||||
|
/// extraction) without downcasting `dyn RenderingContext`.
|
||||||
|
#[cfg(feature = "hardware-render")]
|
||||||
|
pub(super) hardware_context: Option<Rc<crate::HardwareOffscreenContext>>,
|
||||||
pub(super) webview: WebView,
|
pub(super) webview: WebView,
|
||||||
pub(super) delegate: Rc<HostWebViewDelegate>,
|
pub(super) delegate: Rc<HostWebViewDelegate>,
|
||||||
pub(super) requested_url: Option<String>,
|
pub(super) requested_url: Option<String>,
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ struct LiveResponse {
|
|||||||
frame: Option<LiveFrameReport>,
|
frame: Option<LiveFrameReport>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
perf: Option<FramePerfSummary>,
|
perf: Option<FramePerfSummary>,
|
||||||
|
#[serde(default)]
|
||||||
|
surface_handle: Option<BenchSurfaceHandle>,
|
||||||
|
#[serde(default)]
|
||||||
|
current_surface_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||||
|
struct BenchSurfaceHandle {
|
||||||
|
mach_port_name: u32,
|
||||||
|
surface_id: u64,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -99,9 +111,9 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
|||||||
let stdout = child.stdout.take().ok_or("sidecar stdout missing")?;
|
let stdout = child.stdout.take().ok_or("sidecar stdout missing")?;
|
||||||
let mut reader = BufReader::new(stdout);
|
let mut reader = BufReader::new(stdout);
|
||||||
|
|
||||||
let summaries =
|
let outcome =
|
||||||
match drive_bench(&mut stdin, &mut reader, &kind, &tab, &profile_id, &url, frames) {
|
match drive_bench(&mut stdin, &mut reader, &kind, &tab, &profile_id, &url, frames) {
|
||||||
Ok(summaries) => summaries,
|
Ok(outcome) => outcome,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
drop(stdin);
|
drop(stdin);
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
@@ -114,14 +126,53 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
|||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
cleanup(&profile_data_dir)?;
|
cleanup(&profile_data_dir)?;
|
||||||
|
|
||||||
print_summaries(&kind, frames, &summaries);
|
print_summaries(&kind, frames, &outcome.summaries);
|
||||||
|
print_surface_handles(&kind, &outcome.surface_handles);
|
||||||
|
print_current_surface_summary(&kind, &outcome.current_surface_ids);
|
||||||
assert!(
|
assert!(
|
||||||
!summaries.is_empty(),
|
!outcome.summaries.is_empty(),
|
||||||
"expected at least one FramePerfSummary across {frames} frames"
|
"expected at least one FramePerfSummary across {frames} frames"
|
||||||
);
|
);
|
||||||
|
if kind == "hardware" {
|
||||||
|
assert!(
|
||||||
|
!outcome.surface_handles.is_empty(),
|
||||||
|
"hardware path must publish at least one IOSurface handle"
|
||||||
|
);
|
||||||
|
// Mach port dedup: the surfman attached swap chain on macOS
|
||||||
|
// rotates between front+back surfaces, so we expect a SMALL
|
||||||
|
// number of distinct mach port publishes — definitely not one
|
||||||
|
// per frame. Anything close to `frames` is a regression to the
|
||||||
|
// T10.3 starting state where dedup tracked only the last id.
|
||||||
|
let max_expected = 8;
|
||||||
|
assert!(
|
||||||
|
outcome.surface_handles.len() <= max_expected,
|
||||||
|
"expected at most {max_expected} IOSurface publishes (one per unique surface), \
|
||||||
|
got {} — dedup regressed",
|
||||||
|
outcome.surface_handles.len()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!outcome.current_surface_ids.is_empty(),
|
||||||
|
"hardware path must report current_surface_id on every frame"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(
|
||||||
|
outcome.surface_handles.is_empty(),
|
||||||
|
"software path must never publish an IOSurface handle"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
outcome.current_surface_ids.is_empty(),
|
||||||
|
"software path must never report current_surface_id"
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BenchOutcome {
|
||||||
|
summaries: Vec<FramePerfSummary>,
|
||||||
|
surface_handles: Vec<BenchSurfaceHandle>,
|
||||||
|
current_surface_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_sidecar(kind: &str, profile_data_dir: &PathBuf) -> Result<Child, Box<dyn Error>> {
|
fn spawn_sidecar(kind: &str, profile_data_dir: &PathBuf) -> Result<Child, Box<dyn Error>> {
|
||||||
let mut command = Command::new(env!("CARGO_BIN_EXE_ely_servo_sidecar"));
|
let mut command = Command::new(env!("CARGO_BIN_EXE_ely_servo_sidecar"));
|
||||||
command
|
command
|
||||||
@@ -144,13 +195,17 @@ fn drive_bench(
|
|||||||
profile_id: &ProfileId,
|
profile_id: &ProfileId,
|
||||||
url: &str,
|
url: &str,
|
||||||
frames: u32,
|
frames: u32,
|
||||||
) -> Result<Vec<FramePerfSummary>, Box<dyn Error>> {
|
) -> Result<BenchOutcome, Box<dyn Error>> {
|
||||||
let mut summaries = Vec::new();
|
let mut summaries = Vec::new();
|
||||||
|
let mut surface_handles = Vec::new();
|
||||||
|
let mut current_surface_ids = Vec::new();
|
||||||
|
|
||||||
let navigate = build_ensure(tab, profile_id, url, 0, 0, false);
|
let navigate = build_ensure(tab, profile_id, url, 0, 0, false);
|
||||||
write_request(stdin, &navigate)?;
|
write_request(stdin, &navigate)?;
|
||||||
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
||||||
record_summary(&response, kind, &mut summaries);
|
record_summary(&response, kind, &mut summaries);
|
||||||
|
record_surface_handle(&response, kind, &mut surface_handles);
|
||||||
|
record_current_surface_id(&response, &mut current_surface_ids);
|
||||||
|
|
||||||
let mut accumulated_scroll = 0;
|
let mut accumulated_scroll = 0;
|
||||||
for frame_index in 0..frames {
|
for frame_index in 0..frames {
|
||||||
@@ -167,10 +222,59 @@ fn drive_bench(
|
|||||||
return Err(format!("sidecar error at frame {frame_index}: {error}").into());
|
return Err(format!("sidecar error at frame {frame_index}: {error}").into());
|
||||||
}
|
}
|
||||||
record_summary(&response, kind, &mut summaries);
|
record_summary(&response, kind, &mut summaries);
|
||||||
|
record_surface_handle(&response, kind, &mut surface_handles);
|
||||||
|
record_current_surface_id(&response, &mut current_surface_ids);
|
||||||
}
|
}
|
||||||
let _ = accumulated_scroll;
|
let _ = accumulated_scroll;
|
||||||
|
|
||||||
Ok(summaries)
|
Ok(BenchOutcome { summaries, surface_handles, current_surface_ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_surface_handle(
|
||||||
|
response: &LiveResponse,
|
||||||
|
kind: &str,
|
||||||
|
surface_handles: &mut Vec<BenchSurfaceHandle>,
|
||||||
|
) {
|
||||||
|
if let Some(handle) = response.surface_handle {
|
||||||
|
eprintln!(
|
||||||
|
"[iosurface {kind}] new surface_id=0x{:x} mach_port=0x{:x} {}x{}",
|
||||||
|
handle.surface_id, handle.mach_port_name, handle.width, handle.height,
|
||||||
|
);
|
||||||
|
surface_handles.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_current_surface_id(response: &LiveResponse, current_surface_ids: &mut Vec<u64>) {
|
||||||
|
if let Some(surface_id) = response.current_surface_id {
|
||||||
|
current_surface_ids.push(surface_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_surface_handles(kind: &str, surface_handles: &[BenchSurfaceHandle]) {
|
||||||
|
eprintln!(
|
||||||
|
"\n=== ELY_PERF_KIND={kind} iosurface_imports={} (one per unique surface) ===",
|
||||||
|
surface_handles.len()
|
||||||
|
);
|
||||||
|
for (index, handle) in surface_handles.iter().enumerate() {
|
||||||
|
eprintln!(
|
||||||
|
"{:<4} surface_id=0x{:x} mach_port=0x{:x} {}x{}",
|
||||||
|
index, handle.surface_id, handle.mach_port_name, handle.width, handle.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_current_surface_summary(kind: &str, current_surface_ids: &[u64]) {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
let mut counts: BTreeMap<u64, u32> = BTreeMap::new();
|
||||||
|
for id in current_surface_ids {
|
||||||
|
*counts.entry(*id).or_default() += 1;
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"\n=== ELY_PERF_KIND={kind} current_surface_id histogram (per-frame selector) ===",
|
||||||
|
);
|
||||||
|
for (surface_id, count) in counts.iter() {
|
||||||
|
eprintln!("surface_id=0x{:x} frames={}", surface_id, count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_ensure(
|
fn build_ensure(
|
||||||
|
|||||||
Reference in New Issue
Block a user