2026 04 30_renderer_and_host_refactor_plan - mark-ik/graphshell GitHub Wiki
Status: Active Scope: Five related directional decisions from the 2026-04-30 architectural review:
- Renderer policy: Vello for canvas surfaces; WebRender for web/document tiles; do not rely on Servo/WebRender stabilization for application fundamentals.
- Application fundamentals path: test Wry, refine the GUI bridge, use upstream Servo; lean on what we have that isn't Servo's WebRender for the next milestone.
- Middlenet refactor: middlenet was shaped as Blitz-top + webrender- wgpu; refactor toward Vello + Linebender component crates so it can render natively under the new policy.
-
EmbeddedHost audit: lean against keeping
EmbeddedHostas a render mode by default; audit for bits worth graverobbing into the new shape. - Decomposition target: lower the portable-crate file size threshold from 600 LOC to 500 LOC; decompose every portable-crate file exceeding it.
Related:
-
../shell/2026-04-28_iced_jump_ship_plan.md— §3.2 decomposition target list (threshold lowered by this plan) -
../shell/2026-04-24_iced_content_surface_scoping.md— middlenet-first content surface; refactor direction in §3 of this plan -
../shell/2026-04-24_blitz_shaped_chrome_scoping.md— long-horizon Blitz-shaped chrome; this plan partially supersedes its middlenet shape -
2026-03-23_webrender_wgpu_backend_implementation_plan.md— Netrender fork plan (long-horizon) -
2026-04-10_wgpu_gui_bridge_interop_architecture_plan.md— GUI bridge to refine -
ASPECT_RENDER.md— Render aspect authority -
../system/graphshell_net_spec.md— smolnet content fetching flows through this subsystem -
../../technical_architecture/2026-04-22_portable_shell_state_in_graphshell_core.md— portable-crate guarantees
Two primary renderers, two distinct domains:
Vello (Linebender's GPU vector renderer) is the canonical renderer for graph canvas instances, including:
- the main graph canvas
- the canvas base layer (when Frame is empty)
- canvas Panes
- Navigator swatches (the multi-canvas case from iced_composition_skeleton_spec.md §4.8)
- expanded swatch previews
- any future Atlas/Overview or analysis-canvas surface
Vello renders an arbitrary 2D scene composed of paths, gradients, images,
text, and effects — exactly what a graph canvas is. It composes through
the iced shader widget, sharing the iced wgpu device.
WebRender is the canonical renderer for web-document tile content:
- Servo-rendered tiles for full web content
- (Future) WebRender via the Graphshell Netrender fork for Linebender-Servo-component-rendered content
- Document-shaped middlenet content where WebRender is the right renderer (currently rare — middlenet primarily lives with native iced rendering, see §3)
WebRender is not a general 2D scene renderer. Per the 2026-04-30 directive, WebRender's role is bounded: it is the workbench workhorse for documents/pages/sites, not the canvas renderer.
Until the WebRender stabilization path (Netrender fork, Servo wgpu integration, shared-device interop) is reliable, Graphshell does not rely on Servo/WebRender for application fundamentals.
This means:
- The first iced bring-up does not depend on shared-wgpu interop with Servo working perfectly.
- Per-pane web content uses Wry as the primary path during this period (each Wry webview renders into a native overlay or a separate texture; not shared with the iced wgpu device).
- Upstream Servo is consumed where it works (offscreen Servo rendering; Servo subsystem tool pane access; existing landed servo-engine feature-gated paths).
- The Netrender fork plan (2026-03-23_webrender_wgpu_backend_implementation_plan.md) remains active but is treated as a long-horizon goal that, when it lands, slots in as a tile renderer alongside Wry — not as a prerequisite for first bring-up.
Practically: if Netrender stabilizes, web content moves there. If it doesn't, Wry remains the path. Either way, Vello handles canvas; WebRender handles documents-when-available.
Per the 2026-04-30 directive: get the application fundamentals down with what we have that isn't Servo/WebRender. Three concrete paths:
Wry is the immediate path for embedded web content. Already integrated
via crates/iced-wry-viewer (per the 2026-04-25 extraction). Next
steps:
- Validate Wry as the primary content-surface during S3/S4 bring-up.
- Confirm the wry → graph-node materialization (link-click → CreateNodeAtUrl)
round-trip per
iced-wry-viewerdemo. - Test multi-Pane wry hosting (does cross-Pane focus work? do multiple webviews coexist without Z-fighting?).
- Explore Wry's text-extraction story for reader-mode (§5).
crates/iced-graph-canvas-viewer and the wgpu interop architecture
plan
(2026-04-10_wgpu_gui_bridge_interop_architecture_plan.md)
are the existing bridge between iced and the canvas renderer (Vello).
Refinement:
- Promote
iced-graph-canvas-viewerfrom "starting point" to the realCanvasBackend<NodeKey>impl per iced jump-ship plan §S4. - Validate the multi-canvas hosting requirements (§3.2.1 host-neutral necessities) on the Vello path.
- Tighten the wgpu-device sharing (one device for iced + Vello), so Navigator swatches and the main canvas share GPU resources cleanly.
Where Servo's offscreen rendering and devtools access already work, keep using them. The Servo subsystem tool pane (per iced_browser_amenities_spec.md §6) remains a viable surface even before shared-wgpu interop.
The egui-host-with-Servo build path (servo-engine + egui-host)
remains in code under feature gates until the iced path covers
parity for the use cases that need Servo. Egui retirement (per the
landed 314dc093 retire egui and GL compat build roots) is staged;
Servo via the iced host is the destination, but the path can route
through the egui host during transition.
"smolnet" — lightweight, non-Servo, non-Wry network content rendering
— is the third leg. middlenet (per
2026-04-24_iced_content_surface_scoping.md)
covers Gemini, RSS, Markdown, plain text, and similar
document-shaped non-HTML content.
middlenet's network needs flow through graphshell-net per
graphshell-net §8; rendering
goes through Vello (per §3 below).
middlenet was scoped as Blitz-top + webrender-wgpu per the existing 2026-04-24_iced_content_surface_scoping.md: Blitz (Linebender's lightweight HTML/CSS engine) handles document layout; webrender-wgpu (or Netrender) handles rendering.
Under the 2026-04-30 renderer policy, middlenet refactors toward:
- Layout: keep Blitz for document layout — Blitz uses Stylo (Servo's CSS engine) and Taffy, both of which compose well with iced and don't drag in WebRender. Layout output is a tree of positioned, styled boxes plus inline runs.
- Rendering: Vello (instead of webrender-wgpu / Netrender) for immediate-term rendering of Blitz-laid-out content. Vello handles the path/gradient/text/image primitives Blitz needs.
- Text shaping: Parley (Linebender) for text shaping + metrics. Parley pairs naturally with Vello.
-
Images / decoded content: usual
imagecrate + GPU upload through Vello/wgpu.
The full rendering stack becomes pure Linebender + Servo-component crates: Stylo (CSS) + Taffy (layout) + Parley (text) + Vello (paint). No WebRender dependency in the middlenet path.
When/if Netrender stabilizes, middlenet's render path can slot WebRender back in for content where document-renderer optimization matters; until then, Vello.
middlenet covers what fits in a document model without full web runtime:
- Plain text (UTF-8) — trivial to render.
- Markdown (CommonMark + tables, gfm) — pulldown-cmark → Blitz tree.
- HTML (subset) — Blitz natively (it's an HTML engine).
- RSS / Atom — feed parser → templated HTML → Blitz.
- Gemini — gemtext parser → simple HTML → Blitz.
- PDF (future) — separate; not middlenet.
For each content kind, middlenet:
- Fetches via
graphshell-net(aProviderRequestor directDownloadRequest-shape). - Parses to a normalized HTML(-like) tree.
- Lays out via Blitz/Stylo/Taffy.
- Shapes text via Parley.
- Paints to a Vello scene composed into the iced
shaderwidget.
Out of scope for this plan to enumerate refactor commits, but the direction:
- Replace middlenet's webrender-wgpu paint with a Vello paint step (~Stage A of refactor).
- Replace any WebRender-specific layout assumptions with Blitz directly (most of middlenet's tree probably already passes through Blitz; verify and tighten).
- Wire
graphshell-netProviderRequest / DownloadRequest as the only network entry (middlenet does not have its own HTTP). - Validate against existing middlenet tests + add Vello-paint tests.
The refactor superseded the Blitz-shaped chrome scoping (2026-04-24_blitz_shaped_chrome_scoping.md) on the content side; the chrome side (Blitz for shell chrome) is a separate question still tracked by that scoping doc.
Per the 2026-04-30 directive: lean against keeping EmbeddedHost
as a render mode by default; audit for bits worth graverobbing.
EmbeddedHost is a ViewerRenderMode / TileRenderMode value
(renamed from EmbeddedEgui in receipt
§8.1 of the iced jump-ship plan)
designating "viewers that draw through the host's UI primitives
rather than to a backend texture or via a native overlay." It was
the canonical home for non-web tool/diagnostic viewers in the egui
era.
Under the new renderer policy:
- Canvas content has a clear renderer (Vello).
- Web/document content has a clear renderer (WebRender via Servo or Wry; middlenet via Vello).
-
Tool/diagnostic panes historically used
EmbeddedHostbecause egui rendered them inline; in iced these are just plain iced widgets (column!,text,scrollable, etc.) with no special render mode.
The EmbeddedHost mode therefore loses its distinct purpose — every
"embedded host" surface in iced is just a normal widget tree. Keeping
the mode as a separate render category adds bookkeeping without
buying anything.
What may be worth keeping when the mode is retired:
-
TileRenderModeenum's invariant statements (e.g., "exactly one of CompositedTexture / NativeOverlay / EmbeddedHost / Placeholder for a tile"). The remaining variants (CompositedTexture,NativeOverlay,Placeholder) cover the iced cases;Placeholdertakes over for "uninitialized / loading" states. -
CompositorAdaptercallback ordering rules (Surface Composition Pass in TERMINOLOGY.md). These are renderer-independent and apply to canvas Panes / tile Panes regardless of mode. -
The diagnostics channel events (
embedded_host/CHANNEL_COMPOSITOR_OVERLAY_MODE_EMBEDDED_HOST). Drop the embedded-host-specific events; keep the generic mode-transition events. -
Serde aliases for the legacy
EmbeddedEguipayload — keep during migration, retire when egui code retires.
For tool/diagnostic panes, the iced realization is:
- A normal iced widget tree as the Pane body.
-
TileRenderMode::Placeholderwhile loading. - No need for an
EmbeddedHostmode — the pane simply renders through iced's standard widget pipeline.
For non-web non-canvas viewers (e.g., a future PDF viewer, an audio
viewer, a video viewer), each gets its own TileRenderMode if it
needs special compositor handling; otherwise they fit under
CompositedTexture (their content is GPU-textured) or just a
custom iced widget.
- Audit
crates/graphshell-runtime/src/content_surface.rsandcrates/graphshell-runtime/src/ports.rsforEmbeddedHostreferences. Categorize each as keep / graverob / retire. - Update viewer specs
(viewer_presentation_and_fallback_spec.md,
universal_content_model_spec.md,
node_lifecycle_and_runtime_reconcile_spec.md)
to remove
EmbeddedHostfrom the canonical mode set; describe tool/diagnostic panes as plain iced widget trees. - Retire the
EmbeddedHostenum variant in code once the iced bring-up reaches a point where no callers use it (likely S6 of the iced jump-ship plan). - Preserve serde aliases for one release cycle for any persisted
EmbeddedEgui/EmbeddedHostpayloads.
This is an audit + retirement, not a hard cut; egui-host code keeps the mode while the egui path is alive.
Per the 2026-04-30 directive (#8): reader modes tailored for middlenet. Per the settings + permissions spine view/tile scope.
Two related but distinct view modes:
- Native rendering: the content as the viewer renders it natively — Servo's full rendering for HTML, middlenet's Blitz/Vello stack for non-HTML, Wry's webview for the wry path.
- Reader rendering: a content-extracted, simplified view where middlenet's renderer takes over. The Servo-rendered HTML page's main content is extracted (heuristic), reflowed through Blitz, and painted by Vello. Same stack as middlenet's native content rendering.
Reader mode for non-HTML middlenet content (Markdown, RSS, Gemini, plain text) is the default: those content kinds are already "reader" by nature. The mode toggle is meaningful primarily for Servo-rendered web pages.
A per-tile toggle in the tile chrome (icon button, also in the context menu):
-
View: Native— viewer's native rendering -
View: Reader— middlenet-rendered content extraction
State is tile-scope per the settings spine (§3.1 canonical scope: "Reader mode toggle"); a user can default to reader for one graph or one persona.
When a user toggles a Servo-rendered page to reader mode:
- Servo continues to load/run the page in the background (so navigation state is preserved).
- Graphshell asks Servo for the rendered DOM via the devtools/observer protocol (or scrapes the rendered HTML).
- The DOM is fed through a Readability-style extractor (the
open-source Readability port for Rust, or
readable-readability). - The extracted main content is converted to clean HTML.
- Blitz/Stylo/Taffy lay out the cleaned HTML.
- Parley shapes the text.
- Vello paints. (Same middlenet path as §3.)
Toggle back to native: Vello scene is destroyed; Servo's native output reappears (the underlying view never stopped). No re-fetch required.
Reader mode applies a dedicated font_family_content (per
theme tokens spec §2.2) and a wider
density profile. Per-persona reader-mode style overrides
("Pinkish background, serif font, 18pt") live in persona settings.
In reader mode:
- Links remain clickable; activation routes through the same
OpenAddressintent as the omnibar. - Selection works (text selection for clip / quote / agent reference).
- Find-in-page (Ctrl+F) operates on the extracted content (a
ViewerIntent::FindInPagevariant for reader-mode targets). - The reader-mode renderer does not run JavaScript; this is part of what makes it "reader."
- The full extraction-quality story (Readability heuristics, fallback when extraction fails, ML-augmented extraction). Tracked as middlenet content-extraction follow-up.
- PDF reader (separate viewer, not middlenet).
- Print/export from reader mode (separate spec — see graphshell-net §9 print pipeline open item).
Per the 2026-04-30 directive (#12): lower the threshold from 600 LOC to 500 LOC and decompose every portable-crate file exceeding it.
No portable-crate Rust file exceeds 500 LOC (was 600 in the prior iced jump-ship plan §3.2). When a file approaches the threshold, decompose by responsibility before adding more code.
Per the 2026-04-28 inventory in iced jump-ship plan §3.2, files over 600 LOC at that snapshot:
| File | Lines | Decomposition target |
|---|---|---|
crates/graphshell-core/src/graph/mod.rs |
4,937 | identity, node, edge, graphlet, lifecycle, mutation, query, selection |
crates/graph-cartography/src/lib.rs |
3,623 | projection, mapping, layout_export, view_model, registry, error |
crates/graph-tree/src/tree.rs |
1,766 | tree storage, mutation commands, traversal, focus/activation, layout I/O |
crates/graph-canvas/src/derive.rs |
1,391 | node projection, edge projection, selection enrichment, style, diagnostics |
crates/graph-memory/src/lib.rs |
1,372 | memory model, indexing, recall, persistence, scoring |
crates/graph-canvas/src/engine.rs |
1,209 | engine state, tick loop, input commands, camera, backend handoff |
crates/middlenet-core/src/document.rs |
887 | document model, block tree, text ranges, annotations, serialization |
crates/graph-canvas/src/layout/rapier_adapter.rs |
862 | bodies/colliders, force application, constraints, result extraction |
crates/graph-canvas/src/layout/extras.rs |
804 | clustering, pinning, viewport constraints, debug aids |
crates/graph-canvas/src/layout/static_layouts.rs |
801 | per-static-layout-family modules + facade |
crates/graph-canvas/src/layout/registry.rs |
793 | layout descriptors, profile registry, factory, validation |
crates/graph-tree/src/topology.rs |
785 | model, adjacency, paths, invariants |
crates/graphshell-core/src/actions.rs |
711 | identifiers, descriptors, dispatch metadata, serialization |
crates/graphshell-core/src/graph/filter.rs |
709 | AST/types, parser, evaluator, text matching, diagnostics |
crates/graphshell-runtime/src/frame_projection.rs |
708 | input collection, frame/view projection, overlay projection, command-surface projection |
crates/graphshell-core/src/shell_state/frame_model.rs |
673 | frame identity, tree/model, lifecycle commands, persistence shape |
crates/graph-canvas/src/scene_physics.rs |
652 | force model, integration, constraints, scene-runtime adapters |
crates/middlenet-adapters/src/lib.rs |
649 | adapter traits, iced adapter, wry/host adapters, test fixtures |
Plus any portable-crate files between 500 and 600 LOC at the same inventory point, which were not previously called out — those now also need decomposition under the new threshold. A fresh inventory pass is the first sub-step.
- Do not introduce new files over 500 LOC in portable crates.
- When a slice touches an oversized file, either decompose first or extract the touched responsibility in the same change.
- Preserve public crate APIs during the first split (re-export from the old module path); rename external APIs only in a focused follow-up.
- Keep extraction mechanical before changing behavior: move code, re-export, run narrow tests/checks, then make semantic changes.
- Prefer domain names (graphlet, lifecycle, projection) over implementation names (helpers, utils).
- Update iced jump-ship plan §3.2 "no new files over 600 LOC" rule to read 500 LOC.
- Take a fresh inventory across all
crates/portable crates and list 500-600 LOC files alongside the existing >600 LOC list. - Decomposition lands as a separate work stream from the iced bring-up (§6.5).
The decomposition target is decoupled from the iced bring-up: S3/S4 work proceeds against the current code shape; decomposition happens as its own slice work stream so it doesn't block surface implementation. The constraint on new code (no new files over 500 LOC in portable crates) applies immediately; the back-fill decomposition of existing oversized files can land progressively.
- Netrender fork status: the long-horizon Netrender fork (2026-03-23_webrender_wgpu_backend_implementation_plan.md) remains active; this plan does not redirect that work, only decouples the iced bring-up from its completion.
- Vello-via-shader-widget device sharing: how cleanly does the iced wgpu device share with Vello scene buffers across N canvas instances (per the multi-canvas hosting requirements)? Validation needed in the Stage C (canvas Program) iced bring-up.
- Reader-mode extraction quality: Readability ports vary; needs evaluation in middlenet-side tests.
- Wry on Linux: WebKitGTK is the Wry backing on Linux; behavior differs from WebView2/WKWebView. Multi-Wry-pane testing is needed.
- Servo via egui-host transition path: how long does the egui host stay alive specifically for Servo features that haven't ported to iced? Tracked under the iced jump-ship plan's S6 timing.
- PDF / multimedia viewers: not in this plan; their renderer choice (likely separate from Vello/WebRender split) is a future spec.
- Immediate: §6.4 update iced jump-ship plan §3.2 threshold to 500 LOC; the constraint on new files applies immediately.
- iced bring-up (S3/S4 in parallel): §2.1 Wry validation, §2.2 GUI bridge refinement (Vello as canvas backend), §2.3 upstream Servo where it works.
- Stage A done condition uses Vello: per the iced jump-ship plan §12.3, Stage A is the iced Application + Subscription closure. Vello as the canvas renderer is part of that closure.
- Middlenet refactor (§3.4): parallel to iced bring-up; can start as soon as the iced canvas backend is solid.
- EmbeddedHost retirement (§4.5): incremental audit + retire alongside egui retirement (S6 timing).
- Reader modes (§5): depends on middlenet refactor reaching a stable point; not a first-bring-up requirement.
- Decomposition back-fill (§6.5): continuous; lands as its own slice stream alongside everything else.
Vello renders graph canvases (one renderer for all canvas-shaped surfaces); WebRender renders web/document tiles when available; neither path depends on the other. Until Servo/WebRender stabilizes through the Netrender fork, application fundamentals use Wry + upstream Servo + smolnet (middlenet) for content. Middlenet refactors from Blitz-top + webrender-wgpu to Stylo + Taffy + Parley + Vello — all Linebender + Servo-component crates, no WebRender dependency. EmbeddedHost as a render mode is audited for retirement in favor of plain iced widget trees for tool/diagnostic panes. Reader modes use the middlenet stack to render extracted Servo-page content. Decomposition target lowers to 500 LOC for portable-crate files; constraint on new files is immediate, back-fill is continuous.
This plan is a direction — the surface specs (composition skeleton, canvas instances, middlenet) reference this for the renderer choices; the iced jump-ship plan §3.2 picks up the 500-LOC threshold; the EmbeddedHost retirement folds into the existing egui-retirement schedule.