Like CSS Grid, Servo's `Preferences::default()` ships
`layout_variable_fonts_enabled: false`. `Servo::new` forwards it to Stylo
(`layout.variable_fonts.enabled`), and with the gate off Stylo ignores
`font-variation-settings` and variable weight/width axes: a variable font
renders only its default instance, so every requested weight looks
identical. Modern sites lean on variable fonts (Inter, Roboto Flex,
system New York/SF), so text rendered at the wrong weight versus Chrome.
servo-fonts already drives variations through HarfBuzz, so enabling the
pref is the real fix. Verified with a `@font-face` page using a variable
font at `font-variation-settings: "wght" 200` vs `"wght" 900`: identical
weight before, distinctly light vs black after.
runtime.rs 473 lines (<500). fmt/audit/clippy clean; software_host real-
Servo test passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`runtime.rs` had grown past the 500-line ceiling enforced by
`scripts/audit_source_lines.sh` once the grid-pref comment landed (522
lines). Peel the repaint/present pair — `paint_without_readback`,
`paint_without_readback_with_completion`, the private `paint_webview`,
and `paint_with_readback` — into a sibling `runtime_paint.rs`, exactly
the `paint.rs` boundary the embedding architecture doc prescribes.
`runtime_paint` is a child module of `runtime` (declared via `#[path]`,
mirroring `runtime_context`), so it keeps access to the private
`SoftwareServoHost` fields and the `webview()` / `wait_for_paint_completion`
/ `read_rendered_frame` helpers without widening any visibility. No
behaviour change: public API and call sites are identical.
runtime.rs 522 -> 469 lines. Build + clippy clean; full workspace test
suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Preferences::default()` is Servo's conservative library default and
ships `layout_grid_enabled: false`. `Servo::new` forwards prefs to Stylo
(`prefs::set` -> `stylo_static_prefs::set_pref!("layout.grid.enabled")`),
so with the gate off Stylo blockifies `display: grid`: every grid
container collapses to `display: block` and grid-based page layouts
stack into a single column — the "broken" rendering reported on modern
sites.
`ely_servo_preferences()` only flipped `dom_intersection_observer_enabled`
and inherited the grid default, so ELY rendered grid pages collapsed
while Servo's own servoshell (which enables the pref) renders them
correctly. The layout path is implemented — servo-layout drives
`DisplayInside::Grid` through Taffy — so enabling the pref is the real
fix, not a workaround.
Verified with a deterministic `display: grid; grid-template-columns:
1fr 1fr 1fr` page: 9 stacked full-width bars before, a 3x3 grid after.
Wikipedia/HN/GitHub/google.com re-checked unchanged; full workspace
test suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Servo's reference embedder (\`examples/winit_minimal.rs\`) handles
resize with a single call: \`webview.resize(new_size)\`. The Servo
paint pipeline behind that call:
1. early-returns if \`rendering_context.size() == new_size\`,
2. otherwise calls \`rendering_context.resize\` itself,
3. updates \`webview_renderer.rect\` so the compositor relays out
the page at the new device viewport,
4. sends \`transaction.set_document_view(...)\` so WebRender's
document viewport matches the surface,
5. flags the painter \`needs_repaint(RepaintReason::Resize)\`.
Our \`runtime.rs::resize\` called \`webview.rendering_context.resize\`
*before* \`webview.webview.resize\`. surfman accepted the new size, so
the painter saw \`rendering_context.size() == new_size\` and took the
early-return path — steps 3, 4, and 5 never ran. The compositor
kept the original (creation-time) viewport rect while we presented
a much larger surface. The page ended up laid out for a tiny
viewport and either rendered into the top-left of a sea of cleared
background (StableLance) or never painted any pipeline at all
(google.com appeared totally blank).
Drop the direct \`rendering_context.resize\` call and route through
\`WebView::resize\` exactly as the reference embedder does. The
debounce in \`record_viewport_size\` still collapses a sidebar /
window-edge animation into a single trailing-edge resize, so we
also avoid hammering Servo with per-frame surface mutations.
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.
`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.
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.