Commit Graph
53 Commits
Author SHA1 Message Date
ZacharyZhang-NY 6f279bf3e6 T17: pixel-content smoke tests — red/blue data URLs render their actual colour through Servo 2026-05-11 01:03:52 -04:00
ZacharyZhang-NY 42b4e8c87b T15: paint barrier — pump event loop until framebuffer is consistent
`webview.paint()` dispatches a render command to Servo's paint thread
asynchronously, so the subsequent `read_to_image()` raced the paint
thread and reliably returned cleared-white pixels on data: URLs (T10.8).
Clear `has_pending_frame` before dispatching paint, then spin the Servo
event loop until `notify_new_frame_ready` re-arms it or 32 ms elapse
(override via `ELY_PAINT_BARRIER_MS`). Bench: software path now ships
real RGBA bytes for 60 frames instead of all-white.
2026-05-11 00:57:35 -04:00
ZacharyZhang-NY b70fa71a9f T13: push display scale factor into Servo's hidpi so Retina pages lay out at logical CSS dimensions 2026-05-11 00:46:32 -04:00
ZacharyZhang-NY 60a3b9b050 T12: disarm visible-content gate on software path after first real frame 2026-05-11 00:35:11 -04:00
ZacharyZhang-NY d570b84c9c T10.7: bypass visible-content gate on hardware path (196ms → 6ms per frame) 2026-05-11 00:05:04 -04:00
ZacharyZhang-NY 6e2bd20a9b T10.6: drop RGBA payload from the wire when the hardware path publishes a surface 2026-05-10 23:52:12 -04:00
ZacharyZhang-NY 07b9c9da01 T10.3: publish IOSurfaceHandle once per surface, current_surface_id per frame 2026-05-10 23:17:05 -04:00
ZacharyZhang-NY bb0bd0032f T10.2: extract IOSurface mach port from the hardware surface on macOS 2026-05-10 23:01:34 -04:00
ZacharyZhang-NY 1e38ace997 Add manual live_perf_bench driver for software vs hardware comparison 2026-05-10 22:56:06 -04:00
ZacharyZhang-NY 71ddadb482 Wire HardwareOffscreenContext::connection() so Servo's painter constructs 2026-05-10 22:56:03 -04:00
ZacharyZhang-NY 320da3ddb5 Make frame_perf total a real end-to-end timing and right-size buckets 2026-05-10 22:38:12 -04:00
ZacharyZhang-NY f7027e6ea5 Profile frame stages via sidecar histogram → ely::servo::perf 2026-05-10 22:30:57 -04:00
ZacharyZhang-NY 18fd20b577 Plumb the rendering context kind from ely_app env var to the sidecar
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.
2026-05-10 21:27:12 -04:00
ZacharyZhang-NY a7d3e896bb Wire the vendored hardware context into SoftwareServoHost
`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.
2026-05-10 20:55:35 -04:00
ZacharyZhang-NY 048c5dfecd Vendor a headless hardware RenderingContext for Servo
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).
2026-05-10 20:50:59 -04:00
ZacharyZhang-NY e02c0fd502 Drop the per-frame to_vec() clone in the sidecar pixel writer
`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.
2026-05-10 19:54:14 -04:00
ZacharyZhang-NY a80d0393e9 Drop the file system from the live frame pixel pipe
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.
2026-05-10 19:04:34 -04:00
ZacharyZhang-NY 6314e1f777 Make WebView input invariant a property of the dispatch path
Servo's hit-test silently absorbs notify_input_event on a hidden or
unfocused WebView. Today show()+focus() are called at creation and on
the first-navigate rebuild, but every later sibling-WebView creation
also calls focus() — silently stealing focus from the foreground tab.
load() (later navigates), resize, set_page_zoom, and paint never
re-focus, so a click on the visible tab can land on an unreachable
WebView and disappear.

Move the invariant from a state spread across creation/navigation/
tab-switching into a property of the dispatch path itself: a private
webview_for_input(id) helper re-asserts show()+focus() and returns
the WebView; click/hover/drag/touch_tap/scroll/type_text all go
through it. The cosmetic show/focus calls in create_webview_in_context
and the navigate-rebuild branch stay (first-frame paint), now annotated
to point to webview_for_input as the input-path owner.

Not a complete fix on its own. focus() goes through constellation_proxy
asynchronously (servo crate webview.rs:352), so debug_assert!(focused())
right after focus() would race the constellation — doc comment is the
only guard. Sidecar integration tests pass 9/9 but only exercise
single-WebView sessions; multi-tab focus stealing on Google / YouTube /
Twitter still needs human verification in the live shell. Linus's
critique of the GPUI-side data structure (PerTabSurface Option-as-queue
+ Ensure bundling config+input+frame) is a separate layer untouched
by this change.

Pre-existing tests/software_host.rs::manages_real_servo_webview_lifecycle
already times out on https://servo.org/ with state=Complete but
has_pending_frame=false on b8795bf without this change, so unrelated.
2026-05-10 13:59:04 -04:00
ZacharyZhang-NY 22ba8517d1 Show + focus the Servo WebView so it stops dropping every input event
Root cause: Servo's WebView is hidden+unfocused by default. notify_input_event
on a hidden WebView runs paint() hit-test, which returns no hit, and Servo
silently absorbs the event as "already handled". Eleven prior commits all
patched the GPUI side of input forwarding while every event landed in
exactly that black hole.

