T10.5: render IOSurface via gpui::surface(CVPixelBuffer)

This commit is contained in:
2026-05-10 23:37:00 -04:00
parent c4a6ea3c57
commit a447d52262
6 changed files with 177 additions and 177 deletions
Generated
+3 -4
View File
@@ -2243,6 +2243,8 @@ name = "ely_app"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ahash", "ahash",
"core-foundation 0.10.0",
"core-video",
"directories", "directories",
"ed25519-dalek", "ed25519-dalek",
"ely_browser_core", "ely_browser_core",
@@ -2252,11 +2254,11 @@ dependencies = [
"gpui-component", "gpui-component",
"gpui-component-assets", "gpui-component-assets",
"image", "image",
"io-surface",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"objc2-io-surface", "objc2-io-surface",
"objc2-metal",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -6150,11 +6152,8 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2",
"dispatch2",
"objc2", "objc2",
"objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"objc2-io-surface",
] ]
[[package]] [[package]]
+6 -8
View File
@@ -29,18 +29,16 @@ ureq.workspace = true
url.workspace = true url.workspace = true
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.10"
# Pin to the same minor as gpui 0.2.2 so we share its `CVPixelBuffer`
# type — gpui's `Surface::From<CVPixelBuffer>` only matches the
# version it built against.
core-video = "0.4"
io-surface = "0.16"
objc2 = "0.6" objc2 = "0.6"
objc2-core-foundation = { version = "0.3.2", features = ["CFBase", "CFDictionary", "CFNumber", "CFString"] } objc2-core-foundation = { version = "0.3.2", features = ["CFBase", "CFDictionary", "CFNumber", "CFString"] }
objc2-foundation = { version = "0.3.1", features = ["NSDictionary", "NSString", "NSValue"] } objc2-foundation = { version = "0.3.1", features = ["NSDictionary", "NSString", "NSValue"] }
objc2-io-surface = "0.3.2" objc2-io-surface = "0.3.2"
objc2-metal = { version = "0.3.2", features = [
"MTLAllocation",
"MTLDevice",
"MTLPixelFormat",
"MTLResource",
"MTLTexture",
"objc2-io-surface",
] }
[dev-dependencies] [dev-dependencies]
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
+91 -126
View File
@@ -1,65 +1,58 @@
//! macOS-only import of cross-process IOSurface handles into Metal //! macOS-only import of cross-process IOSurface handles into
//! textures. //! `CVPixelBuffer`s suitable for GPUI's `Surface` element.
//! //!
//! `T10.4`: the sidecar publishes an [`crate::services::servo_live`] //! `T10.4` originally imported the IOSurface into an `MTLTexture`
//! `surface_handle` carrying a `mach_port_name` + stable `surface_id`. //! directly, but GPUI 0.2.2 already speaks `CVPixelBuffer` end-to-end
//! The receiver builds an `MTLTexture` from that IOSurface exactly //! through `Window::paint_surface` / `elements::surface::Surface`. Its
//! once per `surface_id` and caches it. Every frame after the first //! internal Blade Metal renderer takes care of building the Metal
//! reads `current_surface_id` and samples the cached texture — zero //! texture, so a parallel MTLTexture cache here would be wasted work.
//! pixel copy, zero re-import. //! The cache now hands the renderer the CVPixelBuffer GPUI already
//! knows how to render.
//! //!
//! Lifetime contract: //! Lifetime contract:
//! //!
//! * `IOSurfaceCreateMachPort` (sidecar side) gives the receiver a //! * `IOSurfaceCreateMachPort` (sidecar side) gives the receiver a
//! send right whose refcount is 1 in our task. Once we've called //! send right whose refcount is 1 in our task. After we resolve
//! `IOSurfaceLookupFromMachPort` to materialise the //! the surface and wrap it in a CVPixelBuffer, the mach port has
//! `IOSurfaceRef`, the mach port has done its job. //! done its job.
//! * Metal's `newTextureWithDescriptor:iosurface:plane:` retains the //! * `CVPixelBufferCreateWithIOSurface` retains the IOSurface for
//! IOSurface for the texture's lifetime. We `mach_port_deallocate` //! the pixel buffer's lifetime. We `mach_port_deallocate`
//! immediately afterwards so the receiver process doesn't //! immediately so the receiver process doesn't accumulate idle
//! accumulate idle mach send rights. //! mach send rights.
//! * Dropping `MetalSurfaceImporter` releases every cached //! * Dropping `IOSurfaceCache` releases every cached
//! `MTLTexture`, which in turn releases each retained IOSurface. //! `CVPixelBuffer`, which in turn releases each retained
//! The sidecar still holds its own retain via surfman, so the //! IOSurface. The sidecar still holds its own retain via surfman,
//! IOSurface itself outlives our cache for as long as the sidecar //! so the IOSurface itself outlives our cache for as long as the
//! keeps painting. //! sidecar keeps painting.
#![cfg(target_os = "macos")] #![cfg(target_os = "macos")]
use std::collections::HashMap; use std::collections::HashMap;
use objc2::rc::Retained; use core_foundation::base::TCFType as _;
use objc2::runtime::ProtocolObject; use core_video::pixel_buffer::CVPixelBuffer;
use objc2_foundation::NSUInteger; #[allow(deprecated)]
use objc2_io_surface::IOSurfaceRef; use io_surface::IOSurface;
use objc2_metal::{
MTLCreateSystemDefaultDevice, MTLDevice, MTLPixelFormat, MTLTexture, MTLTextureDescriptor,
MTLTextureUsage,
};
use thiserror::Error; use thiserror::Error;
/// Owner of the system Metal device + cache of imported textures /// Cache of imported `CVPixelBuffer`s keyed by IOSurface identity.
/// keyed by IOSurface identity. Constructed once per renderer process /// Constructed lazily by the renderer-side client on the first
/// when the first hardware-path tab requests an upload. /// hardware-path frame.
pub(crate) struct MetalSurfaceImporter { pub(crate) struct IOSurfaceCache {
device: Retained<ProtocolObject<dyn MTLDevice>>, pixel_buffers: HashMap<u64, CVPixelBuffer>,
textures: HashMap<u64, Retained<ProtocolObject<dyn MTLTexture>>>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub(crate) enum SurfaceImportError { pub(crate) enum SurfaceImportError {
#[error("system has no default Metal device — hardware path unavailable")]
NoMetalDevice,
#[error("IOSurfaceLookupFromMachPort returned null for port 0x{port:x}")] #[error("IOSurfaceLookupFromMachPort returned null for port 0x{port:x}")]
LookupFailed { port: u32 }, LookupFailed { port: u32 },
#[error("MTLDevice rejected the IOSurface (size {width}x{height})")] #[error("CVPixelBufferCreateWithIOSurface returned status {status}")]
TextureBuildFailed { width: u32, height: u32 }, PixelBufferBuildFailed { status: i32 },
} }
impl MetalSurfaceImporter { impl IOSurfaceCache {
pub fn new() -> Result<Self, SurfaceImportError> { pub fn new() -> Self {
let device = MTLCreateSystemDefaultDevice().ok_or(SurfaceImportError::NoMetalDevice)?; Self { pixel_buffers: HashMap::new() }
Ok(Self { device, textures: HashMap::new() })
} }
/// Import an IOSurface published by the sidecar's /// Import an IOSurface published by the sidecar's
@@ -72,71 +65,59 @@ impl MetalSurfaceImporter {
&mut self, &mut self,
mach_port_name: u32, mach_port_name: u32,
surface_id: u64, surface_id: u64,
width: u32,
height: u32,
) -> Result<(), SurfaceImportError> { ) -> Result<(), SurfaceImportError> {
if self.textures.contains_key(&surface_id) { if self.pixel_buffers.contains_key(&surface_id) {
deallocate_mach_port(mach_port_name); deallocate_mach_port(mach_port_name);
return Ok(()); return Ok(());
} }
let Some(iosurface) = IOSurfaceRef::lookup_from_mach_port(mach_port_name) else { let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
// Lookup failed → port is invalid; nothing to deallocate. else {
return Err(SurfaceImportError::LookupFailed { port: mach_port_name }); return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
}; };
let descriptor = MTLTextureDescriptor::new(); // Both objc2-io-surface and the legacy `io_surface` crate wrap
// surfman's macOS surface backs IOSurface with // the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
// kCVPixelFormatType_32BGRA — match it so Metal samples the // (via core-video) expects the legacy crate's wrapper. Reach for
// correct channel order. `setUsage(ShaderRead)` is the minimum // the raw pointer and let TCFType CFRetain it independently so
// Metal needs to expose the texture to a fragment shader sampler. // both Rust handles can drop without double-freeing.
// The setters are unsafe because they cross the FFI boundary let raw_ptr: *const std::ffi::c_void =
// without descriptor validation; we know our values are sound. (&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
#[expect(unsafe_code)] #[allow(deprecated)]
unsafe { let io_surface_view: IOSurface = {
descriptor.setPixelFormat(MTLPixelFormat::BGRA8Unorm); #[expect(unsafe_code)]
descriptor.setWidth(width as NSUInteger); unsafe {
descriptor.setHeight(height as NSUInteger); IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
descriptor.setUsage(MTLTextureUsage::ShaderRead); }
} };
let texture = self let pixel_buffer = CVPixelBuffer::from_io_surface(&io_surface_view, None)
.device .map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })?;
.newTextureWithDescriptor_iosurface_plane(&descriptor, &iosurface, 0)
.ok_or(SurfaceImportError::TextureBuildFailed { width, height })?;
self.textures.insert(surface_id, texture); self.pixel_buffers.insert(surface_id, pixel_buffer);
deallocate_mach_port(mach_port_name); deallocate_mach_port(mach_port_name);
Ok(()) Ok(())
} }
/// Look up an already-imported texture by `surface_id`. The /// Look up an already-imported pixel buffer by `surface_id`. The
/// receiver's per-frame `current_surface_id` field selects which /// receiver's per-frame `current_surface_id` selects which of the
/// of the chain's rotating front/back surfaces to sample. /// swap chain's rotating front/back surfaces to sample. Returns a
/// `#[allow(dead_code)]` until T10.5 wires the renderer; the /// clone (CVPixelBuffer is reference-counted; cloning is a cheap
/// import side is exercised by the live perf bench right now. /// atomic increment) so the caller can hand it to GPUI's
#[allow(dead_code)] /// `surface(...)` element without holding a borrow on the cache.
pub fn texture_for( pub fn pixel_buffer_for(&self, surface_id: u64) -> Option<CVPixelBuffer> {
&self, self.pixel_buffers.get(&surface_id).cloned()
surface_id: u64,
) -> Option<&Retained<ProtocolObject<dyn MTLTexture>>> {
self.textures.get(&surface_id)
} }
/// Reports how many distinct surfaces have been imported. Used
/// by tests and the live perf bench to assert that dedup is
/// keeping the cache small (one per swap-chain surface).
#[cfg(test)] #[cfg(test)]
pub fn cached_surface_count(&self) -> usize { pub fn cached_surface_count(&self) -> usize {
self.textures.len() self.pixel_buffers.len()
} }
} }
/// Release one send right against the mach port we received. The /// Release one send right against the mach port we received. The
/// IOSurface itself stays alive because the `MTLTexture` (or the /// IOSurface itself stays alive because the `CVPixelBuffer` (or the
/// sidecar's surfman) still retain it. A `KERN_INVALID_NAME` failure /// sidecar's surfman) still retain it.
/// here means the port already drained — survivable, log and move
/// on.
fn deallocate_mach_port(port: u32) { fn deallocate_mach_port(port: u32) {
#[expect(unsafe_code)] #[expect(unsafe_code)]
let result = unsafe { mach_port_deallocate(mach_task_self_, port) }; let result = unsafe { mach_port_deallocate(mach_task_self_, port) };
@@ -160,13 +141,13 @@ unsafe extern "C" {
/// Releases one send right against `name` within `task`. We only /// Releases one send right against `name` within `task`. We only
/// ever call this with our own task; the IOSurface keeps its /// ever call this with our own task; the IOSurface keeps its
/// retain via the MTLTexture so this just frees our port slot. /// retain via the CVPixelBuffer so this just frees our port slot.
fn mach_port_deallocate(task: u32, name: u32) -> i32; fn mach_port_deallocate(task: u32, name: u32) -> i32;
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{MetalSurfaceImporter, SurfaceImportError}; use super::IOSurfaceCache;
use objc2_core_foundation::{ use objc2_core_foundation::{
CFDictionary, CFIndex, CFNumber, CFRetained, CFString, kCFAllocatorDefault, CFDictionary, CFIndex, CFNumber, CFRetained, CFString, kCFAllocatorDefault,
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
@@ -175,7 +156,6 @@ mod tests {
IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceBytesPerRow, kIOSurfaceHeight, IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceBytesPerRow, kIOSurfaceHeight,
kIOSurfacePixelFormat, kIOSurfaceWidth, kIOSurfacePixelFormat, kIOSurfaceWidth,
}; };
use objc2_metal::MTLTexture as _;
use std::os::raw::c_void; use std::os::raw::c_void;
const TEST_WIDTH: u32 = 64; const TEST_WIDTH: u32 = 64;
@@ -185,10 +165,7 @@ mod tests {
/// surfman's macOS backend does — BGRA8 (four-cc '32BGRA'), width /// surfman's macOS backend does — BGRA8 (four-cc '32BGRA'), width
/// + height + bytes_per_element + bytes_per_row in a Core /// + height + bytes_per_element + bytes_per_row in a Core
/// Foundation properties dictionary. The pointer-casts mirror /// Foundation properties dictionary. The pointer-casts mirror
/// `surfman::platform::macos::system::surface::create_io_surface`; /// `surfman::platform::macos::system::surface::create_io_surface`.
/// the dict has CFString keys and CFNumber values and is built
/// with `kCFTypeDictionaryKeyCallBacks` / `kCFTypeDictionaryValueCallBacks`
/// so CF retains its entries.
fn build_local_iosurface() -> CFRetained<IOSurfaceRef> { fn build_local_iosurface() -> CFRetained<IOSurfaceRef> {
let pixel_format: i32 = i32::from_be_bytes(*b"BGRA"); let pixel_format: i32 = i32::from_be_bytes(*b"BGRA");
let bytes_per_element: i32 = 4; let bytes_per_element: i32 = 4;
@@ -227,55 +204,43 @@ mod tests {
} }
#[test] #[test]
fn imports_local_iosurface_into_mtl_texture() { fn imports_local_iosurface_into_pixel_buffer() {
let mut importer = match MetalSurfaceImporter::new() { let mut cache = IOSurfaceCache::new();
Ok(importer) => importer,
Err(SurfaceImportError::NoMetalDevice) => {
eprintln!(
"no Metal device on this host — \
acceptable in headless / no-GPU CI; skipping"
);
return;
}
Err(error) => panic!("unexpected importer error: {error:?}"),
};
let iosurface = build_local_iosurface(); let iosurface = build_local_iosurface();
let mach_port = iosurface.create_mach_port(); let mach_port = iosurface.create_mach_port();
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port"); assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
let surface_id: u64 = 0xDEAD_BEEFu64; let surface_id: u64 = 0xDEAD_BEEFu64;
importer cache.import(mach_port, surface_id).expect("local IOSurface must round-trip into a CVPixelBuffer");
.import(mach_port, surface_id, TEST_WIDTH, TEST_HEIGHT)
.expect("local IOSurface must round-trip through MTLDevice");
let texture = importer let pixel_buffer = cache
.texture_for(surface_id) .pixel_buffer_for(surface_id)
.expect("imported texture must be retrievable by surface_id"); .expect("imported pixel buffer must be retrievable by surface_id");
assert_eq!(texture.width(), TEST_WIDTH as usize, "MTLTexture width must match"); assert_eq!(
assert_eq!(texture.height(), TEST_HEIGHT as usize, "MTLTexture height must match"); pixel_buffer.get_width() as u32,
assert_eq!(importer.cached_surface_count(), 1); 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);
} }
#[test] #[test]
fn second_import_with_same_surface_id_is_idempotent() { fn second_import_with_same_surface_id_is_idempotent() {
let mut importer = match MetalSurfaceImporter::new() { let mut cache = IOSurfaceCache::new();
Ok(importer) => importer,
Err(SurfaceImportError::NoMetalDevice) => return,
Err(error) => panic!("unexpected importer error: {error:?}"),
};
let iosurface = build_local_iosurface(); let iosurface = build_local_iosurface();
let port_a = iosurface.create_mach_port(); let port_a = iosurface.create_mach_port();
let port_b = iosurface.create_mach_port(); let port_b = iosurface.create_mach_port();
assert!(port_a != 0 && port_b != 0 && port_a != port_b); assert!(port_a != 0 && port_b != 0 && port_a != port_b);
importer.import(port_a, 0xAAAA_AAAA, TEST_WIDTH, TEST_HEIGHT).expect("first import"); cache.import(port_a, 0xAAAA_AAAA).expect("first import");
// Same surface_id → defensive dedup path; port_b is deallocated // Same surface_id → defensive dedup path; port_b is deallocated
// without minting a duplicate MTLTexture. // without minting a duplicate CVPixelBuffer.
importer cache.import(port_b, 0xAAAA_AAAA).expect("duplicate import is idempotent");
.import(port_b, 0xAAAA_AAAA, TEST_WIDTH, TEST_HEIGHT) assert_eq!(cache.cached_surface_count(), 1);
.expect("duplicate import is idempotent");
assert_eq!(importer.cached_surface_count(), 1);
} }
} }
+43 -39
View File
@@ -23,18 +23,19 @@ use thiserror::Error;
use super::servo_sidecar_command::{SidecarCommandError, default_sidecar_command}; use super::servo_sidecar_command::{SidecarCommandError, default_sidecar_command};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use super::iosurface_metal::MetalSurfaceImporter; use super::iosurface_metal::IOSurfaceCache;
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
pub(crate) struct ServoLiveClient { pub(crate) struct ServoLiveClient {
child: Child, child: Child,
stdin: ChildStdin, stdin: ChildStdin,
stdout: BufReader<ChildStdout>, stdout: BufReader<ChildStdout>,
/// Cache of imported Metal textures keyed by surface_id. Built /// Cache of imported `CVPixelBuffer`s keyed by surface_id. Built
/// lazily on the first `surface_handle` the sidecar publishes — /// lazily on the first `surface_handle` the sidecar publishes —
/// software-path tabs never trigger construction, so machines /// software-path tabs never trigger construction.
/// without a Metal device aren't penalised.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
metal_importer: Option<MetalSurfaceImporter>, iosurface_cache: IOSurfaceCache,
} }
impl ServoLiveClient { impl ServoLiveClient {
@@ -64,10 +65,11 @@ impl ServoLiveClient {
stdin, stdin,
stdout: BufReader::new(stdout), stdout: BufReader::new(stdout),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
metal_importer: None, iosurface_cache: IOSurfaceCache::new(),
}) })
} }
pub fn ensure( pub fn ensure(
&mut self, &mut self,
request: ServoLiveEnsureRequest, request: ServoLiveEnsureRequest,
@@ -154,7 +156,14 @@ impl ServoLiveClient {
.read_exact(&mut rgba_bytes) .read_exact(&mut rgba_bytes)
.map_err(ServoLiveError::FrameRead)?; .map_err(ServoLiveError::FrameRead)?;
Ok(Some(ServoLiveFrame::from_parts(report, rgba_bytes))) let mut frame = ServoLiveFrame::from_parts(report, rgba_bytes);
#[cfg(target_os = "macos")]
if let Some(surface_id) = response.current_surface_id {
frame.pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
}
Ok(Some(frame))
} }
} }
@@ -167,49 +176,25 @@ impl Drop for ServoLiveClient {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
impl ServoLiveClient { impl ServoLiveClient {
/// Run a freshly-arrived `surface_handle` through the Metal /// Convert the sidecar's `surface_handle` into a `CVPixelBuffer`
/// importer. Lazily constructs the importer on first call so /// in the local cache. Failures are logged but don't error the
/// software-only sessions never touch the GPU. Failures are /// request — the renderer falls back to the existing software
/// logged but don't error the request — T10.5 will fall back to /// `Arc<RenderImage>` path when no pixel buffer is available, so
/// the existing software RGBA path if `texture_for` returns /// the user always sees a frame.
/// `None`, so the user still sees a frame.
fn import_iosurface_handle(&mut self, handle: &LiveSurfaceHandle) { fn import_iosurface_handle(&mut self, handle: &LiveSurfaceHandle) {
if self.metal_importer.is_none() { match self.iosurface_cache.import(handle.mach_port_name, handle.surface_id) {
match MetalSurfaceImporter::new() {
Ok(importer) => {
self.metal_importer = Some(importer);
}
Err(error) => {
tracing::warn!(
target: "ely::servo::iosurface",
error = %error,
"no Metal device for IOSurface import; staying on software path",
);
return;
}
}
}
let Some(importer) = self.metal_importer.as_mut() else {
return;
};
match importer.import(
handle.mach_port_name,
handle.surface_id,
handle.width,
handle.height,
) {
Ok(()) => tracing::info!( Ok(()) => tracing::info!(
target: "ely::servo::iosurface", target: "ely::servo::iosurface",
surface_id = handle.surface_id, surface_id = handle.surface_id,
width = handle.width, width = handle.width,
height = handle.height, height = handle.height,
"imported IOSurface into Metal texture cache", "imported IOSurface into CVPixelBuffer cache",
), ),
Err(error) => tracing::warn!( Err(error) => tracing::warn!(
target: "ely::servo::iosurface", target: "ely::servo::iosurface",
error = %error, error = %error,
surface_id = handle.surface_id, surface_id = handle.surface_id,
"IOSurface→MTLTexture import failed; subsequent samples will miss", "IOSurface→CVPixelBuffer import failed; subsequent samples will miss",
), ),
} }
} }
@@ -262,6 +247,12 @@ pub(crate) struct ServoLiveFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64, sample_hash: u64,
rgba_bytes: Vec<u8>, rgba_bytes: Vec<u8>,
/// Hardware-path companion: when present, the renderer can hand
/// the underlying IOSurface straight to GPUI's Metal pipeline via
/// `gpui::surface(...)` and skip the RGBA upload entirely. Always
/// `None` on the software path and on non-macOS hosts.
#[cfg(target_os = "macos")]
pixel_buffer: Option<CVPixelBuffer>,
} }
impl ServoLiveFrame { impl ServoLiveFrame {
@@ -279,9 +270,20 @@ impl ServoLiveFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: report.sample_hash, sample_hash: report.sample_hash,
rgba_bytes, rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
} }
} }
/// Returns the imported `CVPixelBuffer` matching the frame's
/// current hardware surface, if any. The renderer hands this to
/// `gpui::surface(...)` to skip the RGBA→texture upload path.
#[cfg(target_os = "macos")]
#[must_use]
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
self.pixel_buffer.as_ref()
}
#[must_use] #[must_use]
pub fn loaded_url(&self) -> Option<&str> { pub fn loaded_url(&self) -> Option<&str> {
self.loaded_url.as_deref() self.loaded_url.as_deref()
@@ -345,6 +347,8 @@ impl ServoLiveFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0, sample_hash: 0,
rgba_bytes, rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
} }
} }
} }
@@ -3,6 +3,8 @@ use std::hash::Hasher;
use std::sync::Arc; use std::sync::Arc;
use ahash::AHasher; use ahash::AHasher;
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
use gpui::RenderImage; use gpui::RenderImage;
use image::{ImageBuffer, Rgba}; use image::{ImageBuffer, Rgba};
use thiserror::Error; use thiserror::Error;
@@ -56,6 +58,13 @@ pub(super) struct WebSurfaceFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64, sample_hash: u64,
pub(super) image: Arc<RenderImage>, pub(super) image: Arc<RenderImage>,
/// Hardware-path companion: when present, the view samples the
/// IOSurface through GPUI's Metal pipeline via `gpui::surface(...)`
/// instead of uploading the RGBA bytes again. Always `None` on
/// the software path; the RGBA copy in `image` is the source of
/// truth in that case.
#[cfg(target_os = "macos")]
pub(super) pixel_buffer: Option<CVPixelBuffer>,
} }
impl WebSurfaceFrame { impl WebSurfaceFrame {
@@ -65,6 +74,8 @@ impl WebSurfaceFrame {
zoom_percent: u16, zoom_percent: u16,
frame: ServoLiveFrame, frame: ServoLiveFrame,
) -> Result<Self, WebSurfaceError> { ) -> Result<Self, WebSurfaceError> {
#[cfg(target_os = "macos")]
let pixel_buffer = frame.pixel_buffer().cloned();
Self::from_parts(WebSurfaceFrameParts { Self::from_parts(WebSurfaceFrameParts {
requested_url, requested_url,
loaded_url: frame.loaded_url().map(str::to_string), loaded_url: frame.loaded_url().map(str::to_string),
@@ -83,6 +94,8 @@ impl WebSurfaceFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: frame.sample_hash(), sample_hash: frame.sample_hash(),
rgba_bytes: frame.into_rgba_bytes(), rgba_bytes: frame.into_rgba_bytes(),
#[cfg(target_os = "macos")]
pixel_buffer,
}) })
} }
@@ -108,6 +121,8 @@ impl WebSurfaceFrame {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: parts.sample_hash, sample_hash: parts.sample_hash,
image, image,
#[cfg(target_os = "macos")]
pixel_buffer: parts.pixel_buffer,
}) })
} }
@@ -200,6 +215,8 @@ struct WebSurfaceFrameParts {
#[cfg(all(test, feature = "live-site-smoke"))] #[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64, sample_hash: u64,
rgba_bytes: Vec<u8>, rgba_bytes: Vec<u8>,
#[cfg(target_os = "macos")]
pixel_buffer: Option<CVPixelBuffer>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -3,6 +3,8 @@ use gpui::{
AnyElement, App, Entity, ImageSource, InteractiveElement, IntoElement, MouseButton, ObjectFit, AnyElement, App, Entity, ImageSource, InteractiveElement, IntoElement, MouseButton, ObjectFit,
ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb, ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb,
}; };
#[cfg(target_os = "macos")]
use gpui::surface;
use super::{ElyShell, web_surface_frame::WebSurfaceFrame}; use super::{ElyShell, web_surface_frame::WebSurfaceFrame};
use ely_design_system::colors; use ely_design_system::colors;
@@ -12,6 +14,21 @@ pub(super) fn render_ready_web_surface(
tab: &BrowserTab, tab: &BrowserTab,
state_entity: Entity<ElyShell>, state_entity: Entity<ElyShell>,
) -> AnyElement { ) -> AnyElement {
// Prefer the hardware path when the sidecar published an
// IOSurface and we successfully imported it into a CVPixelBuffer.
// GPUI's `surface(...)` hands the buffer to its Blade Metal
// renderer, which samples the IOSurface directly — no
// RGBA→texture upload, no LAST_FRAME_IMAGE dedup needed. Falls
// back to the software RGBA image when the buffer is missing
// (software webview, import failure, non-macOS host).
#[cfg(target_os = "macos")]
if let Some(pixel_buffer) = frame.pixel_buffer.as_ref() {
return render_web_surface(
tab,
state_entity,
surface(pixel_buffer.clone()).size_full().object_fit(ObjectFit::Fill),
);
}
render_web_surface( render_web_surface(
tab, tab,
state_entity, state_entity,