From bb0bd0032fb446e651af161f6ad8c36524b4392f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 10 May 2026 23:01:34 -0400 Subject: [PATCH] T10.2: extract IOSurface mach port from the hardware surface on macOS --- Cargo.lock | 2 + crates/ely_servo_host/Cargo.toml | 4 ++ .../src/hardware_rendering_context.rs | 50 +++++++++++++++++++ crates/ely_servo_host/src/lib.rs | 2 + .../tests/hardware_rendering_context.rs | 37 ++++++++++++++ 5 files changed, 95 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 37733e6..9c03480 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2302,6 +2302,7 @@ dependencies = [ "glow", "image", "log", + "objc2-io-surface", "serde", "serde_json", "servo", @@ -6131,6 +6132,7 @@ dependencies = [ "libc", "objc2", "objc2-core-foundation", + "objc2-foundation", ] [[package]] diff --git a/crates/ely_servo_host/Cargo.toml b/crates/ely_servo_host/Cargo.toml index 44ef272..e22d668 100644 --- a/crates/ely_servo_host/Cargo.toml +++ b/crates/ely_servo_host/Cargo.toml @@ -16,6 +16,7 @@ hardware-render = [ "dep:image", "dep:log", "dep:surfman", + "dep:objc2-io-surface", ] [[bin]] @@ -38,5 +39,8 @@ surfman = { version = "0.11", optional = true } thiserror.workspace = true url = { workspace = true, optional = true } +[target.'cfg(target_os = "macos")'.dependencies] +objc2-io-surface = { version = "0.3.2", optional = true } + [lints] workspace = true diff --git a/crates/ely_servo_host/src/hardware_rendering_context.rs b/crates/ely_servo_host/src/hardware_rendering_context.rs index 83eb980..cb6420b 100644 --- a/crates/ely_servo_host/src/hardware_rendering_context.rs +++ b/crates/ely_servo_host/src/hardware_rendering_context.rs @@ -147,6 +147,56 @@ 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, +} + +#[cfg(target_os = "macos")] +impl HardwareOffscreenContext { + /// 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. + /// + /// Implementation note: surfman's CGL backend keeps the bound + /// surface inside the GL context. To inspect it we temporarily + /// `unbind_surface_from_context`, call `device.native_surface()` + /// (which retains the `IOSurfaceRef`), then `bind_surface_to_context` + /// again. The unbind path calls `glFlush` so the IOSurface contents + /// are consistent for any reader importing it after this returns. + pub fn current_iosurface_mach_port(&self) -> Result { + let device = &mut self.inner.device.borrow_mut(); + let context = &mut self.inner.context.borrow_mut(); + // `new` always binds a surface and `current_iosurface_mach_port` + // is the only method that unbinds; the `None` branch only fires + // if the invariant has been broken from outside. + let surface = + device.unbind_surface_from_context(context)?.ok_or(SurfmanError::Failed)?; + let native = device.native_surface(&surface); + let mach_port = native.0.create_mach_port(); + let info = device.surface_info(&surface); + let handle = IOSurfaceHandle { + mach_port_name: mach_port, + width: u32::try_from(info.size.width).unwrap_or(0), + height: u32::try_from(info.size.height).unwrap_or(0), + }; + device.bind_surface_to_context(context, surface).map_err(|(error, _)| error)?; + Ok(handle) + } +} + /// Trimmed mirror of `paint_api::rendering_context::SurfmanRenderingContext`. /// /// Only the methods the public type above actually uses are kept; the diff --git a/crates/ely_servo_host/src/lib.rs b/crates/ely_servo_host/src/lib.rs index ce9cbaa..5199ad2 100644 --- a/crates/ely_servo_host/src/lib.rs +++ b/crates/ely_servo_host/src/lib.rs @@ -18,6 +18,8 @@ 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 host::{ KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame, diff --git a/crates/ely_servo_host/tests/hardware_rendering_context.rs b/crates/ely_servo_host/tests/hardware_rendering_context.rs index fdcdcb1..548b42b 100644 --- a/crates/ely_servo_host/tests/hardware_rendering_context.rs +++ b/crates/ely_servo_host/tests/hardware_rendering_context.rs @@ -35,3 +35,40 @@ fn constructs_or_explains_why_not() { } } } + +#[cfg(target_os = "macos")] +#[test] +fn extracts_iosurface_mach_port_from_current_surface() { + let width = 256; + let height = 192; + let context = match HardwareOffscreenContext::new(PhysicalSize::new(width, height)) { + Ok(context) => context, + Err(error) => { + eprintln!( + "hardware GL adapter not available on this host \ + (acceptable in headless / no-GPU environments): {error:?}" + ); + return; + } + }; + + let first = context + .current_iosurface_mach_port() + .expect("first IOSurface mach port extraction must succeed"); + assert!( + first.mach_port_name != 0, + "IOSurfaceCreateMachPort must return a non-null mach_port_t (got 0)" + ); + assert_eq!(first.width, width, "reported width must match surface width"); + assert_eq!(first.height, height, "reported height must match surface height"); + + // The unbind/rebind cycle must leave the context usable: a second + // call should still produce a valid mach port without panicking on + // a stale `Framebuffer::None`. + let second = context + .current_iosurface_mach_port() + .expect("repeated mach port extraction must succeed after rebind"); + assert!(second.mach_port_name != 0); + assert_eq!(second.width, width); + assert_eq!(second.height, height); +}