T10.1: with the vendored `HardwareOffscreenContext` (048c5df) and the
host-level kind dispatch (a7d3e89) in place, the sidecar binary
still ignored the rendering context kind — every spawn was wired to
the software path regardless of how the host process was built. This
commit threads the choice end-to-end:
* `ely_servo_sidecar` learns a `--rendering-context [software|
hardware]` flag on its `live` subcommand. `LiveArgs` carries
the parsed `RenderingContextKind` (defaulting to `Software` so
existing invocations stay bit-identical) and `live.rs::run_live`
routes it through to `SoftwareServoHost::new_with_config_dir_and_kind`.
Unknown values produce a typed
`SidecarArgsError::InvalidRenderingContext`; a missing value
after the flag produces the existing `MissingArgumentValue`.
* `ely_app` reads `ELY_SERVO_RENDERING_CONTEXT` (with values
`software` / `hardware`, case-insensitive) and, if set, appends
`--rendering-context VALUE` to the sidecar command line.
Unset or unrecognised values fall through to the sidecar's own
software default — a stale env var or a typo never breaks the
browser startup. The sidecar arg parser is the source of truth
for legality of explicit values; the env helper only gates
which values reach it.
* Five new unit tests in `args::tests` pin the new parse paths:
default-is-software, explicit-software, explicit-hardware,
bogus-value-rejected, missing-value-rejected. Run via
`cargo test -p ely_servo_host --features servo-engine --bin
ely_servo_sidecar` and now hit alongside the five existing
snapshot tests for 10 passes.
End-to-end perf expectation: with the sidecar binary built using
`--features servo-engine,hardware-render` and the env var set to
`hardware`, every spawned sidecar webview rasterises through the
real GPU adapter (via the vendored
`HardwareOffscreenContext`/surfman/CGL chain on macOS). The host
still reads back RGBA into a `Vec<u8>` for the existing pipe
protocol; the IOSurface zero-copy bridge that deletes that
read-back is T10.2–T10.5 in docs/t10-iosurface-plan.md and lands
in subsequent commits.
cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.
cargo test -p ely_servo_host --features servo-engine --bin ely_servo_sidecar: 10 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.
`LiveOutcome::frame` was carrying the rendered bytes as a fresh
`Vec<u8>` cloned out of `RenderedFrame::rgba_bytes()`. At 60 fps
on a 1080p canvas that was an extra 8 MB allocation + memcpy per
frame on top of the clone `host.last_rendered_frame()` already
paid for. `LiveOutcome` now carries the owned `RenderedFrame`
directly, and `write_outcome` writes its `rgba_bytes()` slice
straight to stdout — same single-clone cost as the host already
incurred, no second allocation.
Small Karpathy-style follow-up from the T8 review ("the host
still copies its rendered buffer to a transient Vec<u8> before
write_all; expose &[u8] straight to write_all once the profile
shows that allocation in the top five"). Profile data is still
deferred (T9 was marked vibe-benchmarking until T10 lands a real
spec), but removing the cheap clone is structurally cleaner and
makes the dispatch path one allocation lighter regardless.
cargo test --bin ely_app: 118 passed, 0 failed, 1 ignored.
cargo test -p ely_servo_host --features servo-engine --test sidecar: 9 passed.
Every live frame was round-tripping through the local file system:
the sidecar called `fs::write(rgba_out, frame.rgba_bytes())` in
`poll_frame`, the JSON response carried `rgba_path`, and the main
process turned around and called `fs::read(rgba_path)` to lift the
bytes back into a `Vec<u8>`. At 1080p that is 8 MB of syscall +
memcpy + page cache traffic per frame; at 60 fps it dwarfs every
other cost in the pipeline and shows up as scroll/zoom jank the user
can feel before any other bottleneck.
Replace it with a same-pipe binary protocol. The sidecar writes the
JSON `LiveResponse` line as before, then writes the raw RGBA frame
bytes on the same stdout immediately after the trailing `\n`. The
client `read_line`s the JSON, parses `rgba_byte_count` from the
header, and `read_exact`s exactly that many bytes from the same
`BufReader<ChildStdout>` (the buffered reader drains its own buffer
before pulling from the child). No tmpfs directory, no
`fs::remove_dir_all` on drop, no `rgba_path` field, no per-frame
filename plumbing.
Boundary defence on the client side: the header's `rgba_byte_count`
is cross-checked against `width * height * 4` before any allocation
or `read_exact`. A buggy or compromised sidecar can no longer ask
the GPUI process to allocate an arbitrarily large buffer or park on
`read_exact` for a payload that will never arrive.
The sidecar process boundary stays exactly where it was; only the
pixel transport between the two processes changes. The `one-shot`
sidecar binary path (used by `tests/sidecar.rs` and PRD smoke tests)
still writes to its CLI-supplied `--rgba-out` path — those tests
were untouched and continue to pass 9/9.
cargo test --bin ely_app: 112 passed.
cargo test -p ely_servo_host --features servo-engine --test sidecar: 9 passed.
Follow-ups deferred to the profile step (T9):
* the host still copies its rendered buffer to a transient
`Vec<u8>` via `frame.rgba_bytes().to_vec()` before write_all;
expose `&[u8]` straight to `write_all` once the profile shows
that allocation in the top five.
* `poll_frame` calls `snapshot` twice per iteration; harmless
under the software renderer but worth folding into a single
snapshot once we have numbers.
* raw memcpy bandwidth is still ~480 MB/s at 60 fps 1080p; the
GPU-side fix (T10: OffscreenRenderingContext + IOSurface
zero-copy) is the next material change.
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.
The live-mode `poll_frame` budgeted 60 ms for Servo to paint after
each `Ensure` request. That value was set when the only thing being
applied was navigation, where the GPUI tick timer (16 ms cadence) would
backfill missed frames quickly. Once we started forwarding clicks and
typing through the same `Ensure`, 60 ms was tighter than the
software renderer needs to handle MouseDown + MouseUp + layout +
paint on a real page like google.com — so the response carried the
pre-click frame and the user saw no visible reaction.
Bump the budget to 250 ms. That covers the click → focus-ring paint
cycle on the software backend without making nav slower (`apply_layout`
exits early on stable size, so unchanged ensures still return on the
first poll).
Without continuous MouseMove events, Servo never updates hover state:
no cursor changes over links, no :hover CSS effects, no mouseenter
JavaScript events. The web page appears completely non-interactive.
Track mouse position from GPUI on_mouse_move through the full IPC
pipeline to Servo. Hover position is included with each ensure
request so Servo updates hover state at frame rate (~60fps).