Fix: webview.show() + webview.focus() immediately after WebViewBuilder::build
in create_webview_in_context, and again in navigate() only on the
should_create_initial_document branch (the load() branch keeps existing
visibility+focus, otherwise a background tab finishing navigation would
steal focus from the foreground tab — Linus correctness ask).

Also unblocks the sidecar binary build, which had been frozen at the
May 9 13:53 stale binary because servo-engine feature was broken on two
fronts:

- ServoHost trait was missing the hover() method that the SoftwareServoHost
  impl declared (regression from "Plug three holes" commit eb58ce7).
- Servo SDK renamed Key::Enter / Backspace / Tab / Escape / Delete /
  Arrow{Up,Down,Left,Right} / Home / End / Page{Up,Down} to
  Key::Named(NamedKey::*). keyboard.rs updated to match.

Sidecar binary rebuilt: 335 MB at May 10 01:44. The earlier 11 commits
were never reaching users because they couldn't recompile the sidecar
without these two upstream-API repairs.
2026-05-10 01:46:39 -04:00
ZacharyZhang-NY eb58ce75da Plug three holes in Servo input forwarding
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.
2026-05-10 00:51:37 -04:00
ZacharyZhang-NY 4d82023a68 Servo sidecar: extend post-input frame wait to 250 ms
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).
2026-05-10 00:21:33 -04:00
ZacharyZhang-NY 3caf207129 update 2026-05-09 16:27:40 -04:00
ZacharyZhang-NY 4c6d9c757a Add mouse hover tracking across the Servo input pipeline
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).
2026-05-09 16:21:43 -04:00
ZacharyZhang-NY f3a8671bd5 Add keyboard special key support for web content interaction
Enter, Backspace, Tab, Escape, Delete, arrow keys, Home, End,
PageUp, PageDown now forward to Servo. Previously only printable
characters reached the web engine, making forms unusable.
2026-05-09 15:36:37 -04:00
ZacharyZhang-NY 1faa7423c1 Add Servo page zoom support 2026-05-09 12:32:44 -04:00
ZacharyZhang-NY 8b9c4579c1 Add active profile local data export 2026-05-09 08:23:37 -04:00
ZacharyZhang-NY 928bb3cfb0 Log PRD site rendering evidence 2026-05-09 03:23:39 -04:00
ZacharyZhang-NY 89a8e05eeb Harden PRD live site rendering checks 2026-05-09 01:03:19 -04:00
ZacharyZhang-NY 79d9646ab8 Pass site permission grants to sidecar snapshots 2026-05-09 00:01:14 -04:00
ZacharyZhang-NY 4ed1666ece Scope Servo permissions by profile origin 2026-05-08 23:47:05 -04:00
ZacharyZhang-NY 8334fcbd6e Wire sidecar profile data isolation 2026-05-08 22:10:10 -04:00
ZacharyZhang-NY 527762fd36 Harden PRD live site smoke coverage 2026-05-08 21:13:34 -04:00
ZacharyZhang-NY a299815e51 Stabilize live site smoke output 2026-05-08 20:05:24 -04:00
ZacharyZhang-NY a5907efeb0 Verify PRD sites through app sidecar 2026-05-08 19:43:54 -04:00
ZacharyZhang-NY e80e52bc7e Harden PRD site smoke retries 2026-05-08 19:23:38 -04:00
ZacharyZhang-NY 59b90c60ae Ensure PRD sites render through app sidecar 2026-05-08 18:31:52 -04:00
ZacharyZhang-NY e8a33d53f5 Stabilize PRD site sidecar snapshots 2026-05-08 17:54:32 -04:00
ZacharyZhang-NY cb115b61fe Bridge Servo screenshot capture 2026-05-08 16:06:26 -04:00
ZacharyZhang-NY 74c1e9ad4b Add PRD reference site smoke coverage 2026-05-08 15:48:09 -04:00
ZacharyZhang-NY 660d9f54e2 Bridge Servo webview resize 2026-05-08 15:22:05 -04:00
ZacharyZhang-NY 0ea43e8b81 Bridge Servo mouse drag input 2026-05-08 15:15:38 -04:00
ZacharyZhang-NY 7a09967d19 Bridge Servo touch tap input 2026-05-08 15:00:06 -04:00
ZacharyZhang-NY 292824dc10 Bridge Servo keyboard text input 2026-05-08 14:36:48 -04:00
ZacharyZhang-NY b786073826 Bridge Servo click input 2026-05-08 14:23:12 -04:00
ZacharyZhang-NY 7178174e20 Add Servo scroll snapshots 2026-05-08 13:54:56 -04:00
ZacharyZhang-NY 8e12d6690b Render PRD sites in real viewport 2026-05-08 13:20:43 -04:00
ZacharyZhang-NY 2fb6b15e2d Render Servo sidecar frames in shell 2026-05-08 12:43:38 -04:00
ZacharyZhang-NY fcdc2de076 Add Servo sidecar snapshot command 2026-05-08 12:17:41 -04:00
ZacharyZhang-NY 5f0773a043 Add Servo site compatibility frame smoke 2026-05-08 12:08:57 -04:00
ZacharyZhang-NY f10a2349f6 Avoid headless Servo paint noise 2026-05-08 00:15:29 -04:00