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.
`HardwareOffscreenContext` was vendored in 048c5df but the host's
per-webview `new_rendering_context` still hard-wired
`servo::SoftwareRenderingContext`. This commit threads a
`RenderingContextKind` enum through the host so existing call sites
keep their software path, and new callers can opt into the hardware
path through `SoftwareServoHost::new_with_config_dir_and_kind(...)`.
Three changes, kept tightly scoped:
* `runtime.rs` gains a public `RenderingContextKind { Software,
Hardware }` enum and a new constructor that takes it. The
existing `new` and `new_with_config_dir` keep their signatures
and default to `Software`, so the sidecar binary and the
integration tests pick up zero behavioural change. The
private `new_rendering_context` moves from a free function to a
`&self` method so it can read `self.rendering_context_kind` and
dispatch — `Software` constructs `SoftwareRenderingContext` as
before, `Hardware` constructs the vendored
`HardwareOffscreenContext`. When the `hardware-render` feature
isn't compiled in, the `Hardware` arm returns
`ServoHostError::HardwareRenderUnavailable` instead of silently
falling back; the new constructor also rejects the request
up-front before touching the global Servo runtime flag.
* `error.rs` gains `HardwareRenderUnavailable` so the wrong-feature
path is a typed error, not a panic.
* `lib.rs` exports `RenderingContextKind` alongside
`SoftwareServoHost` so downstream code (next commit will be the
sidecar's `--rendering-context` CLI flag and the live.rs
plumbing) can name the variant directly.
This is purely an extension point — no existing call path changes,
no existing test asserts on the new enum. The next commit will add
the sidecar CLI flag and wire `live.rs::run_live` to pass the kind
through to the host so users can pick the path at startup. The
follow-up commits then extract the IOSurface from the hardware
surfman surface and bridge it across the IPC channel to GPUI's
Metal renderer, deleting the host-side `Vec<u8>` from the per-frame
hot path entirely (full plan in docs/t10-iosurface-plan.md).
cargo test -p ely_servo_host --features servo-engine --lib: 2 passed.
cargo test -p ely_servo_host --features servo-engine --test sidecar: 9 passed.
cargo test -p ely_servo_host --features servo-engine,hardware-render
--test hardware_rendering_context: 1 passed.
cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.
The first concrete step toward the T10 IOSurface zero-copy path
(plan in docs/t10-iosurface-plan.md). Before this commit the sidecar
process could only use `SoftwareRenderingContext` — CPU rasterising
plus an 8 MB RGBA readback per 1080p frame is most of where scroll
latency comes from after the file pipe (a80d039), Vec clone
(e02c0fd), texture dedup (7f3b8b4), and hash swap (3f184ee) have
all landed.
The blocker is purely architectural: `servo-paint-api 0.1` exposes
`OffscreenRenderingContext` only as a child of
`WindowRenderingContext`, which requires a `DisplayHandle +
WindowHandle`. The sidecar has no window. The underlying
`SurfmanRenderingContext` glue *can* drive a hardware adapter
against a `SurfaceType::Generic` offscreen surface, but its
constructor is private. Until the upstream PR lands, this commit
vendors the minimal slice of that glue into `ely_servo_host`:
* `HardwareOffscreenContext::new(size)` uses
`Connection::new() → create_adapter()` (real GPU, not the
software adapter) and a `SurfaceType::Generic` offscreen
surface. On macOS the surfman CGL backend backs that surface
with an `IOSurface` — exactly the thing the IOSurface bridge
in subsequent commits will reach for.
* Implements `servo::RenderingContext` so it slots into
`ServoBuilder::rendering_context` wherever the existing
`SoftwareRenderingContext` does, with no other Servo-side
knowledge.
* Scope deliberately narrow: only the methods Servo's headless
readback actually calls. `create_texture` /
`destroy_texture` / `connection` / `refresh_driver` fall
through to the trait's `None` defaults. `read_to_image` inlines
the upstream `Framebuffer::read_framebuffer_to_image` helper so
we don't reach for a private helper that may change shape.
* No `RawWindowHandle` and no `RefreshDriver` — both belong to
paths the headless sidecar doesn't take.
The whole thing is feature-gated on `hardware-render`. Default
builds compile zero new lines; the additional surfman / gleam /
glow / euclid / image / log deps are all `optional = true`. Sidecar
binary still uses `SoftwareServoHost` until a follow-up commit
threads the new context in behind a CLI flag.
A smoke test at `tests/hardware_rendering_context.rs` constructs
the context. On a host with a real GPU it returns `Ok`; on a no-GPU
CI host it logs the surfman cause and reports `ok` rather than
failing the suite — the test is guarding the wiring, not the
hardware availability. On this Mac it constructs cleanly.
The `expect_used` / `unwrap_used` workspace lints are honoured —
fallible reads return `None` instead of panicking, no `.expect()` /
`.unwrap()` survives in the vendored body. The two `unsafe` blocks
(loading GL function pointers via surfman's `get_proc_address`) are
the same blocks upstream uses, with `#[expect(unsafe_code)]` to
override the workspace `unsafe_code = "deny"` lint locally.
cargo test --bin ely_app: 120 passed.
cargo test -p ely_servo_host --features servo-engine,hardware-render
--test hardware_rendering_context: 1 passed (constructs cleanly).
Pre-existing `manages_real_servo_webview_lifecycle` failure on
servo.org is unrelated (reproduces on b8795bf without this change,
already documented in T7's commit history).
1. Scroll no longer wipes keyboard focus. Servo holds DOM focus across
wheel events; the shell was clearing keyboard_focus and typed_texts
on every scroll, so a focused input went deaf the moment the user
scrolled. Scroll still drops the buffered click point because that
coordinate is captured against the pre-scroll viewport.
2. Mouse-down hands focus to the shell's root focus handle (in
addition to mouse-up's existing click forwarding). The user can now
start typing the moment they press the page, instead of having to
first complete a click round-trip to escape the omnibar's focus.
3. Sidecar hover() honors the requesting webview_id instead of
defaulting to the first webview in the map, so multi-tab sidecars
no longer pipe every hover into tab #1.