# GPUI Framework — Detailed Findings **Date:** 2026-03-19 (updated 2026-03-19 22:30) **Source:** Hands-on prototyping (ui-gpui/, 2,490 lines) + source code analysis of GPUI 0.2.2 + Zed editor + 4 parallel research agents ## BREAKTHROUGH: 4.5% → 0.83% CPU **The #1 optimization rule in GPUI:** Never make animated state an Entity CHILD of the view that renders it. Make it a PEER Entity that the view READS. ``` WRONG (4.5% CPU): Workspace → ProjectGrid → ProjectBox → PulsingDot (child Entity) cx.notify() on PulsingDot → mark_view_dirty walks ancestors → Workspace dirty → refreshing=true → ALL .cached() children miss RIGHT (0.83% CPU): Workspace → ProjectGrid → ProjectBox (reads BlinkState) BlinkState (peer Entity, not child) cx.notify() on BlinkState → only dirties views that .read(cx) it → ProjectBox dirty, Workspace/ProjectGrid NOT dirty → siblings cached ``` Key mechanisms: 1. **`.cached(StyleRefinement::default())`** on Entity children → GPU scene replay (zero Rust code) 2. **Shared state Entity** instead of child Entity → isolates dirty propagation 3. **`mark_view_dirty()` walks ancestors** — this is GPUI's fundamental constraint 4. **`refreshing=true` on cache miss** cascades down → children can't cache if parent is dirty ## DEEP DIVE: Why 3% is the floor for div-based views (2026-03-19 late) ### The full dirty propagation chain (confirmed by source analysis) ``` cx.notify(entity_id) → App::notify → WindowInvalidator::invalidate_view(entity_id) → dirty_views.insert(entity_id) → draw() → invalidate_entities() → mark_view_dirty(entity_id) → dispatch_tree.view_path_reversed(entity_id) → walks parent chain → inserts entity + ALL render-time ancestors into dirty_views → AnyView::prepaint checks: dirty_views.contains(MY entity_id) → if true: cache MISS, re-render → if false: cache HIT, replay GPU commands ``` ### Why Zed achieves <1% with the SAME mechanism Zed's BlinkManager → observer → cx.notify(Editor) → Editor + Pane + Workspace ALL in dirty_views. ALL re-render. But: - Workspace::render() = ~5 div builders = <0.1ms - Pane::render() = ~10 div builders = <0.2ms - Editor::render() = returns EditorElement struct = <0.01ms - EditorElement::prepaint/paint = real work, bounded to visible rows Total: <1ms per blink frame. Our ProjectBox::render() = ~9 divs + 3 tab buttons = ~15ms. The 15ms includes Taffy layout + element prepaint + paint pipeline overhead per div. ### SharedBlink pattern (Arc) Replaced Entity with `Arc`: - Background timer toggles atomic bool every 500ms - Timer calls cx.notify() on ProjectBox directly (no intermediate entity) - ProjectBox::render() reads bool atomically — no Entity subscription overhead - Same 3% CPU — confirms cost is in render() itself, not entity machinery ### Hierarchy flattening (4 levels → 2 levels) Removed ProjectGrid entity level. ProjectBoxes render directly from Workspace. Dispatch tree: Workspace → ProjectBox (2 levels, same as Zed's Workspace → Pane). CPU unchanged at 3% — confirms cost is per-frame render(), not ancestor walk count. ### Path to <1%: Custom Element for ProjectBox header Convert header (accent stripe + dot + name + CWD + tab bar) from div tree to custom `impl Element` that paints directly via `window.paint_quad()` + `window.text_system().shape_line().paint()`. This eliminates: - 9 div() constructor calls - 9 Taffy layout nodes - 9 prepaint traversals - 9 paint traversals Replaced by ~6 direct GPU primitive insertions (paint_quad × 4 + shape_line × 2). Expected reduction: 15ms → <1ms per frame. ## Overview GPUI is the GPU-accelerated UI framework powering the Zed editor. Apache-2.0 licensed (the crate itself; Zed app is GPL-3.0). Pre-1.0 with breaking changes every 2-3 months. 76.7k stars (Zed repo), $32M Sequoia funding. ## Architecture ### Rendering Model - Hybrid immediate + retained mode - Target: 120 FPS (Metal on macOS, Vulkan via wgpu on Linux/Windows) - No CSS — all styling via Rust method chains: `.bg()`, `.text_color()`, `.border()`, `.rounded()`, `.px()`, `.py()`, `.flex()` - Text rendering via GPU glyph atlas - Binary size: ~12MB (bare GPUI app) - RAM baseline: ~73-200MB (depending on complexity) ### Component Model ```rust struct MyView { /* state */ } impl Render for MyView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div().flex().bg(rgba(0x1e1e2eff)).child("Hello") } } ``` ### Entity System - `Entity` — strong reference to a view/model (Arc-based) - `WeakEntity` — weak reference (used in async contexts) - `Context` — mutable access to entity + app state during render/update - `cx.new(|cx| T::new())` — create child entity - `entity.update(cx, |view, cx| { ... })` — mutate entity from parent - `cx.notify()` — mark entity as dirty for re-render ### State Management - `Entity` for shared mutable state (like Svelte stores) - `cx.observe(&entity, callback)` — react to entity changes - `entity.read(cx)` — read entity state (SUBSCRIBES to changes in render context!) - `cx.notify()` — trigger re-render of current entity ## Event Loop ### X11 (Linux) - Uses `calloop` event loop with periodic timer at monitor refresh rate (from xrandr CRTC info, fallback 16.6ms = 60Hz) - Each tick: check `invalidator.is_dirty()` → if false, skip frame (near-zero cost) - If dirty: full window draw + present - When window hidden: timer removed entirely (0% CPU) - File: `crates/gpui_linux/src/linux/x11/client.rs` ### Wayland - Entirely compositor-driven: `wl_surface.frame()` callback - No fixed timer — compositor tells GPUI when to render - Even more efficient than X11 (no polling) - File: `crates/gpui_linux/src/linux/wayland/client.rs` ### Window Repaint Flow ``` cx.notify(entity_id) → App::notify() → push Effect::Notify → Window::invalidate() → set dirty=true → calloop tick → is_dirty()=true → Window::draw() → Window::draw() → render all dirty views → paint to GPU → present → is_dirty()=false → sleep until next tick or next notify ``` ## Animation Patterns ### What We Tested (chronological) | Approach | Result | CPU | |----------|--------|-----| | `request_animation_frame()` in render | Continuous vsync loop, full window repaint every frame | **90%** | | `cx.spawn()` + `tokio::time::sleep()` | spawn didn't fire from `cx.new()` closure | N/A | | Custom `Element` with `paint_quad()` + `request_animation_frame()` | Works but same vsync loop | **90%** | | `cx.spawn()` + `background_executor().timer(200ms)` + `cx.notify()` | Works but timer spawns don't fire from `cx.new()` | **10-15%** | | Zed BlinkManager pattern: `cx.spawn()` from `entity.update()` after registration + `timer(500ms)` + `cx.notify()` | **Works correctly** | **5%** | | Same + cached SharedStrings + removed diagnostics | Same pattern, cheaper render tree | **4.5%** | ### The Correct Pattern (Zed BlinkManager) From Zed's `crates/editor/src/blink_manager.rs`: ```rust fn blink_cursors(&mut self, epoch: usize, cx: &mut Context) { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { self.visible = !self.visible; cx.notify(); // marks view dirty, does NOT repaint immediately let epoch = self.next_blink_epoch(); let interval = self.blink_interval; cx.spawn(async move |this, cx| { cx.background_executor().timer(interval).await; this.update(cx, |this, cx| this.blink_cursors(epoch, cx)); }).detach(); } } ``` Key elements: 1. **Epoch counter** — each `next_blink_epoch()` increments a counter. Stale timers check `epoch == self.blink_epoch` and bail out. No timer accumulation. 2. **Recursive spawn** — each blink schedules the next one. No continuous loop. 3. **`background_executor().timer()`** — sleeps the async task for the interval. NOT `tokio::time::sleep` (GPUI has its own executor). 4. **`cx.notify()`** — marks ONLY this view dirty. Window draws on next calloop tick. ### Critical Gotcha: `cx.spawn()` Inside `cx.new()` **`cx.spawn()` called inside a `cx.new(|cx| { ... })` closure does NOT execute.** The entity is not yet registered with the window at that point. The spawn is created but the async task never runs. **Fix:** Call `start_blinking()` via `entity.update(cx, |dot, cx| dot.start_blinking(cx))` AFTER `cx.new()` returns, from the parent's `init_subviews()`. ### Why 4.5% CPU Instead of ~1% (Zed) `cx.notify()` on PulsingDot propagates to the window level. GPUI redraws ALL dirty views in the window — including parent views (Workspace, ProjectGrid, ProjectBox). Our prototype rebuilds ~200 div elements per window redraw. Zed achieves ~1% because: 1. Its render tree is heavily optimized with caching 2. Static parts are pre-computed, not rebuilt per frame 3. Entity children (`Entity`) passed via `.child(entity)` are cached by GPUI 4. Zed's element tree is much deeper but uses IDs for efficient diffing Our remaining 4.5% optimization path: - Move header, tab bar, content area into separate Entity views (GPUI caches them independently) - Avoid re-building tab buttons and static text on every render - Use `SharedString` everywhere (done — reduced from 5% to 4.5%) ## API Reference (GPUI 0.2.2) ### Entity Creation ```rust let entity: Entity = cx.new(|cx: &mut Context| T::new()); ``` ### Entity Update (from parent) ```rust entity.update(cx, |view: &mut T, cx: &mut Context| { view.do_something(cx); }); ``` ### Async Spawn (from entity context) ```rust cx.spawn(async move |weak: WeakEntity, cx: &mut AsyncApp| { cx.background_executor().timer(Duration::from_millis(500)).await; weak.update(cx, |view, cx| { view.mutate(); cx.notify(); }).ok(); }).detach(); ``` ### Timer ```rust cx.background_executor().timer(Duration::from_millis(500)).await; ``` ### Color ```rust // rgba(0xRRGGBBAA) — u32 hex let green = rgba(0xa6e3a1ff); let semi_transparent = rgba(0xa6e3a180); // Note: alpha on bg() may not work on small elements; use color interpolation instead ``` ### Layout ```rust div() .flex() // display: flex .flex_col() // flex-direction: column .flex_row() // flex-direction: row .flex_1() // flex: 1 .w_full() // width: 100% .h(px(36.0)) // height: 36px .min_w(px(400.0)) .px(px(12.0)) // padding-left + padding-right .py(px(8.0)) // padding-top + padding-bottom .gap(px(8.0)) // gap .rounded(px(8.0)) // border-radius .border_1() // border-width: 1px .border_color(color) .bg(color) .text_color(color) .text_size(px(13.0)) .overflow_hidden() .items_center() // align-items: center .justify_center() // justify-content: center .cursor_pointer() .hover(|s| s.bg(hover_color)) .id("unique-id") // for diffing + hit testing .child(element_or_entity) .children(option_entity) // renders Some, skips None ``` ### Custom Element (for direct GPU painting) ```rust impl Element for MyElement { type RequestLayoutState = (); type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static Location<'static>> { None } fn request_layout(&mut self, ..., window: &mut Window, cx: &mut App) -> (LayoutId, ()) { let layout_id = window.request_layout(Style { size: ..., .. }, [], cx); (layout_id, ()) } fn prepaint(&mut self, ..., bounds: Bounds, ...) -> () { () } fn paint(&mut self, ..., bounds: Bounds, ..., window: &mut Window, ...) { window.paint_quad(fill(bounds, color).corner_radii(radius)); } } ``` ### Animation Frame Scheduling ```rust // From render() — schedules next vsync frame (CAUTION: 60fps = 90% CPU) window.request_animation_frame(); // From anywhere — schedule callback on next frame window.on_next_frame(|window, cx| { /* ... */ }); ``` ### Window ```rust window.set_window_title("Title"); window.request_animation_frame(); // schedule next frame window.on_next_frame(callback); // callback on next frame window.paint_quad(quad); // direct GPU paint in Element::paint() ``` ## Known Limitations (2026-03) 1. **Pre-1.0 API** — breaking changes every 2-3 months 2. **No per-view-only repaint** — `cx.notify()` propagates to window level, redraws all dirty views 3. **`cx.spawn()` in `cx.new()` doesn't fire** — must call after entity registration 4. **`rgba()` alpha on `.bg()` unreliable** for small elements — use color interpolation 5. **No CSS** — every style must be expressed via Rust methods 6. **No WebDriver** — can't use existing E2E test infrastructure 7. **No plugin host API** — must build your own (WASM/wasmtime or subprocess) 8. **Sparse documentation** — "read Zed source" is the primary reference 9. **macOS-first** — Linux (X11/Wayland) added 2025, Windows added late 2025 10. **X11 calloop polls at monitor Hz** — non-zero baseline CPU even when idle (~0.5%) ## Comparison with Dioxus Blitz | Aspect | GPUI | Dioxus Blitz | |--------|------|-------------| | Styling | Rust methods (`.bg()`, `.flex()`) | CSS (same as browser) | | Animation | Spawn + timer + notify (~4.5% CPU) | Class toggle + no CSS transition (~5% CPU) | | Animation limit | cx.notify propagates to window | CSS transition = full scene repaint | | Custom paint | Yes (Element trait + paint_quad) | No (CSS only, no shader/canvas API) | | Render model | Retained views + element diff | HTML/CSS via Vello compute shaders | | Terminal | alacritty_terminal + GPUI rendering | xterm.js in WebView (or custom build) | | Migration cost | Full rewrite (no web tech) | Low (same wry webview as Tauri) | | Ecosystem | 60+ components (gpui-component) | CSS ecosystem (any web component) | | Text rendering | GPU glyph atlas | Vello compute shader text | ## Files in Our Prototype ``` ui-gpui/ ├── Cargo.toml ├── src/ │ ├── main.rs — App entry, window creation │ ├── theme.rs — Catppuccin Mocha as const Rgba values │ ├── state.rs — AppState, Project, AgentSession types │ ├── backend.rs — GpuiEventSink + Backend (PtyManager bridge) │ ├── workspace.rs — Root view (sidebar + grid + statusbar) │ ├── components/ │ │ ├── sidebar.rs — Icon rail │ │ ├── status_bar.rs — Bottom bar (agent counts, cost) │ │ ├── project_grid.rs — Grid of ProjectBox entities │ │ ├── project_box.rs — Project card (header, tabs, content, dot) │ │ ├── agent_pane.rs — Message list + prompt │ │ ├── pulsing_dot.rs — Animated status dot (BlinkManager pattern) │ │ ├── settings.rs — Settings drawer │ │ └── command_palette.rs — Ctrl+K overlay │ └── terminal/ │ ├── renderer.rs — GPU terminal (alacritty_terminal cells) │ └── pty_bridge.rs — PTY via agor-core ``` ## Sources - [GPUI crate (crates.io)](https://crates.io/crates/gpui) - [GPUI README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md) - [Zed BlinkManager source](https://github.com/zed-industries/zed/blob/main/crates/editor/src/blink_manager.rs) - [GPUI X11 client source](https://github.com/zed-industries/zed/blob/main/crates/gpui_linux/src/linux/x11/client.rs) - [GPUI Wayland client source](https://github.com/zed-industries/zed/blob/main/crates/gpui_linux/src/linux/wayland/client.rs) - [GPUI window.rs source](https://github.com/zed-industries/zed/blob/main/crates/gpui/src/window.rs) - [gpui-component library](https://github.com/4t145/gpui-component) - [awesome-gpui list](https://github.com/zed-industries/awesome-gpui) - [Zed GPU rendering blog](https://zed.dev/blog/videogame)