T10.3: publish IOSurfaceHandle once per surface, current_surface_id per frame

This commit is contained in:
2026-05-10 23:17:05 -04:00
parent bb0bd0032f
commit 07b9c9da01
9 changed files with 431 additions and 54 deletions
@@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
fs,
io::{self, BufRead, Write},
thread,
@@ -38,6 +38,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
let mut perf =
FramePerfAggregator::new(context_label, FramePerfAggregator::DEFAULT_WINDOW_SIZE);
let mut pending_summary: Option<FramePerfSummary> = None;
let mut published_surface_ids: HashMap<String, HashSet<u64>> = HashMap::new();
let stdin = io::stdin();
let mut stdout = io::stdout().lock();
@@ -54,7 +55,9 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
// `write_outcome`.
let frame_started_at = Instant::now();
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)),
};
write_outcome(
@@ -79,6 +82,7 @@ const fn rendering_context_label(kind: RenderingContextKind) -> &'static str {
fn handle_request(
host: &mut SoftwareServoHost,
sessions: &mut HashMap<String, LiveSession>,
published_surface_ids: &mut HashMap<String, HashSet<u64>>,
request: LiveRequest,
) -> Result<LiveOutcome, LiveSidecarError> {
match request {
@@ -101,7 +105,8 @@ fn handle_request(
let tab = TabId::parse(tab_id.clone())?;
let profile = ProfileId::parse(profile_id)?;
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)? {
session.awaiting_visible_frame = true;
@@ -131,17 +136,69 @@ fn handle_request(
)? {
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 } => {
let Some(session) = sessions.get_mut(&tab_id) else {
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
/// the same stdout pipe. The client reads the JSON line, takes
/// `rgba_byte_count` from the report, then reads that many bytes
@@ -3,7 +3,7 @@
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 thiserror::Error;
@@ -89,19 +89,53 @@ pub(super) struct LiveResponse {
pub frame: Option<LiveFrameReport>,
#[serde(skip_serializing_if = "Option::is_none")]
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 {
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 {
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 {
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")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IOSurfaceHandle {
pub mach_port_name: u32,
pub width: u32,
pub height: u32,
}
use crate::iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
#[cfg(target_os = "macos")]
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
/// return its mach port name plus dimensions. Increments the
/// IOSurface's mach-port use count; the receiving process holds it
/// via `IOSurfaceLookupFromMachPort` and is responsible for
/// `mach_port_deallocate` once the import is finished.
/// return its mach port name plus dimensions and stable surface
/// id. Increments the IOSurface's mach-port use count; the
/// receiving process holds it via `IOSurfaceLookupFromMachPort` and
/// is responsible for `mach_port_deallocate` once the import is
/// finished.
///
/// Implementation note: surfman's CGL backend keeps the bound
/// surface inside the GL context. To inspect it we temporarily
@@ -189,6 +193,7 @@ impl HardwareOffscreenContext {
let info = device.surface_info(&surface);
let handle = IOSurfaceHandle {
mach_port_name: mach_port,
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),
};
@@ -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 -2
View File
@@ -2,6 +2,7 @@ mod error;
#[cfg(feature = "hardware-render")]
mod hardware_rendering_context;
mod host;
mod iosurface_handle;
#[cfg(feature = "servo-engine")]
mod keyboard;
#[cfg(feature = "servo-engine")]
@@ -18,8 +19,7 @@ mod runtime_webview;
pub use error::ServoHostError;
#[cfg(feature = "hardware-render")]
pub use hardware_rendering_context::HardwareOffscreenContext;
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
pub use hardware_rendering_context::IOSurfaceHandle;
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
pub use host::{
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
+87 -20
View File
@@ -70,6 +70,17 @@ pub enum RenderingContextKind {
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,
@@ -377,10 +388,10 @@ impl SoftwareServoHost {
size: ServoSurfaceSize,
) -> Result<WebViewId, ServoHostError> {
let webview_id = WebViewId::new();
let rendering_context = self.new_rendering_context(size)?;
let handles = self.new_rendering_context(size)?;
let delegate =
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())
.build();
// Cosmetic: makes the first frame paint into the rendering
@@ -396,7 +407,9 @@ impl SoftwareServoHost {
HostWebView {
tab_id,
profile_id,
rendering_context,
rendering_context: handles.rendering_context,
#[cfg(feature = "hardware-render")]
hardware_context: handles.hardware_context,
webview,
delegate,
requested_url: None,
@@ -406,27 +419,81 @@ impl SoftwareServoHost {
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(
&self,
size: ServoSurfaceSize,
) -> Result<Rc<dyn RenderingContext>, ServoHostError> {
let rendering_context: Rc<dyn RenderingContext> = match self.rendering_context_kind {
RenderingContextKind::Software => Rc::new(
servo::SoftwareRenderingContext::new(size.physical())
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
),
#[cfg(feature = "hardware-render")]
RenderingContextKind::Hardware => Rc::new(
crate::HardwareOffscreenContext::new(size.physical())
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
),
#[cfg(not(feature = "hardware-render"))]
RenderingContextKind::Hardware => {
return Err(ServoHostError::HardwareRenderUnavailable);
) -> 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,
})
}
};
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
Ok(rendering_context)
#[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> {
@@ -13,6 +13,12 @@ pub(super) struct HostWebView {
pub(super) tab_id: TabId,
pub(super) profile_id: ProfileId,
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) delegate: Rc<HostWebViewDelegate>,
pub(super) requested_url: Option<String>,