# Servo Embedding Architecture ## Decision ELY runs one Servo process for each browser profile. Servo's configuration and site-data manager are process-scoped, so the process boundary is the storage partition boundary for cookies, HTTP cache, local storage, and other profile data. ```text ELY App Process GPUI chrome WebSurfaceStore Profile Runtime Broker Profile A worker ── stdio IPC ──> Servo sidecar A ──> profiles/A/servo Profile B worker ── stdio IPC ──> Servo sidecar B ──> profiles/B/servo Private worker ── stdio IPC ──> Servo sidecar P ──> transient directory ``` Each sidecar binds to the first `ProfileId` it receives and rejects requests for another profile. The parent broker also keys workers by `ProfileId + ProfileDataMode` and discards responses whose scope no longer matches the tab session. ## Current Code Boundary ```text WebSurfaceStore -> WebSurfaceRuntime -> one LiveRuntimeWorker per profile scope -> ServoLiveClient -> ely_servo_sidecar live -> SoftwareServoHost -> servo::Servo + servo::WebView macOS -> HardwareOffscreenContext (CGL + surfman) -> IOSurface + Mach descriptor -> CVPixelBuffer -> GPUI surface element Linux / Windows -> SoftwareRenderingContext -> RGBA frame -> GPUI RenderImage ``` The app process owns browser state, profile routing, input coalescing, frame cadence, and GPUI presentation. A sidecar owns one Servo runtime, its WebViews, and the profile's persistent storage. The line-delimited JSON protocol carries commands and frame metadata. Software frames append an exact `width * height * 4` RGBA payload. macOS hardware frames set `rgba_byte_count` to zero and select an imported IOSurface through `surface_handle` and `current_surface_id`. The Mach channel transfers the IOSurface send right; the JSON port number is diagnostic metadata. The protocol supports: - `handshake`: verify protocol version `4` before accepting browser commands. - `ensure`: create or update a WebView, navigation, viewport, zoom, resolved color scheme, permissions, and input. - `poll`: advance Servo and return pending frame or metadata state. - `close`: destroy one tab's WebView. - `shutdown`: acknowledge graceful process shutdown so Servo flushes profile storage. Each `ensure` carries the complete permission snapshot for its Profile. The sidecar atomically replaces that Profile at the newest request generation, so cross-tab round-robin cannot restore an older grant. Entries carry per-key revisions plus explicit `transferred-allow-once` state and full-snapshot removal semantics. BrowserCore moves an accepted one-time grant into revocable transferred state, the worker preserves the grant request from idle coalescing, and the sidecar returns a consumption receipt after the first matching request. A fresh worker interprets transferred state as consumed and returns the same completion receipt, which keeps process-restart behavior fail closed. `ensure` and `poll` carry `ready_surface_ids` and `pending_surface_ids`. The app distinguishes completed imports from handles queued behind bounded importer backpressure. Cache entries retain the IOSurface reference and keep only a weak reference to an active `CVPixelBuffer` backing. The sidecar preserves pending publications, republishes a surface missing from both sets, and replays the cached surface report once after an asynchronous import completes. The release app locates an adjacent `ely_servo_sidecar` binary. `scripts/run_dev.sh` builds the locked sidecar before starting the app and points the runtime at that binary. The macOS bundle script places release executables in `Contents/MacOS`, installs `AppIcon.icns`, and verifies its ad-hoc code signature. The native distribution script places adjacent release executables in one directory for macOS, Linux, and Windows packaging inputs. ## Profile Data Contract Persistent scopes use: ```text /profiles//servo ``` The default Standard Profile stores its generated identity at `/profiles/default/profile-id`; later launches restore that identity before constructing `BrowserCore`, so the persistent Servo path remains stable. Transient scopes receive a `0700` directory under the user-specific ELY runtime directory on Linux and the user-specific ELY cache directory on macOS and Windows. The root rejects symbolic links. A root lock serializes creation and stale cleanup; each live directory holds an exclusive lease so concurrent ELY instances preserve one another's data. Closing the final tab retires the profile worker in the background, waits for sidecar shutdown, and then removes the directory. A new Private scope starts immediately in a fresh random directory while earlier cleanup completes. Startup removes unlocked crash remnants. A sidecar receives its directory at process launch and keeps it for its process lifetime. The integration gates prove these invariants with real Servo networking: 1. Cookie and local-storage values survive a graceful restart with the same directory. 2. A fresh directory starts with empty cookie and local-storage state. 3. Two simultaneous app-level profiles keep cookies, local storage, and HTTP cache isolated. 4. Reopening a tab in each profile observes only that profile's state and cached response. 5. Closing the final Private tab and reopening the same profile starts with empty site data. ## Upstream Servo Route ELY pins fork commit `8f6850ceb0bd92aea4127d8078ebb2c0485e7f39`, based on Servo upstream commit `a87625150758fe91a29e7f6553ff2f798b87027e`, workspace version `0.4.0`, in `Cargo.toml` and `Cargo.lock`. The fork moves macOS CoreText fallback ahead of Servo's static fallback-family list so the first CSS font preserves its serif or sans-serif cascade and language. Servo's rendering lifecycle remains authoritative inside each sidecar: ```text command or poll -> Servo spin_event_loop -> WebViewDelegate::notify_new_frame_ready -> WebView::paint -> RenderingContext frame -> sidecar response ``` Relevant upstream evidence at the pinned revision: - `ports/servoshell/window.rs` builds WebViews from a Servo instance and rendering context. - `ports/servoshell/window.rs` repaints with `webview.paint()` and presents the context. - `ports/servoshell/running_app_state.rs` maps `notify_new_frame_ready` to repaint scheduling. - `components/paint/paint.rs` owns one WebRender painter per rendering context. - `components/fonts/font.rs` asks CoreText for a style- and language-compatible macOS fallback before consulting the cross-platform fallback-family list. ## Ownership ```text crates/ely_browser_core Browser domain state: tabs, spaces, profiles, search, settings. crates/ely_app/src/shell GPUI chrome, profile broker, frame cadence, input, and presentation. crates/ely_app/src/services Sidecar discovery, process lifecycle, and wire client. crates/ely_servo_host Servo host API, rendering contexts, sidecar protocol, and process entry point. ``` Each production source file in the embedding path stays below 500 lines. Responsibilities remain split by lifecycle, protocol, session state, frame output, and browser-shell orchestration. ## Rendering Transports ### macOS hardware transport ```text Servo WebView::paint -> CGL HardwareOffscreenContext -> surfman swap chain -> IOSurface -> IOSurfaceCreateMachPort -> mach_msg port descriptor -> IOSurfaceLookupFromMachPort -> CVPixelBuffer -> GPUI surface element -> Metal BGRA texture ``` The parent creates a unique bootstrap Mach service before spawning the sidecar. The sidecar looks up that service and moves each new IOSurface send right in a complex Mach message. The app verifies the system IOSurface ID and dimensions, wraps the imported surface in a `CVPixelBuffer`, and releases the received Mach right. The cache retains up to 16 inactive IOSurfaces and temporarily retains additional surfaces whose GPU backing is still active. Each hardware frame carries `current_surface_id`. The first frame for an IOSurface also carries its dimensions and system surface ID in `surface_handle`. GPUI presents the selected `CVPixelBuffer` through its surface element and macOS Metal BGRA pipeline. This path keeps pixels on the GPU side of the profile process boundary. CoreVideo raises the IOSurface use count while a `CVPixelBuffer` backing is alive. The frame and GPUI `SurfaceLease` share that backing, and the Metal command-buffer completion handler releases the final GPU lease. The host retains presented surfman surfaces and returns an acknowledged, non-current surface to the swap chain only after `IOSurfaceIsInUse` becomes false. A sidecar waits for the app's import acknowledgement before painting the next hardware frame. The macOS parent selects hardware rendering by default. Setting `ELY_SERVO_RENDERING_CONTEXT=software` selects the RGBA path for software-specific diagnostics. ### Linux and Windows software transport Linux and Windows use Servo's `SoftwareRenderingContext`. The sidecar paints into RGBA8 memory, writes the JSON frame header, then writes the bounded raw payload on stdout. The app validates the dimensions and byte count before allocating and creates a GPUI `RenderImage`. ## Acceptance Gates - Workspace format, check, lint, and unit tests pass. - Servo host check and lint pass with `servo-engine,hardware-render` enabled. - The macOS hardware-context test presents a real IOSurface and exports a live Mach send right. - The hardware sidecar test transfers that right across processes and resolves it with `IOSurfaceLookupFromMachPort`. - The app hardware test imports a real BGRA `CVPixelBuffer` and verifies its GPUI lease releases the IOSurface use count. - Sidecar persistence integration passes against a loopback HTTP server. - PRD live-site tests pass through the GPUI web-surface adapter. - Profile Cookie, local-storage, and HTTP-cache isolation passes through two live sidecars. - The real-window render probe captures visible page content. - The macOS application bundle contains both executables in `Contents/MacOS`. - Native distributions place both executables at the distribution root. - Every production source file stays below 500 lines.