feat(servo-shell): dispatch shell client event queue in servo event loop

Wire ShellClient into App so its Wayland event queue is dispatched each
frame via about_to_wait. This ensures configure, focus_changed, and
window_closed events from the compositor are processed. window_closed
now triggers a clean Servo shutdown.

The EGL rendering path (WindowRenderingContext + surfman eglSwapBuffers)
produces frames transparently via Mesa DMA-BUF buffer sharing; no
explicit zwp_linux_dmabuf_v1 code is required in the shell.

Remaining: ZweftShellWindowV1 is created with surface=null; sharing the
winit wl_surface with the shell client connection is not currently
feasible without refactoring to a single shared Wayland connection.
This commit is contained in:
Marco Allegretti 2026-03-11 20:46:53 +01:00
parent c244a58844
commit 3ee2f283d8
3 changed files with 43 additions and 13 deletions

View file

@ -64,19 +64,28 @@ On Fedora/RHEL: `mesa-libGL-devel openssl-devel dbus-devel systemd-devel libxkbc
Default: `SoftwareRenderingContext` (CPU rasterisation) blitted to a
`softbuffer`-backed winit window.
EGL path (scaffolded): set `WEFT_EGL_RENDERING=1` at runtime. The embedder
attempts `WindowRenderingContext::new` using the winit display and window
handles. If construction fails it falls back to software automatically.
When the EGL path is active Servo presents directly to the EGL surface;
the softbuffer blit is skipped. Full DMA-BUF export to the Wayland
compositor is not yet wired (`RenderingCtx::Egl` blit body is a no-op).
EGL path: set `WEFT_EGL_RENDERING=1` at runtime. The embedder attempts
`WindowRenderingContext::new` using the winit display and window handles.
If construction fails it falls back to software automatically.
When the EGL path is active Servo presents directly to the EGL surface via
surfman's `eglSwapBuffers`; the softbuffer blit is skipped. Mesa handles
DMA-BUF buffer sharing with the compositor transparently.
## Known gaps at this pin
- **GAP-1**: ~~Wayland input events not forwarded to Servo~~ **Resolved** — keyboard and
mouse events forwarded via `webview.notify_input_event`; key mapping in `keyutils.rs`.
- **GAP-2**: EGL `WindowRenderingContext` path scaffolded (`WEFT_EGL_RENDERING=1`);
DMA-BUF export to the Wayland compositor (linux-dmabuf-unstable-v1) not yet wired.
- **GAP-2**: EGL `WindowRenderingContext` path scaffolded (`WEFT_EGL_RENDERING=1`).
When EGL is active, Servo presents frames via surfman's `eglSwapBuffers`; Mesa handles
DMA-BUF buffer sharing with the compositor transparently — no explicit
`zwp_linux_dmabuf_v1` code is needed in the shell for basic rendering.
The `zweft_shell_manager_v1` event queue is now dispatched each frame so `configure`,
`focus_changed`, and `window_closed` events are processed; `window_closed` triggers a
clean Servo shutdown.
Remaining gap: the `ZweftShellWindowV1` is created with `surface = null`; the winit
`wl_surface` is not yet associated with the shell window slot (requires sharing a single
Wayland connection between winit and the shell client, which is not currently feasible
without significant refactoring).
- **GAP-3**: WebGPU adapter on Mesa may fail CTS — validation task, requires Mesa GPU hardware.
- **GAP-4**: ~~CSS Grid~~ **Grid resolved** (Taffy-backed, fully wired).
~~CSS `backdrop-filter` unimplemented~~ **`backdrop-filter` resolved** (servo/servo issue

View file

@ -99,6 +99,7 @@ struct App {
shutting_down: bool,
modifiers: ModifiersState,
cursor_pos: servo::euclid::default::Point2D<f32>,
shell_client: Option<crate::shell_client::ShellClient>,
}
impl App {
@ -107,6 +108,7 @@ impl App {
waker: WeftEventLoopWaker,
ws_port: u16,
app_rx: mpsc::Receiver<crate::appd_ws::AppdCmd>,
shell_client: Option<crate::shell_client::ShellClient>,
) -> Self {
Self {
url,
@ -123,6 +125,7 @@ impl App {
shutting_down: false,
modifiers: ModifiersState::default(),
cursor_pos: servo::euclid::default::Point2D::zero(),
shell_client,
}
}
@ -246,6 +249,18 @@ impl ApplicationHandler<ServoWake> for App {
event_loop.exit();
return;
}
if let Some(sc) = &mut self.shell_client {
match sc.dispatch_pending() {
Ok(false) => {
self.shutting_down = true;
if let Some(servo) = &self.servo {
servo.start_shutting_down();
}
}
Err(e) => tracing::warn!("shell client dispatch error: {e}"),
Ok(true) => {}
}
}
while let Ok(cmd) = self.app_rx.try_recv() {
match cmd {
crate::appd_ws::AppdCmd::Launch { session_id, app_id } => {
@ -418,7 +433,11 @@ fn app_store_roots() -> Vec<PathBuf> {
roots
}
pub fn run(html_path: &Path, ws_port: u16) -> anyhow::Result<()> {
pub fn run(
html_path: &Path,
ws_port: u16,
shell_client: Option<crate::shell_client::ShellClient>,
) -> anyhow::Result<()> {
let url_str = format!("file://{}", html_path.display());
let raw_url =
ServoUrl::parse(&url_str).map_err(|e| anyhow::anyhow!("invalid URL {url_str}: {e}"))?;
@ -436,7 +455,7 @@ pub fn run(html_path: &Path, ws_port: u16) -> anyhow::Result<()> {
let waker_for_thread = waker.clone();
crate::appd_ws::spawn_appd_listener(ws_port, app_tx, Box::new(move || waker_for_thread.wake()));
let mut app = App::new(url, waker, ws_port, app_rx);
let mut app = App::new(url, waker, ws_port, app_rx, shell_client);
event_loop
.run_app(&mut app)
.map_err(|e| anyhow::anyhow!("event loop run: {e}"))

View file

@ -34,7 +34,7 @@ fn run() -> anyhow::Result<()> {
let ws_port = appd_ws_port();
tracing::info!(ws_port, "appd WebSocket port");
let _shell = match shell_client::ShellClient::connect() {
let shell = match shell_client::ShellClient::connect() {
Ok(c) => {
tracing::info!("shell window registered with compositor");
Some(c)
@ -45,7 +45,7 @@ fn run() -> anyhow::Result<()> {
}
};
embed_servo(&wayland_display, &html_path, ws_port)
embed_servo(&wayland_display, &html_path, ws_port, shell)
}
fn system_ui_html_path() -> anyhow::Result<PathBuf> {
@ -86,12 +86,14 @@ fn embed_servo(
_wayland_display: &str,
html_path: &std::path::Path,
ws_port: u16,
shell_client: Option<shell_client::ShellClient>,
) -> anyhow::Result<()> {
#[cfg(feature = "servo-embed")]
return embedder::run(html_path, ws_port);
return embedder::run(html_path, ws_port, shell_client);
#[cfg(not(feature = "servo-embed"))]
{
let _ = shell_client;
tracing::warn!(
path = %html_path.display(),
ws_port,