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.
399 lines
16 KiB
Rust
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,
|
|
)
|
|
}
|
|
}
|