//! Headless hardware [`RenderingContext`] for Servo, vendored from //! `servo-paint-api`'s private `SurfmanRenderingContext` and reshaped //! so it can be constructed without a `RawWindowHandle`. //! //! Why this file exists: `servo-paint-api 0.1` exposes three //! constructors — `SoftwareRenderingContext` (CPU-only), //! `WindowRenderingContext` (requires `DisplayHandle + WindowHandle`), //! and `OffscreenRenderingContext` (must be a child of a //! `WindowRenderingContext`). The sidecar process has no window, so //! none of the three works for us when we want **hardware** //! rasterising. The underlying `SurfmanRenderingContext` glue *can* //! drive a hardware adapter against a `SurfaceType::Generic` //! offscreen surface — that's exactly what we need — but its //! constructor is `fn new` (private). Until Servo accepts an upstream //! PR exposing a headless hardware constructor, this file vendors the //! minimal slice of glue we need. //! //! Scope kept deliberately narrow: //! //! * `prepare_for_rendering`, `read_to_image`, `size`, `resize`, //! `present`, `make_current`, `gleam_gl_api`, `glow_gl_api`, and //! `connection` are vendored. `connection` is mandatory: //! `servo-paint`'s painter calls `rendering_context.connection() //! .expect("Failed to get connection")` while constructing its //! painter, so a `None` default panics the compositor before the //! first frame is ever painted. //! * `create_texture`/`destroy_texture` still fall through to the //! trait defaults — Servo only reaches for them when sharing //! surfman surfaces with its compositor for WebGL/WebGPU, which //! this readback path does not exercise. //! * No `RefreshDriver`. The sidecar drives its own polling loop. //! * The reading path inlines `read_framebuffer_to_image` from the //! same upstream file so we don't take a dependency on a private //! helper that may change shape. //! //! This is feature-gated on `hardware-render`. The default build path //! (and every existing test in this repo) keeps using //! `SoftwareRenderingContext`; the hardware constructor only exists //! when the feature is enabled, which is also when the additional //! surfman/gleam/glow deps are pulled in. #![cfg(feature = "hardware-render")] use std::cell::{Cell, RefCell}; use std::rc::Rc; use std::sync::Arc; use dpi::PhysicalSize; use euclid::Size2D; use gleam::gl::{self, Gl}; use image::RgbaImage; use servo::{DeviceIntRect, RenderingContext}; use surfman::chains::{PreserveBuffer, SwapChain, SwapChainAPI}; #[cfg(target_os = "macos")] use surfman::platform::macos::cgl::surface::NativeSurface; use surfman::{ Connection, Context, ContextAttributeFlags, ContextAttributes, Device, Error as SurfmanError, GLApi, NativeWidget, Surface, SurfaceAccess, SurfaceType, }; /// A headless hardware-backed [`RenderingContext`]. /// /// Construct with [`HardwareOffscreenContext::new`]; drop normally to /// release the surfman context, surface, and swap chain. pub struct HardwareOffscreenContext { size: Cell>, inner: SurfmanInner, swap_chain: SwapChain, #[cfg(target_os = "macos")] held_presented_surface: RefCell>, #[cfg(target_os = "macos")] last_presented_iosurface: RefCell>, } impl HardwareOffscreenContext { /// Build a new hardware context with an offscreen /// [`SurfaceType::Generic`] surface of the requested size. /// /// Uses `Connection::new()` to pick the platform default /// (CGL on macOS — which backs surfaces with `IOSurface`s — /// EGL on Linux, WGL on Windows) and `create_adapter()` for the /// real GPU adapter. Falls back nowhere: if the host can't give /// us a hardware GL context, the returned `Err` carries the /// surfman cause and the caller is expected to either retry with /// the software path or surface the failure. pub fn new(size: PhysicalSize) -> Result { let connection = Connection::new()?; let adapter = connection.create_adapter()?; let inner = SurfmanInner::new(&connection, &adapter)?; let surfman_size = Size2D::new(size.width as i32, size.height as i32); let surface = inner.create_surface(SurfaceType::Generic { size: surfman_size })?; inner.bind_surface(surface)?; inner.make_current()?; let swap_chain = inner.create_attached_swap_chain()?; Ok(Self { size: Cell::new(size), inner, swap_chain, #[cfg(target_os = "macos")] held_presented_surface: RefCell::new(None), #[cfg(target_os = "macos")] last_presented_iosurface: RefCell::new(None), }) } } impl Drop for HardwareOffscreenContext { fn drop(&mut self) { let device = &mut self.inner.device.borrow_mut(); let context = &mut self.inner.context.borrow_mut(); #[cfg(target_os = "macos")] self.destroy_held_presented_surface(device, context); let _ = self.swap_chain.destroy(device, context); } } impl RenderingContext for HardwareOffscreenContext { fn prepare_for_rendering(&self) { self.inner.prepare_for_rendering(); } fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option { self.inner.read_to_image(source_rectangle) } fn size(&self) -> PhysicalSize { self.size.get() } fn resize(&self, size: PhysicalSize) { if self.size.get() == size { return; } self.size.set(size); let device = &mut self.inner.device.borrow_mut(); let context = &mut self.inner.context.borrow_mut(); #[cfg(target_os = "macos")] self.destroy_held_presented_surface(device, context); let size = Size2D::new(size.width as i32, size.height as i32); let _ = self.swap_chain.resize(device, context, size); } fn present(&self) { let device = &mut self.inner.device.borrow_mut(); let context = &mut self.inner.context.borrow_mut(); #[cfg(target_os = "macos")] self.recycle_held_presented_surface(); let _ = self.swap_chain.swap_buffers(device, context, PreserveBuffer::No); #[cfg(target_os = "macos")] self.capture_presented_iosurface(device); } fn make_current(&self) -> Result<(), SurfmanError> { self.inner.make_current() } fn gleam_gl_api(&self) -> Rc { self.inner.gleam_gl.clone() } fn glow_gl_api(&self) -> Arc { self.inner.glow_gl.clone() } fn connection(&self) -> Option { Some(self.inner.device.borrow().connection()) } } #[cfg(target_os = "macos")] use crate::iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity}; #[cfg(target_os = "macos")] impl HardwareOffscreenContext { /// Cheap, non-mutating identity probe of the IOSurface that was /// just presented. Used by the sidecar to dedup mach port creation. pub fn peek_iosurface_identity(&self) -> Result, SurfmanError> { Ok(self.last_presented_iosurface.borrow().as_ref().map(|surface| surface.identity)) } /// Snapshot the just-presented IOSurface and 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. pub fn current_iosurface_mach_port(&self) -> Result { let presented = self.last_presented_iosurface.borrow(); let presented = presented.as_ref().ok_or(SurfmanError::Failed)?; let mach_port = presented.native.0.create_mach_port(); Ok(IOSurfaceHandle { mach_port_name: mach_port, surface_id: presented.identity.surface_id, width: presented.identity.width, height: presented.identity.height, }) } fn capture_presented_iosurface(&self, device: &mut Device) { let Some(surface) = self.swap_chain.take_pending_surface() else { self.last_presented_iosurface.borrow_mut().take(); return; }; let info = device.surface_info(&surface); let native = device.native_surface(&surface); let identity = 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), }; self.held_presented_surface.replace(Some(surface)); self.last_presented_iosurface.replace(Some(PresentedIOSurface { identity, native })); } fn recycle_held_presented_surface(&self) { if let Some(surface) = self.held_presented_surface.borrow_mut().take() { self.swap_chain.recycle_surface(surface); } } fn destroy_held_presented_surface(&self, device: &mut Device, context: &mut Context) { self.last_presented_iosurface.borrow_mut().take(); if let Some(mut surface) = self.held_presented_surface.borrow_mut().take() { let _ = device.destroy_surface(context, &mut surface); } } } #[cfg(target_os = "macos")] struct PresentedIOSurface { identity: IOSurfaceIdentity, native: NativeSurface, } /// Trimmed mirror of `paint_api::rendering_context::SurfmanRenderingContext`. /// /// Only the methods the public type above actually uses are kept; the /// upstream original also wires up texture sharing, refresh drivers, /// and several other knobs that Servo's compositor reaches into but /// the embedder's headless readback path does not. struct SurfmanInner { gleam_gl: Rc, glow_gl: Arc, device: RefCell, context: RefCell, } impl Drop for SurfmanInner { fn drop(&mut self) { let device = &mut self.device.borrow_mut(); let context = &mut self.context.borrow_mut(); let _ = device.destroy_context(context); } } impl SurfmanInner { fn new(connection: &Connection, adapter: &surfman::Adapter) -> Result { let device = connection.create_device(adapter)?; let flags = ContextAttributeFlags::ALPHA | ContextAttributeFlags::DEPTH | ContextAttributeFlags::STENCIL; let gl_api = connection.gl_api(); let version = match &gl_api { GLApi::GLES => surfman::GLVersion { major: 3, minor: 0 }, GLApi::GL => surfman::GLVersion { major: 3, minor: 2 }, }; let context_descriptor = device.create_context_descriptor(&ContextAttributes { flags, version })?; let context = device.create_context(&context_descriptor, None)?; // Loading the GL function pointers requires unsafe ABI calls // through surfman's `get_proc_address` — these are the same // calls the upstream `SurfmanRenderingContext::new` makes, // and they're sound for the same reason: surfman guarantees // the returned function pointers match the requested API. #[expect(unsafe_code)] let gleam_gl = { match gl_api { GLApi::GL => unsafe { gl::GlFns::load_with(|name| device.get_proc_address(&context, name)) }, GLApi::GLES => unsafe { gl::GlesFns::load_with(|name| device.get_proc_address(&context, name)) }, } }; #[expect(unsafe_code)] let glow_gl = unsafe { glow::Context::from_loader_function(|name| device.get_proc_address(&context, name)) }; Ok(Self { gleam_gl, glow_gl: Arc::new(glow_gl), device: RefCell::new(device), context: RefCell::new(context), }) } fn create_surface( &self, surface_type: SurfaceType, ) -> Result { let device = &mut self.device.borrow_mut(); let context = &self.context.borrow(); device.create_surface(context, SurfaceAccess::GPUOnly, surface_type) } fn bind_surface(&self, surface: Surface) -> Result<(), SurfmanError> { let device = &self.device.borrow(); let context = &mut self.context.borrow_mut(); device.bind_surface_to_context(context, surface).map_err(|(err, mut surface)| { let _ = device.destroy_surface(context, &mut surface); err })?; Ok(()) } fn create_attached_swap_chain(&self) -> Result, SurfmanError> { let device = &mut self.device.borrow_mut(); let context = &mut self.context.borrow_mut(); SwapChain::create_attached(device, context, SurfaceAccess::GPUOnly) } fn make_current(&self) -> Result<(), SurfmanError> { let device = &self.device.borrow(); let context = &self.context.borrow(); device.make_context_current(context) } fn framebuffer_id(&self) -> u32 { let device = &self.device.borrow(); let context = &self.context.borrow(); device .context_surface_info(context) .unwrap_or(None) .and_then(|info| info.framebuffer_object) .map_or(0, |framebuffer| framebuffer.0.into()) } fn prepare_for_rendering(&self) { let framebuffer_id = self.framebuffer_id(); self.gleam_gl.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_id); } /// Inlined copy of `Framebuffer::read_framebuffer_to_image` from /// `paint-api`. Reads the bound framebuffer into a `Vec`, /// flips it vertically (GL's origin is bottom-left, the rest of /// the embedder expects top-left), and returns it as an /// [`RgbaImage`]. Returns `None` if `RgbaImage::from_raw` rejects /// the buffer (size mismatch); GL errors are logged but don't /// abort the read — the caller can decide whether a corrupt /// frame is recoverable. fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option { let framebuffer_id = self.framebuffer_id(); self.gleam_gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id); // Working around an OSMesa headless bug carried forward from // the upstream implementation, see servo/servo#18606. self.gleam_gl.bind_vertex_array(0); let mut pixels = self.gleam_gl.read_pixels( source_rectangle.min.x, source_rectangle.min.y, source_rectangle.width(), source_rectangle.height(), gl::RGBA, gl::UNSIGNED_BYTE, ); let gl_error = self.gleam_gl.get_error(); if gl_error != gl::NO_ERROR { log::warn!("GL error 0x{gl_error:x} after read_pixels in hardware offscreen context"); } let source_rectangle = source_rectangle.to_usize(); let stride = source_rectangle.width().checked_mul(4)?; let mirror = pixels.clone(); for y in 0..source_rectangle.height() { let dst_start = y.checked_mul(stride)?; let src_start = (source_rectangle.height().checked_sub(y + 1)?).checked_mul(stride)?; let dst_end = dst_start.checked_add(stride)?; let src_end = src_start.checked_add(stride)?; if dst_end > pixels.len() || src_end > mirror.len() { return None; } pixels[dst_start..dst_end].clone_from_slice(&mirror[src_start..src_end]); } RgbaImage::from_raw( source_rectangle.width() as u32, source_rectangle.height() as u32, pixels, ) } }