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)]:
This commit is contained in:
Marco Allegretti 2026-03-11 14:52:13 +01:00
parent 6b428e5a47
commit 2a9f034815
4 changed files with 330 additions and 6 deletions

View file

@ -8,6 +8,12 @@ rust-version.workspace = true
name = "weft-servo-shell" name = "weft-servo-shell"
path = "src/main.rs" 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] [dependencies]
anyhow = "1.0" anyhow = "1.0"
tracing = "0.1" tracing = "0.1"

View file

@ -0,0 +1,81 @@
# Servo Pin
## Current pin
| Field | Value |
|---------|--------------------------------------------------------------------------|
| Source | <https://github.com/servo/servo> |
| 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
3060 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.

View file

@ -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<Mutex<EventLoopProxy<ServoWake>>>,
}
#[derive(Debug, Clone)]
struct ServoWake;
impl EventLoopWaker for WeftEventLoopWaker {
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
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<std::sync::atomic::AtomicBool>,
}
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<Arc<Window>>,
servo: Option<servo::Servo>,
webview: Option<servo::WebView>,
redraw_requested: Arc<std::sync::atomic::AtomicBool>,
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<Window>, 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<ServoWake> 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::<ServoWake>::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}"))
}

View file

@ -2,6 +2,8 @@ use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
#[cfg(feature = "servo-embed")]
mod embedder;
mod protocols; mod protocols;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -66,11 +68,19 @@ fn appd_ws_port() -> u16 {
fn embed_servo( fn embed_servo(
_wayland_display: &str, _wayland_display: &str,
_html_path: &std::path::Path, html_path: &std::path::Path,
_ws_port: u16, ws_port: u16,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
anyhow::bail!( #[cfg(feature = "servo-embed")]
"Servo embedding not yet implemented; \ return embedder::run(html_path, ws_port);
see docs/architecture/winit-wayland-audit.md for gap assessment"
) #[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")
}
} }