stack-RFC-019 — Koder UI render strategy: Verge over wgpu/Vello
Proposes the long-term UI architecture for the Koder Stack: converge every surface of every component onto ONE Koder-owned Rust UI framework — the Verge design system as a shared widget/scene layer, rendered by a single wgpu/Vello backend that becomes native GPU (Vulkan/Metal/DX12/WebGPU) per platform "for free". This removes the Flutter desktop-feel tax and the GTK dependency at the convergence target, and makes Verge pixel-identical everywhere while Koder owns the whole UI stack (the Koda/kdb/kodec "own-your-primitives" line). Because a UI framework is the single largest software undertaking and the frontier is immature for mobile/web/a11y, the Stack is NOT bet on a deliberation: the decision is GATED by a measured proof-slice (the terminal as first consumer) that must clear a11y + headless-testability + startup/feel before any broad migration; if it fails, fall back to a native GTK4/Libadwaita desktop kit (option b).
Status: proposed. This RFC formalizes a multi-turn architectural deliberation (kterm daily-driver friction, 2026-06-24). It proposes a direction and a gate. AMENDED 2026-06-25 (owner): the broad migration is now authorized — Flutter is sunset Stack-wide, Kroma is the default for all new UI; per-app migration stays gated on Kroma parity (see
ratification_note). The owner (visual designer,design-governance.kmd) ratifies.
1. Problem
Koder builds UI on Flutter (koder_kit, RFC-006 app/desktop/ = Flutter LinuxmacOSWindows). Flutter buys one codebase across mobilewebTV/desktop — but on the desktop daily-driver it pays a structural tax the owner hit head-on with kterm:
- Cold-start latency — the Dart VM + Flutter engine + Skia init + AOT load +
widget-tree build, every launch. A native GTK app (Tilix) skips all of it.
- Window-op latency (openclosemaximize) —
window_managerround-trips +full-window repaint, heavier than native GTK.
- Render feel — Flutter renders its own widgets (Skia), never the platform's;
the same root as kterm#002's 250× VT-throughput gap vs the native VTE.
- On the owner's NVIDIA laptop, hardware-GL Flutter even segfaults (forced to
software GL → slower still).
The deeper truth surfaced in deliberation: "one codebase, GPU-rendered" and "native feel" are mutually exclusive at the render layer. Flutter chose one-codebase (and pays the feel tax); GTK/VTE chose native-feel (per-platform cost). You cannot have both from the same widgets.
2. The insight that resolves it
A terminal's "Tilix feel" is fast startup + fast text grid + snappy window-ops + a theme you like — NOT "native-looking buttons" (a terminal is a grid). And Koder wants its own look everywhere (Verge), not each OS's look. So the right target is not "be native" — it's "be a faster, Koder-owned canvas framework, with the fastest GPU path per platform."
Two facts make this cheap to architect:
wgpualready IS the per-platform fast-render abstraction. One render layercompiles to Vulkan (Linux), Metal (macOS), DX12 (Windows), WebGPU (web), GLES/Vulkan (mobile) — native GPU everywhere, no N render variants to write. (
Vello, Linebender's compute-shader 2D vector renderer, is the high-perf substrate;Xilemis the widget framework on top — the serious "Rust Flutter".)- Verge is already implementation-agnostic.
verge.kmdis a spec of tokens/presets/components (with an AA-contrast CI gate, R8) consumed by N renderers (koderkit Flutter, koderweb_kit JS). "Verge as the shared layer over a render backend" is how Verge already works — this RFC adds one more, owned, backend.
So the architecture is two layers, not N:
Verge widget/scene layer (Rust, written ONCE: tokens, components, layout)
│
one wgpu/Vello render layer → Vulkan / Metal / DX12 / WebGPU (native GPU, free)
│
winit windowing → Linux / macOS / Windows / Web / (mobile)3. Options considered
| Description | Long-term | |
|---|---|---|
| a — right-tool-per-surface (status quo) | Flutter (mobilewebTVdesktop) + GTK desktop + templHTMX web | N UI stacks forever; Verge re-implemented per stack; Flutter feel-tax persists; violates reuse-first at the framework tier |
| b — native GTK4/Libadwaita + VTE desktop kit + Flutter elsewhere | Consolidate desktop on the Kroll GTK bet; VTE gives Tilix feel now | Rents VTE+GTK (non-endgame); Linux-strong only; still 2-3 stacks; Verge inherits Adwaita |
| c — ONE Rust framework: Verge over wgpu/Vello (this RFC) | Converge all surfaces onto a Koder-owned GPU UI framework | One codebase; Verge pixel-identical everywhere; native GPU per platform (free via wgpu); own the whole stack; removes Flutter AND GTK at convergence; solves the root cause on EVERY surface |
4. Decision (proposed): c, gated by a proof-slice
Per the Solidez rubric — D10 (reuse/amortization: one framework vs N), D12 (root-cause > symptom; effort is explicitly not a tiebreaker), D9 (reversibility: wgpu decouples from the GPU vendor; Koder owns the stack), and self-hosted-first (own-your-primitives endgame, the Kodakdbkodec line) — c is the structurally superior long-term form. stack-principles §2 says pursue the better form when achievable; the achievability risk (a11y, mobile/web maturity, the headless harness) is exactly what a proof-slice answers, not a deduction.
Therefore: c is the target; the proof-slice is the gate. The Stack is NOT migrated on this RFC. The gate is run first.
4.1 Proof-slice = the terminal (first consumer)
The terminal is the ideal proving ground: near-zero widgets (it's a grid), the owner's daily pain, and the GPU grid showcases the perf (and closes kterm#002/
003 throughput as a bonus). Build a native Rust terminal on the Verge-over-wgpu
substrate (grid via the alacritty_terminal model + wgpu/Vello render + Verge chrome).
4.2 Acceptance gate (all three must pass)
- a11y baseline — AA contrast (renders Verge tokens,
verge.kmdR8 → passes byconstruction) + screen-readerfocusIME via AccessKit (the hard part — must reach the
self-hosted-firstG4 UI a11y baseline). - Headless-testability —
headless-first.kmdis mandatory for any KoderUI. The framework must ship a headless test path (own harness, composing with
stack-RFC-005's 3-layer model +.kdtfidelity contract). "Passa na minha tela" is not a valid state. - Measured feel — startup + openclose + frame latency * native (GTKVTE)*
and << Flutter, on the
stack-RFC-012UI-interaction-perf corpus.
4.3 Branch on the gate
- Pass → ratify c as the Stack UI future; migrate incrementally (Flutter
stays only on not-yet-migrated surfaces during transition; amend RFC-006 to admit the native framework as a first-class
app/desktop/(and beyond) path). - Fail (a11y or maturity blocker) → fall back to option b (GTK4/Libadwaita
desktop kit + VTE) for the desktop daily-driver; Flutter stays elsewhere.
5. Sequencing (no throwaway stack)
- Now — bridge: quake-mode for the current Flutter kterm (resident process,
F12 summon) → kills the cold-start today, zero throwaway.
- Proof-slice: the native terminal on Verge-over-wgpu, run against §4.2.
- Converge or fall back per §4.3. Do not build the GTK kit (b) unless the
proof fails — building b then converging to c would be wasted stack.
6. Trade-offs accepted
A multi-year UI-framework build, with a11y + the headless harness as the hardest sub-problems and frontier risk on mobilewebTV — in exchange for one codebase across all surfaces of all components, Verge-native everywhere, native-GPU-fast per platform, and ownership of the entire UI stack. Mitigated by proving on the terminal before committing the Stack.
7. Open questions
- Framework base: Xilem+Vello (serious, but pre-1.0) vs a thinner roll-your-own
on wgpu vs evaluating icedSlintMakepad. Decide at proof-slice kickoff.
- a11y depth required to clear G4 (AccessKit maturity per platform).
- MobilewebTV timeline — desktop first; mobile is Flutter's strongest turf and
the framework's weakest; web a11ySEO of a canvas needs the templHTMX split kept.
- koder_kit (Flutter) coexistence — long migration; both live during transition.
- Naming/location of the new framework component (a new
engines/sdk/SDK;resolve via
specs/naming/forms.kmd+component-names.md).
8. Significance
This is the largest architectural decision the Stack can take — bigger than any single product. It is proposed, not ratified. The proof-slice exists precisely so the bet is made on evidence (the Stack's proof-slice culture), not on the elegance of a diagram.
9. Amendment 2026-06-25 #2 — Kavi folded into Kroma + Kroma self-host flip
Status: RATIFIED 2026-06-25 (owner, /k-go). Normative. Resolves the §7 open question the original RFC left implicit: the relationship between Kroma and Koda's own native UI framework (#261). The RFC named Kroma as the convergence target but never mentioned the Koda-native UI track — this section closes that gap.
9.1 Context — two native UI frameworks existed in parallel
- Kroma (
engines/sdk/kroma) — the Stack's UI convergence target (this RFC):Rust, Verge over wgpuVellowinit, GPU-first, replaces Flutter everywhere.
- Koda native UI (koda#261, the "Flutter da Koda") — a cross-platform UI
framework written in Koda, own rendering (WaylandWin32WASM Canvas2D/Android backends done). Provisionally named "Kavi" in koda#809 (2026-06-25), with a planned own-dir
engines/lang/kavi(koda#850).
Same purpose (one Koder-owned cross-platform native UI), opposite sides of the self-hosted line (Rust vs Koda). Maintaining both is duplicated effort and two widget vocabularies. The owner resolved it: consolidate on Kroma.
9.2 Decision (1) — Kavi is folded into Kroma; the name is retired
There is ONE UI framework: Kroma. The Koda-native UI work (#261) is not an independent framework — it becomes the seed of the Koda implementation of Kroma (the future flip, §9.3). Consequences:
- Name "Kavi" retired (owner prefers "Kroma").
koda#809(Kavi naming) →superseded; its
component-names.mdregistry row (planned @engines/lang/kavi) is removed;koda#850(relocate Kavi to own dir) → gated, expected to be dropped (anengines/lang/kavimodule contradicts folding into Kroma). - The #261 backends/widget lib (#599) are not thrown away — they are the most
mature Koda-side UI code and become input to the Kroma-Koda widget layer (§9.3), or, in the interim, a Koda↔Kroma-Rust bridge (§9.4).
9.3 Decision (2) — Kroma flips Rust → Koda, transitionally (self-hosted-first)
Kroma-Rust already satisfies "Koder owns the UI stack" — but only down to the Rust layer. The deepest "own-your-primitives" line (Kodakdbkodec) wants the UI framework in Koda too. So, as the endgame, Kroma flips from Rust to Koda, using the kodec shadow-active model (the same Flipping Point that drives kodec→Koda):
- Canonical = Rust until the flip. Kroma-Rust ships and owns Stack UI now;
the Koda rewrite must not block Rust-Kroma adoption.
- Kroma-Koda = shadow, gated on koda#848 (GPU backend: SPIRVWGSLPTX) +
Koda-native equivalents of winitwgpuVello (windowing + GPU abstraction + compute vector renderer).
- Parity is TRANSITIONAL, not permanent. A cross-impl parity harness keeps the
two in lockstep during the migration: visual-regression + animationgesturea11y parity (
visual-regression-tdds,animation-parity-tdds,gesture-tdds,verge.kmd), behavioral parity, and perf ≥ Rust on the §4.2 / RFC-012 corpus. On clearing the §4.2 gate + perf, Kroma-Koda becomes canonical and Rust is retired — the Stack does NOT carry two full UI implementations forever (a UI framework's surface is far larger than a codec's; permanent dual-maintenance was explicitly rejected). Tracked inregistries/self-hosted-pairs.mdlike kodec.
9.4 Open items for the flip kickoff
- Interim Koda-UI path: until Kroma-Koda exists, how does a Koda program render
UI? FFI bindings to Kroma-Rust, or the #261 Koda code as an interim bridge — decide at kickoff. (Does not block Rust-Kroma.)
- Naming/location of the Koda impl:
engines/lang/kroma? akromasub-target? Resolve via
forms.kmd+component-names.mdwhen the flip starts (NOT now —engines/lang/kaviis dropped). - koda#848 scope is the hard gate; the flip cannot begin until Koda emits GPU
shaders. A dedicated flip-tracking ticket opens when #848 nears done.
9.5 Disposition of the just-shipped Kavi work (koda, 2026-06-25)
koda#809(name Kavi) → superseded (kept indone/as the record of thereversed decision; registry row removed; #261/#599 notes updated to Kroma).
koda#850(relocate toengines/lang/kavi) → gated (`gated_by:owner-decision`), expected to be dropped/replaced by the Kroma-Koda flip ticket.