250 lines
9.8 KiB
Rust
250 lines
9.8 KiB
Rust
//! macOS-only import of cross-process IOSurface handles into
|
|
//! `CVPixelBuffer`s that preserve the sidecar's IOSurface identity.
|
|
//!
|
|
//! `T10.4` originally imported the IOSurface into an `MTLTexture`
|
|
//! directly. GPUI 0.2.2 exposes `Window::paint_surface` /
|
|
//! `elements::surface::Surface` for `CVPixelBuffer`; the local GPUI
|
|
//! patch adds a BGRA fragment pipeline for Servo's hardware
|
|
//! IOSurfaces, so this cache is the renderer-side handoff point.
|
|
//!
|
|
//! Lifetime contract:
|
|
//!
|
|
//! * `IOSurfaceCreateMachPort` (sidecar side) gives the receiver a
|
|
//! send right whose refcount is 1 in our task. After we resolve
|
|
//! the surface and wrap it in a CVPixelBuffer, the mach port has
|
|
//! done its job.
|
|
//! * `CVPixelBufferCreateWithIOSurface` retains the IOSurface for
|
|
//! the pixel buffer's lifetime. We `mach_port_deallocate`
|
|
//! immediately so the receiver process doesn't accumulate idle
|
|
//! mach send rights.
|
|
//! * Dropping `IOSurfaceCache` releases every cached
|
|
//! `CVPixelBuffer`, which in turn releases each retained
|
|
//! IOSurface. The sidecar still holds its own retain via surfman,
|
|
//! so the IOSurface itself outlives our cache for as long as the
|
|
//! sidecar keeps painting.
|
|
|
|
#![cfg(target_os = "macos")]
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use core_foundation::base::TCFType as _;
|
|
use core_video::pixel_buffer::CVPixelBuffer;
|
|
#[allow(deprecated)]
|
|
use io_surface::IOSurface;
|
|
use thiserror::Error;
|
|
|
|
/// Cache of imported `CVPixelBuffer`s keyed by IOSurface identity.
|
|
/// Constructed lazily by the renderer-side client on the first
|
|
/// hardware-path frame.
|
|
pub(crate) struct IOSurfaceCache {
|
|
pixel_buffers: HashMap<u64, CVPixelBuffer>,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub(crate) enum SurfaceImportError {
|
|
#[error("IOSurfaceLookupFromMachPort returned null for port 0x{port:x}")]
|
|
LookupFailed { port: u32 },
|
|
#[error("CVPixelBufferCreateWithIOSurface returned status {status}")]
|
|
PixelBufferBuildFailed { status: i32 },
|
|
}
|
|
|
|
impl IOSurfaceCache {
|
|
pub fn new() -> Self {
|
|
Self { pixel_buffers: HashMap::new() }
|
|
}
|
|
|
|
/// Import an IOSurface published by the sidecar's
|
|
/// `surface_handle` field. Idempotent on `surface_id`: a second
|
|
/// call with the same id immediately deallocates the duplicate
|
|
/// mach port without re-importing. The sender's T10.3 dedup means
|
|
/// the duplicate path should never fire in practice — it's here
|
|
/// so a misbehaving sidecar can't quietly leak ports.
|
|
pub fn import(
|
|
&mut self,
|
|
mach_port_name: u32,
|
|
surface_id: u64,
|
|
) -> Result<(), SurfaceImportError> {
|
|
if self.pixel_buffers.contains_key(&surface_id) {
|
|
deallocate_mach_port(mach_port_name);
|
|
return Ok(());
|
|
}
|
|
|
|
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
|
|
else {
|
|
return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
|
|
};
|
|
|
|
// Both objc2-io-surface and the legacy `io_surface` crate wrap
|
|
// the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
|
|
// (via core-video) expects the legacy crate's wrapper. Reach for
|
|
// the raw pointer and let TCFType CFRetain it independently so
|
|
// both Rust handles can drop without double-freeing.
|
|
let raw_ptr: *const std::ffi::c_void =
|
|
(&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
|
|
#[allow(deprecated)]
|
|
let io_surface_view: IOSurface = {
|
|
#[expect(unsafe_code)]
|
|
unsafe {
|
|
IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
|
|
}
|
|
};
|
|
|
|
let pixel_buffer = CVPixelBuffer::from_io_surface(&io_surface_view, None)
|
|
.map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })?;
|
|
|
|
self.pixel_buffers.insert(surface_id, pixel_buffer);
|
|
deallocate_mach_port(mach_port_name);
|
|
Ok(())
|
|
}
|
|
|
|
/// Look up an already-imported pixel buffer by `surface_id`. The
|
|
/// receiver's per-frame `current_surface_id` selects which of the
|
|
/// swap chain's rotating front/back surfaces to sample. Returns a
|
|
/// clone (CVPixelBuffer is reference-counted; cloning is a cheap
|
|
/// atomic increment) so the caller can hand it to GPUI's
|
|
/// `surface(...)` element without holding a borrow on the cache.
|
|
pub fn pixel_buffer_for(&self, surface_id: u64) -> Option<CVPixelBuffer> {
|
|
self.pixel_buffers.get(&surface_id).cloned()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn cached_surface_count(&self) -> usize {
|
|
self.pixel_buffers.len()
|
|
}
|
|
}
|
|
|
|
/// Release one send right against the mach port we received. The
|
|
/// IOSurface itself stays alive because the `CVPixelBuffer` (or the
|
|
/// sidecar's surfman) still retain it.
|
|
fn deallocate_mach_port(port: u32) {
|
|
#[expect(unsafe_code)]
|
|
let result = unsafe { mach_port_deallocate(mach_task_self_, port) };
|
|
if result != KERN_SUCCESS {
|
|
tracing::warn!(
|
|
target: "ely::servo::iosurface",
|
|
mach_port_name = port,
|
|
kern_result = result,
|
|
"mach_port_deallocate returned non-success",
|
|
);
|
|
}
|
|
}
|
|
|
|
const KERN_SUCCESS: i32 = 0;
|
|
|
|
#[expect(unsafe_code)]
|
|
unsafe extern "C" {
|
|
/// Global mach task port for the running process. Defined in
|
|
/// `mach/mach_init.h` as `extern mach_port_t mach_task_self_;`.
|
|
static mach_task_self_: u32;
|
|
|
|
/// Releases one send right against `name` within `task`. We only
|
|
/// ever call this with our own task; the IOSurface keeps its
|
|
/// retain via the CVPixelBuffer so this just frees our port slot.
|
|
fn mach_port_deallocate(task: u32, name: u32) -> i32;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::IOSurfaceCache;
|
|
use objc2_core_foundation::{
|
|
CFDictionary, CFIndex, CFNumber, CFRetained, CFString, kCFAllocatorDefault,
|
|
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
|
|
};
|
|
use objc2_io_surface::{
|
|
IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceBytesPerRow, kIOSurfaceHeight,
|
|
kIOSurfacePixelFormat, kIOSurfaceWidth,
|
|
};
|
|
use std::os::raw::c_void;
|
|
|
|
const TEST_WIDTH: u32 = 64;
|
|
const TEST_HEIGHT: u32 = 48;
|
|
|
|
/// Build a CPU-backed IOSurface from scratch, the same way
|
|
/// surfman's macOS backend does.
|
|
///
|
|
/// BGRA8 (four-cc '32BGRA'), width + height + bytes_per_element
|
|
/// + bytes_per_row live in a Core Foundation properties dictionary.
|
|
///
|
|
/// The pointer-casts mirror
|
|
/// `surfman::platform::macos::system::surface::create_io_surface`.
|
|
fn build_local_iosurface() -> Result<CFRetained<IOSurfaceRef>, String> {
|
|
let pixel_format: i32 = i32::from_be_bytes(*b"BGRA");
|
|
let bytes_per_element: i32 = 4;
|
|
let bytes_per_row: i32 = (TEST_WIDTH as i32) * bytes_per_element;
|
|
|
|
let width_num = CFNumber::new_i32(TEST_WIDTH as i32);
|
|
let height_num = CFNumber::new_i32(TEST_HEIGHT as i32);
|
|
let bpe_num = CFNumber::new_i32(bytes_per_element);
|
|
let bpr_num = CFNumber::new_i32(bytes_per_row);
|
|
let pf_num = CFNumber::new_i32(pixel_format);
|
|
|
|
#[expect(unsafe_code)]
|
|
unsafe {
|
|
let keys: [&CFString; 5] = [
|
|
kIOSurfaceWidth,
|
|
kIOSurfaceHeight,
|
|
kIOSurfaceBytesPerElement,
|
|
kIOSurfaceBytesPerRow,
|
|
kIOSurfacePixelFormat,
|
|
];
|
|
let values: [&CFNumber; 5] = [&width_num, &height_num, &bpe_num, &bpr_num, &pf_num];
|
|
let keys_ptr: *mut *const c_void = keys.as_ptr() as *mut *const c_void;
|
|
let values_ptr: *mut *const c_void = values.as_ptr() as *mut *const c_void;
|
|
let properties = CFDictionary::new(
|
|
kCFAllocatorDefault,
|
|
keys_ptr,
|
|
values_ptr,
|
|
keys.len() as CFIndex,
|
|
&kCFTypeDictionaryKeyCallBacks,
|
|
&kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
.ok_or_else(|| "CFDictionaryCreate returned null".to_string())?;
|
|
IOSurfaceRef::new(&properties)
|
|
.ok_or_else(|| "IOSurfaceCreate returned null".to_string())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn imports_local_iosurface_into_pixel_buffer() -> Result<(), String> {
|
|
let mut cache = IOSurfaceCache::new();
|
|
let iosurface = build_local_iosurface()?;
|
|
let mach_port = iosurface.create_mach_port();
|
|
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
|
|
let surface_id: u64 = 0xDEAD_BEEFu64;
|
|
|
|
cache.import(mach_port, surface_id).map_err(|error| error.to_string())?;
|
|
|
|
let pixel_buffer = cache
|
|
.pixel_buffer_for(surface_id)
|
|
.ok_or_else(|| "imported pixel buffer was missing".to_string())?;
|
|
assert_eq!(
|
|
pixel_buffer.get_width() as u32,
|
|
TEST_WIDTH,
|
|
"CVPixelBuffer width must match the source IOSurface",
|
|
);
|
|
assert_eq!(
|
|
pixel_buffer.get_height() as u32,
|
|
TEST_HEIGHT,
|
|
"CVPixelBuffer height must match the source IOSurface",
|
|
);
|
|
assert_eq!(cache.cached_surface_count(), 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn second_import_with_same_surface_id_is_idempotent() -> Result<(), String> {
|
|
let mut cache = IOSurfaceCache::new();
|
|
let iosurface = build_local_iosurface()?;
|
|
let port_a = iosurface.create_mach_port();
|
|
let port_b = iosurface.create_mach_port();
|
|
assert!(port_a != 0 && port_b != 0 && port_a != port_b);
|
|
|
|
cache.import(port_a, 0xAAAA_AAAA).map_err(|error| error.to_string())?;
|
|
// Same surface_id → defensive dedup path; port_b is deallocated
|
|
// without minting a duplicate CVPixelBuffer.
|
|
cache.import(port_b, 0xAAAA_AAAA).map_err(|error| error.to_string())?;
|
|
assert_eq!(cache.cached_surface_count(), 1);
|
|
Ok(())
|
|
}
|
|
}
|