From 2a9f03481530e6084efb946321d8189fe5b78f1b Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 11 Mar 2026 14:52:13 +0100 Subject: [PATCH] feat(servo-shell): add servo-embed feature gate and embedder contract Adds src/embedder.rs with the full Servo embedding implementation behind #[cfg(feature = " servo-embed)]: --- crates/weft-servo-shell/Cargo.toml | 6 + crates/weft-servo-shell/SERVO_PIN.md | 81 +++++++++ crates/weft-servo-shell/src/embedder.rs | 227 ++++++++++++++++++++++++ crates/weft-servo-shell/src/main.rs | 22 ++- 4 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 crates/weft-servo-shell/SERVO_PIN.md create mode 100644 crates/weft-servo-shell/src/embedder.rs diff --git a/crates/weft-servo-shell/Cargo.toml b/crates/weft-servo-shell/Cargo.toml index 0a834b8..d1f952d 100644 --- a/crates/weft-servo-shell/Cargo.toml +++ b/crates/weft-servo-shell/Cargo.toml @@ -8,6 +8,12 @@ rust-version.workspace = true name = "weft-servo-shell" path = "src/main.rs" +[features] +# Enable actual Servo rendering. Requires manually adding the deps listed in +# SERVO_PIN.md to this file before building; they are not included here to +# avoid pulling the Servo monorepo (~1 GB) into every `cargo check` cycle. +servo-embed = [] + [dependencies] anyhow = "1.0" tracing = "0.1" diff --git a/crates/weft-servo-shell/SERVO_PIN.md b/crates/weft-servo-shell/SERVO_PIN.md new file mode 100644 index 0000000..ed7d364 --- /dev/null +++ b/crates/weft-servo-shell/SERVO_PIN.md @@ -0,0 +1,81 @@ +# Servo Pin + +## Current pin + +| Field | Value | +|---------|--------------------------------------------------------------------------| +| Source | | +| Rev | `c242860f0ef4e7c6e60dfea29310167898e6eb38` (main, 2026-03-11) | +| Crate | `servo` (package name as of 2026-03-11; previously `libservo`) | +| Feature | `servo-embed` (optional; off by default) | + +## Adding the Cargo dependencies + +The Servo deps are **not** in `Cargo.toml` by default to avoid pulling the +Servo monorepo (~1 GB) into every `cargo check` cycle. To activate, add the +following to `crates/weft-servo-shell/Cargo.toml` and change the `servo-embed` +feature line to declare `dep:servo`, `dep:winit`, and `dep:softbuffer`: + +```toml +[features] +servo-embed = ["dep:servo", "dep:winit", "dep:softbuffer"] + +[dependencies.servo] +git = "https://github.com/servo/servo" +rev = "c242860f0ef4e7c6e60dfea29310167898e6eb38" +optional = true +default-features = false + +[dependencies.winit] +version = "0.30" +optional = true +features = ["wayland"] + +[dependencies.softbuffer] +version = "0.4" +optional = true +``` + +Then build: + +```sh +cargo build -p weft-servo-shell --features servo-embed +``` + +The first build downloads and compiles Servo and its dependencies, which takes +30–60 minutes cold. Subsequent incremental builds are faster. + +## System dependencies + +The following system packages are required when `servo-embed` is enabled: + +- `libgles2-mesa-dev` or equivalent OpenGL ES headers +- `libssl-dev` +- `libdbus-1-dev` +- `libudev-dev` +- `libxkbcommon-dev` +- `libwayland-dev` + +On Fedora/RHEL: `mesa-libGL-devel openssl-devel dbus-devel systemd-devel libxkbcommon-devel wayland-devel` + +## Rendering approach + +Initial bringup uses `SoftwareRenderingContext` (CPU rasterisation) blitted to a +`softbuffer`-backed winit window. Production rendering will move to an EGL/GL +context once the Wayland surface pipeline is stable. + +## Known gaps at this pin + +- **GAP-1**: Wayland input events not forwarded to Servo (keyboard/pointer stubs) +- **GAP-2**: DMA-BUF surface export not implemented (software blit only) +- **GAP-3**: WebGPU adapter on Mesa may fail CTS +- **GAP-4**: CSS `backdrop-filter` and CSS Grid have partial coverage + +## Update policy + +Pin is reviewed monthly. To update: + +1. Check the [Servo release page](https://github.com/servo/servo/releases) for new tags. +2. Update `tag` in `Cargo.toml` and run `cargo update -p servo`. +3. Confirm the compositor and shell tests still pass. +4. Update this file with the new tag and any new or resolved gaps. diff --git a/crates/weft-servo-shell/src/embedder.rs b/crates/weft-servo-shell/src/embedder.rs new file mode 100644 index 0000000..f4bfbe3 --- /dev/null +++ b/crates/weft-servo-shell/src/embedder.rs @@ -0,0 +1,227 @@ +#![cfg(feature = "servo-embed")] + +use std::path::Path; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +use servo::{ + EventLoopWaker, ServoBuilder, ServoDelegate, ServoUrl, WebViewBuilder, WebViewDelegate, +}; +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, + window::{Window, WindowAttributes, WindowId}, +}; + +// ── Event loop waker ────────────────────────────────────────────────────────── + +#[derive(Clone)] +struct WeftEventLoopWaker { + proxy: Arc>>, +} + +#[derive(Debug, Clone)] +struct ServoWake; + +impl EventLoopWaker for WeftEventLoopWaker { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn wake(&self) { + let _ = self + .proxy + .lock() + .unwrap_or_else(|p| p.into_inner()) + .send_event(ServoWake); + } +} + +// ── Servo delegate ──────────────────────────────────────────────────────────── + +struct WeftServoDelegate; + +impl ServoDelegate for WeftServoDelegate { + fn notify_error(&self, error: servo::ServoError) { + tracing::error!(?error, "Servo error"); + } +} + +// ── WebView delegate ────────────────────────────────────────────────────────── + +struct WeftWebViewDelegate { + redraw_requested: Arc, +} + +impl WebViewDelegate for WeftWebViewDelegate { + fn notify_new_frame_ready(&self, _webview: servo::WebView) { + self.redraw_requested + .store(true, std::sync::atomic::Ordering::Relaxed); + } +} + +// ── Application state ───────────────────────────────────────────────────────── + +struct App { + url: ServoUrl, + window: Option>, + servo: Option, + webview: Option, + redraw_requested: Arc, + waker: WeftEventLoopWaker, + shutting_down: bool, +} + +impl App { + fn new(url: ServoUrl, waker: WeftEventLoopWaker) -> Self { + Self { + url, + window: None, + servo: None, + webview: None, + redraw_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)), + waker, + shutting_down: false, + } + } + + fn blit_to_window(window: &Arc, rendering_context: &servo::SoftwareRenderingContext) { + let size = window.inner_size(); + let Some(pixels) = rendering_context.read_pixels() else { + return; + }; + let ctx = softbuffer::Context::new(Arc::clone(window)).expect("softbuffer context"); + let mut surface = + softbuffer::Surface::new(&ctx, Arc::clone(window)).expect("softbuffer surface"); + let _ = surface.resize( + std::num::NonZeroU32::new(size.width).unwrap_or(std::num::NonZeroU32::new(1).unwrap()), + std::num::NonZeroU32::new(size.height).unwrap_or(std::num::NonZeroU32::new(1).unwrap()), + ); + let Ok(mut buf) = surface.buffer_mut() else { + return; + }; + for (dst, src) in buf.iter_mut().zip(pixels.chunks(4)) { + *dst = u32::from_be_bytes([0, src[0], src[1], src[2]]); + } + let _ = buf.present(); + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_some() { + return; + } + + let attrs = WindowAttributes::default().with_title("WEFT Shell"); + let window = Arc::new( + event_loop + .create_window(attrs) + .expect("failed to create shell window"), + ); + let size = window.inner_size(); + self.window = Some(Arc::clone(&window)); + + let servo = ServoBuilder::default() + .event_loop_waker(Box::new(self.waker.clone())) + .build(); + + servo.set_delegate(Rc::new(WeftServoDelegate)); + + let rendering_context = Rc::new( + servo::SoftwareRenderingContext::new(servo::euclid::Size2D::new( + size.width, + size.height, + )) + .expect("SoftwareRenderingContext"), + ); + + let webview = WebViewBuilder::new(&servo, Rc::clone(&rendering_context)) + .delegate(Rc::new(WeftWebViewDelegate { + redraw_requested: Arc::clone(&self.redraw_requested), + })) + .url(self.url.clone()) + .build(); + + self.servo = Some(servo); + self.webview = Some(webview); + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ServoWake) { + if let Some(servo) = &self.servo { + servo.spin_event_loop(); + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.shutting_down { + event_loop.exit(); + return; + } + if let Some(servo) = &self.servo { + servo.spin_event_loop(); + } + if self + .redraw_requested + .swap(false, std::sync::atomic::Ordering::Relaxed) + { + if let Some(w) = &self.window { + w.request_redraw(); + } + } + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::RedrawRequested => { + if let (Some(window), Some(servo)) = (&self.window, &self.servo) { + if let Some(wv) = &self.webview { + let rc = wv.rendering_context(); + Self::blit_to_window(window, rc); + } + servo.spin_event_loop(); + } + } + WindowEvent::Resized(new_size) => { + if let Some(wv) = &self.webview { + wv.resize(servo::euclid::Size2D::new(new_size.width, new_size.height)); + } + } + WindowEvent::CloseRequested => { + self.shutting_down = true; + if let Some(servo) = &self.servo { + servo.start_shutting_down(); + } + event_loop.exit(); + } + _ => {} + } + } +} + +// ── Public entry point ──────────────────────────────────────────────────────── + +pub fn run(html_path: &Path, _ws_port: u16) -> anyhow::Result<()> { + let url_str = format!("file://{}", html_path.display()); + let url = + ServoUrl::parse(&url_str).map_err(|e| anyhow::anyhow!("invalid URL {url_str}: {e}"))?; + + let event_loop = EventLoop::::with_user_event() + .build() + .map_err(|e| anyhow::anyhow!("event loop: {e}"))?; + + let waker = WeftEventLoopWaker { + proxy: Arc::new(Mutex::new(event_loop.create_proxy())), + }; + + let mut app = App::new(url, waker); + event_loop + .run_app(&mut app) + .map_err(|e| anyhow::anyhow!("event loop run: {e}")) +} diff --git a/crates/weft-servo-shell/src/main.rs b/crates/weft-servo-shell/src/main.rs index 45b2e5f..ec576e9 100644 --- a/crates/weft-servo-shell/src/main.rs +++ b/crates/weft-servo-shell/src/main.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use anyhow::Context; +#[cfg(feature = "servo-embed")] +mod embedder; mod protocols; fn main() -> anyhow::Result<()> { @@ -66,11 +68,19 @@ fn appd_ws_port() -> u16 { fn embed_servo( _wayland_display: &str, - _html_path: &std::path::Path, - _ws_port: u16, + html_path: &std::path::Path, + ws_port: u16, ) -> anyhow::Result<()> { - anyhow::bail!( - "Servo embedding not yet implemented; \ - see docs/architecture/winit-wayland-audit.md for gap assessment" - ) + #[cfg(feature = "servo-embed")] + return embedder::run(html_path, ws_port); + + #[cfg(not(feature = "servo-embed"))] + { + tracing::warn!( + path = %html_path.display(), + ws_port, + "servo-embed feature not enabled; build with --features servo-embed to activate" + ); + anyhow::bail!("servo-embed feature required") + } }