Files
ELY-Browser/crates/ely_servo_host/src/hardware_rendering_context.rs
T
ZacharyZhang-NY 90c029eddb Move Servo IPC off UI thread
Root cause of the post-tab lag: the GPUI 16 ms timer was calling
`WebSurfaceRuntime::ensure_tab` and `tick` on the UI thread, and each
call did a synchronous `serde_json` write plus `read_line` against the
Servo sidecar over stdin/stdout. With even one visible tab, every
frame stalled on cross-process IPC.

Introduce `web_surface_worker.rs` — a per-profile worker thread that
owns the `ServoLiveClient`, drains a coalescing request queue
(latest Ensure/Poll per tab wins, no unbounded growth), and ships
results back through a `std::sync::mpsc` channel. `WebSurfaceRuntime`
now submits work non-blockingly and drains responses in `tick`; the
UI thread never blocks on the sidecar.

Adjacent in-flight cleanup riding along: hardware IOSurface
rendering-context completion (sidecar `live_protocol`,
`hardware_rendering_context`, GPUI BGRA surface shader), CSS viewport
size + device pixel ratio plumbing into `ServoLiveFrame`, and the
Send opt-ins for `CVPixelBuffer`-bearing types so frames can cross
the thread boundary.
2026-05-15 16:41:40 -04:00

399 lines
16 KiB
Rust

//! 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<PhysicalSize<u32>>,
inner: SurfmanInner,
swap_chain: SwapChain<Device>,
#[cfg(target_os = "macos")]
held_presented_surface: RefCell<Option<Surface>>,
#[cfg(target_os = "macos")]
last_presented_iosurface: RefCell<Option<PresentedIOSurface>>,
}
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<u32>) -> Result<Self, SurfmanError> {
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<RgbaImage> {
self.inner.read_to_image(source_rectangle)
}
fn size(&self) -> PhysicalSize<u32> {
self.size.get()
}
fn resize(&self, size: PhysicalSize<u32>) {
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<dyn Gl> {
self.inner.gleam_gl.clone()
}
fn glow_gl_api(&self) -> Arc<glow::Context> {
self.inner.glow_gl.clone()
}
fn connection(&self) -> Option<Connection> {
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<Option<IOSurfaceIdentity>, 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<IOSurfaceHandle, SurfmanError> {
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<dyn Gl>,
glow_gl: Arc<glow::Context>,
device: RefCell<Device>,
context: RefCell<Context>,
}
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<Self, SurfmanError> {
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<NativeWidget>,
) -> Result<Surface, SurfmanError> {
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<SwapChain<Device>, 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<u8>`,
/// 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<RgbaImage> {
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,
)
}
}