stack-RFC-019 — Koder UI render strategy: Verge over wgpu/Vello

accepted

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_manager round-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:

  1. wgpu already IS the per-platform fast-render abstraction. One render layer

    compiles 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; Xilem is the widget framework on top — the serious "Rust Flutter".)

  2. Verge is already implementation-agnostic. verge.kmd is 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)

  1. a11y baseline — AA contrast (renders Verge tokens, verge.kmd R8 → passes by

    construction) + screen-readerfocusIME via AccessKit (the hard part — must reach the self-hosted-first G4 UI a11y baseline).

  2. Headless-testabilityheadless-first.kmd is mandatory for any Koder

    UI. The framework must ship a headless test path (own harness, composing with stack-RFC-005's 3-layer model + .kdt fidelity contract). "Passa na minha tela" is not a valid state.

  3. Measured feel — startup + openclose + frame latency * native (GTKVTE)*

    and << Flutter, on the stack-RFC-012 UI-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)

  1. Now — bridge: quake-mode for the current Flutter kterm (resident process,

    F12 summon) → kills the cold-start today, zero throwaway.

  2. Proof-slice: the native terminal on Verge-over-wgpu, run against §4.2.
  3. 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.md registry row (planned @ engines/lang/kavi) is removed; koda#850 (relocate Kavi to own dir) → gated, expected to be dropped (an engines/lang/kavi module 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 in registries/self-hosted-pairs.md like 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? a kroma

    sub-target? Resolve via forms.kmd + component-names.md when the flip starts (NOT now — engines/lang/kavi is 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 in done/ as the record of the

    reversed decision; registry row removed; #261/#599 notes updated to Kroma).

  • koda#850 (relocate to engines/lang/kavi) → gated (`gated_by:

    owner-decision`), expected to be dropped/replaced by the Kroma-Koda flip ticket.