Files
llimphi/SDD.md
T
sergio e65e9cc623 feat: llimphi standalone — framework UI soberano extraído del monorepo
Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:23:42 +00:00

367 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Llimphi — motor gráfico soberano
> Llimphi (quechua: *color / brillo / pigmento*, en el sentido de "pintar la pantalla"). Tipo: **NATIVE GPU rendering suite**.
> **Regla dura para apps:** nada de cómputo pesado síncrono en `App::update`/`init`/handlers — congela la UI ("Not Responding"). Ver [COMPUTO-FUERA-DEL-HILO-UI.md](COMPUTO-FUERA-DEL-HILO-UI.md) (patrón worker + checklist por app, prioridad urgente).
> **¿Buscás cómo *usar* Llimphi?** Este SDD es el *porqué* (diseño, fases, roadmap). La referencia de *uso* — bucle Elm, DSL `View<Msg>`, catálogo de widgets/módulos, GPU directo — está en [MANUAL.md](MANUAL.md), verificada contra el código.
## Tesis
Soberanía total sobre el píxel. Renderizar las geometrías exactas del simulador cósmico (`cosmos`), el compositor (`mirada`), las apps de escritorio (`nahual`) y el visor (`pluma`) sin cajas negras de Apple/Google/navegadores. Reemplazo total de **GPUI** en la pila gioser.
## Anatomía — 4 capas estrictas (S₀ → S₂)
Cada capa hace **una sola cosa** con precisión matemática.
```
[ CUADRANTE III · 0x02 RUWAY ]
4. llimphi-ui — Lógica de Interfaz (Árbol Monádico / DAG UI)
│ (manejo de estado, eventos de teclado/ratón)
3. llimphi-layout — Motor de Layout (Cálculo Espacial)
│ (cajas, dimensiones, restricciones flex/grid)
2. llimphi-raster — Rasterizador Vectorial (La Brocha Fina)
│ (primitivas matemáticas → píxeles via Compute Shaders)
1. llimphi-hal — Abstracción de Hardware (Puente al Silicio)
│ (GPU o Framebuffer, sin importar el OS)
[ HARDWARE · GPU / Pantalla ]
```
## Fases de forja
### Fase 1 — Puente al Silicio (`llimphi-hal`)
Aislar el motor del sistema operativo. Llimphi debe pintar tanto en una ventana Wayland controlada por `mirada` como en el framebuffer directo al arrancar `wawa`.
- **Abstractor:** `wgpu` (impl Rust de WebGPU sobre Vulkan nativo). Control de memoria seguro, bajísima sobrecarga.
- **Ventana:** `winit` para desarrollo en Linux. La arquitectura define un **trait `Surface`** abstracto: el día de mañana se desenchufa `winit` y se le pasa el puntero de memoria bruto del kernel `wawa`.
- **Hito:** Compilar, iniciar Vulkan por debajo, limpiar la pantalla pintándola de un solo color gris plomo a 144 Hz.
### Fase 2 — Brocha Matemática (`llimphi-raster`)
Pintar curvas y grafos orbitales con precisión Δ < 10⁻⁹ rad sin destrozar la CPU. En lugar de rasterizar píxel por píxel, **delegar todo el cálculo vectorial a los Compute Shaders de la GPU**.
- **Motor:** `vello`.
- **Integración:** Conectar la textura de salida de `wgpu` como lienzo destino de `vello`.
- **Ejecución:** Construir una `Scene` en `vello`. Pasarle primitivas geométricas puras (líneas, curvas de Bézier, texto).
- **Hito:** Renderizar en pantalla el grafo de un nodo estático con anti-aliasing perfecto calculado íntegramente por la GPU.
### Fase 3 — Física del Espacio (`llimphi-layout`)
Posicionar dinámicamente paneles, texto y ventanas requiere resolver ecuaciones de restricciones espaciales. No escribir un sistema propio de márgenes/padding: es un sumidero infinito.
- **Motor:** `taffy` (de la gente de Dioxus). Algoritmos Flexbox + CSS Grid en Rust puro.
- **Flujo:** Antes de decirle a `llimphi-raster` dónde pintar, pasar el árbol de nodos a `taffy` para calcular las coordenadas `(x, y, width, height)` absolutas de toda la interfaz.
- **Hito:** Paneles laterales y cajas que se redimensionan automáticamente, calculados en < 1 ms por frame.
### Fase 4 — Árbol de Estado Monádico (`llimphi-ui`)
El mayor problema de las interfaces (y por qué falló el paradigma OOP en esto) es el manejo del estado. Aquí se inyecta la cosmovisión estructural.
- **Arquitectura:** Nada de mutabilidad compartida (`Rc<RefCell<...>>` disperso). Unidireccional estilo Elm o **DAG (Grafo Acíclico Dirigido)**: el estado de la aplicación es **inmutable** y cada evento (click, tecla) genera una **nueva versión** del estado.
- **Bucle:**
1. El usuario hace click (Input).
2. El evento actualiza el Estado Global.
3. El Estado Global reconstruye el Árbol UI.
4. El Árbol pasa por `llimphi-layout` (Layout).
5. Las coordenadas resultantes generan primitivas para `llimphi-raster` (Scene).
6. `llimphi-hal` renderiza y hace el swap de la pantalla.
## Veredicto arquitectónico
No es una biblioteca genérica. Es un **motor de combate**. `wgpu + vello + taffy + DAG monádico` da un frontend capaz de competir en rendimiento con los mejores editores del mundo, diseñado como **traje a medida** para las topologías de gioser. Sin abstracciones de navegadores, sin cajas negras de Apple/Google.
## Pila exacta (sin negociación)
| Capa | Crate raíz | Deps externas |
|---|---|---|
| HAL | `llimphi-hal` | `wgpu`, `winit`, `raw-window-handle` |
| Raster | `llimphi-raster` | `vello`, `vello_encoding`, `peniko` |
| Text | `llimphi-text` | `parley` (shaping + fontique + swash, hereda vello via raster) |
| Layout | `llimphi-layout` | `taffy` |
| UI | `llimphi-ui` | `llimphi-{hal,raster,layout,text}` |
## Migración GPUI → Llimphi
Apps actualmente en GPUI que deben portarse:
- `02_ruway/nahual/*` (todas las apps GPUI: shell, file-explorer, database-explorer, image-viewer, text-viewer + 8 libs + 12 widgets)
- `02_ruway/mirada/mirada-launcher`, `mirada-portal`, `mirada-greeter`
- `00_unanchay/pluma/pluma-editor-gpui`
- `01_yachay/dominium/dominium-canvas-gpui`
- `01_yachay/cosmos/cosmos-app` (canvas + panels GPUI)
**Estrategia:** Las apps mantienen su lógica de dominio en sus `*-core` agnósticos. Solo se reemplaza la capa de presentación: en lugar de `use gpui::*`, pasan a usar `use llimphi_ui::*`.
## Estado (2026-05-31)
### Hecho
- Las 5 capas del framework en producción: `llimphi-hal` (wgpu+winit), `llimphi-raster` (vello), `llimphi-text` (parley, ahora con vello directo y texto multicolor en una pasada), `llimphi-layout` (taffy, con `LayoutTree::clear()` para reuso entre frames), `llimphi-ui` (bucle Elm + runtime winit).
- Split compositor/runtime: `llimphi-compositor` (winit-free: View tree, mount, paint/paint_gpu, hit-test) separado de `llimphi-ui` (runtime winit) → habilita un futuro runtime sobre el framebuffer de `wawa` sin winit.
- GPUI extinto (2026-05-26): toda app gráfica de la suite corre sobre Llimphi.
- Backend GPU directo (sin vello) completo y validado en hardware real (Iris Xe): `GpuPipelines` + `GpuBatch` + `View::gpu_paint_with`; ~11× vs vello a 1M puntos persistente, >140 fps.
- Catálogo de ~44 widgets: incluye text-editor (split en `-core` agnóstico + `-lsp`), nodegraph, tiled/panes/splitter, tree, list, grid (virtualizada 2D), gallery, timeline (scrub clickeable), menubar/edit-menu/context-menu, clipboard del sistema, tabs, modal, toast, y la familia de controles (button/field/slider/switch/segmented/...).
- 10 módulos compuestos: command-palette, diff-viewer, fif (find-in-files), file-picker, bookmarks, mini-map, shuma-term, symbol-outline, selector, plugin-host.
- `llimphi-workspace` (chasis tipo tmux) + `llimphi-gallery` (showcase) + `llimphi-motion`/`llimphi-icons`/`llimphi-surface` auxiliares.
### Pendiente
- Runtime sobre framebuffer de `wawa` (`WawaFramebufferSurface`) reusando el compositor winit-free — habilitado por el split pero aún no escrito.
- Backend GPU directo: sin MSAA/AA fino, sin texto, una sola `line_width` por flush; falta primer caller real denso (cosmos starfield) que mida una falla concreta antes de extender shaders.
- Widgets `llimphi-widget-{transport, waveform}` aún por extraer (la nota de media los deja como futuro no bloqueante).
- Investigación abierta: cuelgue/deadlock de apps Llimphi tras click/scroll (hipótesis `get_current_texture` Wayland FIFO) — pendiente reproducir+backtrace.
## Estado — bitácora histórica
- **2026-05-25:** SDD escrito. Esqueletos de los 4 crates creados.
- **2026-05-25 (tarde):** Las 4 fases en código y compilando. Examples:
- `cargo run -p llimphi-hal --example clear_screen --release` — ventana gris plomo a refresh del display ✅ (verificado en hardware).
- `cargo run -p llimphi-raster --example render_node --release` — nodo con AA perfecto vía vello/wgpu.
- `cargo run -p llimphi-layout --example layout_panels --release` — sidebar + header/body/footer flex que se reorganiza al resize.
- `cargo run -p llimphi-ui --example counter --release` — bucle Elm completo: click hit-test → update → view → layout → raster → present.
- **2026-05-25 (noche):** quinto crate `llimphi-text` (skrifa + vello). Bug de `max_storage_buffers_per_shader_stage` corregido (`Limits::default()` en vez de `downlevel`). `View::text()` permite poner texto centrado en cualquier nodo. Examples:
- `cargo run -p llimphi-text --example hello_text --release` — "Llimphi" + tagline sobre fondo negro.
- `counter` ahora muestra el número real (no barras) y los botones llevan label.
- **2026-05-25 (cierre):** dos fixes de hardware + parley.
- **Storage write fix:** swapchain de muchos adapters Linux/Vulkan no acepta storage writes en Rgba8Unorm. Patrón nuevo: textura intermedia con `STORAGE_BINDING | TEXTURE_BINDING` donde pinta vello + `TextureBlitter` que la copia al swapchain en `Surface::present(frame, &hal)`. Cambio de API: `frame.present()``surface.present(frame, &hal)`.
- **Paint-order fix:** `mount_recursive` registraba en post-orden y el background del root tapaba a los hijos. Ahora pre-orden depth-first.
- **Parley:** llimphi-text reescrito sobre parley. API nueva: `Typesetter` (cachea FontContext + LayoutContext), `TextBlock { text, size_px, color, origin, max_width, alignment, line_height }`, `Alignment { Start, Center, End, Justify }`, `measure(&mut ts, &block)`. Bidi + ligatures + fallback CJK/emoji vía fontique. `hello_text` muestra título + párrafo justificado con script mixto Latin/Arabic/CJK.
- **2026-05-25 (cierre+1):** teclado en `llimphi-ui`. `App` gana `fn on_key(model, &KeyEvent) -> Option<Msg>` con default `None`. Re-export `Key` y `NamedKey` de winit. Runtime mantiene `Modifiers` state vía `ModifiersChanged`. `TextSpec` gana `alignment` (default `Center`, los labels de botón siguen igual) + `View::text_aligned(...)`. Example nuevo `editor`: text field con char insertion, backspace, enter, tab→4-spaces, ctrl+L limpia.
- **2026-05-26:** migración GPUI → Llimphi **completada**. GPUI queda extinto: toda app gráfica de la suite (pluma, mirada, cosmos, dominium, nahual, iniy, khipu, chasqui…) corre sobre Llimphi. No se agrega código nuevo sobre GPUI (ver regla dura §3 de `CLAUDE.md`).
- **2026-05-31:** split de `llimphi-widget-text-editor` (4328 LOC) → núcleo agnóstico `llimphi-widget-text-editor-core` (buffer/cursor/ops/undo/bracket/find/diagnostics/clipboard/highlight, sin render: sólo `peniko::Color`) + widget Llimphi (state + view) que lo re-exporta. Núcleo reutilizable en TUI/web/headless. `LayoutTree::clear()` para reusar el árbol taffy entre frames (`llimphi-layout`).
- **2026-05-31 (texto multicolor):** syntax highlighting en una sola pasada de shaping. `llimphi-text` gana `RunBrush` + `Typesetter::layout_runs` (color por rango de bytes vía `parley::RangedBuilder`/`StyleProperty::Brush`) + `draw_layout_runs`; `View::text_runs` lo expone. El editor pasó de un nodo (+ layout parley) por token a uno por línea.
- **2026-05-31 (split compositor/runtime):** `llimphi-ui` (1943 LOC) partido para separar la composición declarativa del runtime winit:
- **`llimphi-compositor`** (nuevo, **winit-free**): el árbol `View<Msg>`, `mount` sobre taffy, `paint`/`paint_gpu` a `vello::Scene` y el hit-test. Depende sólo de `llimphi-layout` + `llimphi-text` + `vello` + `wgpu` (este último sólo por la firma de `GpuPaintFn`; `wgpu` no es windowing). **No depende de `llimphi-hal`.**
- **`llimphi-ui`**: queda como el runtime winit (`App`/`Handle`/`run`/event loop/`KeyEvent`) y re-exporta el compositor entero → los consumidores siguen usando `llimphi_ui::View` etc. sin cambios.
- Prerrequisito habilitado: `llimphi-text` ahora depende de `vello` directo (no de `llimphi-raster`), así que la pila de render (`compositor``text`/`vello`) es winit-free. Eso abre la puerta a un runtime sobre el framebuffer del kernel `wawa` (`WawaFramebufferSurface`) que reuse el mismo compositor sin arrastrar winit. `Renderer` (lo único que necesita `llimphi-hal`) se queda en `llimphi-raster`, consumido por `llimphi-ui`.
## Roadmap — GPU directo wgpu (sin vello)
### Por qué
`llimphi-raster` traduce hoy todo a `vello::Scene` (BezPath / kurbo /
peniko) y vello rasteriza vía compute shaders. Para 99 % de la suite
sobra: pluma editor, shuma shell, mirada compositor, nahual, iniy, khipu,
chasqui explorer, etc. pintan decenas a centenas de primitivos por frame.
El techo aparece cuando una app necesita rendir **>1 M primitivos por
frame**. En ese régimen el overhead de construir `BezPath`, ensamblar
buffers para los shaders internos de vello y hacer una pasada compute
por cada batch domina sobre el tiempo de raster real. Casos concretos
en gioser:
| App | Carga potencial | Trigger probable |
|---|---|---|
| **cosmos** | Catálogo Gaia DR3, mapas de cielo enteros | Starfield denso o sky-survey overlay |
| **tinkuy** | Particle engine N→∞ por diseño | Sim con > 10⁵ partículas |
| **nakui** | 100 K filas × 26 cols = 2.6 M celdas potencialmente visibles | Viewport con dataset grande |
| **dominium** | Mean-field con N agentes | Cuando se pase de 10³ a 10⁵ |
| **pineal** | Sus painters ya producen `Vec<f32>` interleaved (principio P1) — son los primeros listos para consumir el backend | Cualquiera de los anteriores que use pineal-* |
El techo es **horizontal**. Resolverlo en cualquier app individual sería
duplicación; el lugar es el motor.
### Qué es
Un backend alternativo en `llimphi-raster` que **salta vello** y sube
los slices de coordenadas directamente a vertex buffers `wgpu`, dispara
shaders WGSL chiquitos y emite una draw call por batch.
```
hoy: painter → vello::Scene → BezPath → vello → wgpu → GPU
con esto: painter → GpuBatch → vertex buffer → wgpu → GPU
```
El trait que ven las apps (`Canvas` para pineal, `View::paint_with` para
llimphi-ui) **no cambia**. Cambia el implementador por debajo cuando se
elige "modo GPU directo".
### Trade-offs vs vello
| | Vello (hoy) | GPU directo |
|---|---|---|
| AA | Analítico, perfecto | MSAA hardware o supersample en shader |
| Curvas suaves | Bezier nativo | Hay que teselar primero |
| Texto | Sí, vello + parley | No — usar vello para text aunque coexista |
| Throughput primitivos | Bueno hasta ~100 K | Apto para 110 M |
| Costo de mantener | Cero (vello lo mantiene Linebender) | Shaders WGSL + pipelines propias |
Decisión: los dos backends **coexisten**. La app elige por hint
(`View::gpu_paint_with` para denso, `paint_with` para todo lo demás).
### Plan de tareas
**Fase 0 — Spike de medición (½ día). ✓ HECHO (2026-05-28).**
Benchmark sintético: pintar 100 K, 500 K y 1 M puntos con `SceneCanvas`
actual vs un mock GPU-directo (vertex buffer + shader trivial). Si el
factor no es ≥ 5× en el rango de 500 K, abortar — vello ya es
suficiente y no vale el costo de mantenimiento. Métrica de éxito: 60 fps
con 1 M puntos en GPU mid (Radeon 5500M, Intel Iris Xe).
Implementado en `llimphi-raster/examples/spike_gpu_directo.rs`. Cubre
ambos backends contra una textura `Rgba8Unorm` 1024×1024 headless,
warmup 5 + 15 frames medidos, bloquea hasta GPU idle (`Maintain::Wait`)
para que los `ms` reportados sean tiempo real CPU+GPU.
El binario `llimphi-gpu-bench` (en su propio crate) reporta info del
adapter wgpu + corre dos escenarios distintos: **rebuild por frame**
(LCG + `write_buffer` de 12-160 MB por frame, peor caso) y
**persistente** (buffer/Scene preparados UNA vez, bucle medido sólo
emite la draw call — caso real de cosmos/tinkuy/nakui).
**Resultados — Intel Iris Xe (TGL GT2), Mesa 26.1.1, Vulkan, 2026-05-28:**
Rebuild por frame:
| N | vello ms | directo ms | factor |
|---:|---:|---:|---:|
| 25K | 7.3 | 1.2 | **6.05×** |
| 50K | 12.9 | 1.4 | **8.94×** |
| 100K | 21.7 | 3.2 | **6.67×** |
| 200K | 26.1 | 6.1 | 4.30× |
| 500K | 94.4 | 18.0 | **5.25×** |
| 1M | 202.4 | 49.0 | 4.13× |
Persistente (datos fijos, sólo redraw):
| N | vello ms | directo ms | factor | fps directo |
|---:|---:|---:|---:|---:|
| 100K | 18.6 | 0.8 | **22.55×** | 1210 |
| 500K | 34.1 | 3.4 | **9.97×** | 293 |
| 1M | 83.1 | 7.1 | **11.76×** | 141 |
| 2M | 101.7 | 16.0 | **6.37×** | 63 |
| 5M | crash | 41.8 | — | 24 |
| 10M | crash | 79.7 | — | 13 |
Veredictos contra el criterio del SDD:
- **Factor ≥5× a 500K**: ✓ PASA. Rebuild 5.25×, persistente 9.97×.
- **≥60 fps @ 1M**: ✓ PASA en persistente (141 fps); falla en rebuild
(22 fps) — pero rebuild no es el use case real.
- **Techo de vello**: ~2 M paths en GPU mid. Más alto que mi hipótesis
inicial (que era 200300 K, contaminada por llvmpipe), pero existe.
El path directo escala lineal a >10 M sin crashes.
Conclusión: el GPU directo cumple su propósito. La diferencia entre
rebuild y persistente (520×) confirma que el patrón correcto es
"datos cambian → vello, datos estáticos → GPU directo persistente".
**Fase 1 — Hook en `llimphi-ui` (12 días).**
Hoy `View::paint_with(F)` da
`F: Fn(&mut vello::Scene, &mut Typesetter, PaintRect)`. Agregar:
```rust
View::gpu_paint_with(F)
where F: Fn(&wgpu::Device, &wgpu::Queue,
&mut wgpu::CommandEncoder,
&wgpu::TextureView, PaintRect)
```
El runtime de llimphi-ui ya tiene `Device`/`Queue` para vello; sólo hay
que exponer el `CommandEncoder` y `TextureView` del frame durante el
mount/paint. Compatibilidad: ambos hooks coexisten en el mismo View
tree; el orden de pintura sigue siendo pre-orden DFS.
**Fase 2 — Pipelines y shaders en `llimphi-raster` (35 días).**
Tres pipelines WGSL precompiladas y cacheadas:
- `lines_pipeline` — line list, anchura uniforme (expandida a tris en
vertex shader como hace pineal-export::png).
- `tris_pipeline` — triangle list con per-vertex color.
- `rects_pipeline` — instanced quad con per-instance `[x, y, w, h, color]`.
Vertex format común: `[x: f32, y: f32, rgba: u32]`. Sin texturas; eso
queda para una fase posterior si aparece demanda.
**Fase 3 — `GpuBatch` accumulator (23 días).**
Estructura que las apps usan dentro del callback:
```rust
let mut batch = GpuBatch::new(device);
batch.add_lines(&coords, color);
batch.add_tris(&coords, &colors);
batch.add_rect(rect, color);
batch.flush(encoder, view); // 1 draw call por pipeline usada
```
Grow strategy: vertex buffer dobla capacidad cada vez que se queda
chico. Sin copy back — vive del frame, se reusa el siguiente.
**Fase 4 — `GpuSceneCanvas` en pineal-render (1 día).**
Wrapper que implementa el trait `Canvas` de pineal usando `GpuBatch`
por debajo. Cero cambios en los painters. Permite usar el catálogo
entero de pineal en modo denso simplemente eligiendo el otro
constructor de Canvas dentro del `gpu_paint_with`.
**Fase 5 — Primer caller real (cosmos starfield, 23 días).**
Adaptar `cosmos-canvas-llimphi` para subir todas las estrellas del
viewport en una draw call usando `gpu_paint_with`. Métrica: dataset
HYG (~120 K estrellas brillantes) renderizadas a 144 fps en GPU mid.
**Fase 6 — Tests + demo + SDD (1 día). ✓ HECHO (2026-05-28).**
- `llimphi-raster/examples/gpu_million_points.rs`: usa `GpuPipelines` +
`GpuBatch` puros (sin app, sin runtime Elm) para pintar N rects
sintéticos. Validación headless del HAL + bench de referencia
post-implementación. Smoke en `tests/gpu_batch_smoke.rs`.
- Tabla "cuándo elegir" → abajo.
- Pineal SDD §4 actualizado con `GpuSceneCanvas` en producción.
### ¿Cuándo elegir vello vs GPU directo?
| Pregunta | Vello (`paint_with`) | GPU directo (`gpu_paint_with`) |
|---|---|---|
| ¿Cuántos primitivos por frame? | < ~500 K (rebuild) o < ~2 M (Scene reusada) | 100 K 10 M+ |
| ¿Los datos cambian cada frame? | Sí — vello rebuild es barato hasta 500 K | Posible pero con coste de `write_buffer`; ideal estático |
| ¿Curvas Bezier nativas? | Sí | No (teselar antes) |
| ¿Texto? | Sí | No — usar vello hermano u overlay |
| ¿AA fino requerido? | Sí (analítico) | No (sin MSAA todavía) |
| ¿Múltiples grosores de stroke? | Sí | Una sola `line_width` por flush |
| ¿Anti-fluctuación de pixel? | Sí | Subpixel jitter visible |
| Ejemplos de uso | pluma editor, shuma shell, mirada, nahual, iniy, khipu, chasqui explorer, dominium UI | cosmos starfield denso, tinkuy particles, nakui viewport, pineal denso |
Default razonable: **`paint_with`** salvo que el caller ya midió que el
volumen lo justifica. El costo de mantener un pipeline + WGSL propios
es alto comparado con seguir usando vello.
Patrón "buffer persistente": para el use case denso real (catálogo
fijo, particles iniciales, dataset estático), construir el
`wgpu::Buffer` y `BindGroup` UNA vez con `GpuPipelines::{rects, tris,
lines, bind_layout}` expuestos y emitir el draw call manualmente
desde el `gpu_paint_with` reusando esos recursos. Eso da factores
~11× vs vello a 1M en GPU mid (medido Iris Xe), y >140 fps.
`GpuBatch` queda para datos transitorios (UI dinámica densa).
Convivencia: una misma `View` puede registrar AMBOS hooks. El runtime
pinta vello primero (toda la Scene), luego ejecuta los GPU painters
en orden DFS. Para texto encima de un render GPU denso, se usa
`App::view_overlay` (segunda Scene vello sobre el main).
**Estimado total: 1015 días de trabajo concentrado.**
**Trabajo real (1 día, 2026-05-28):** todas las fases completas, sólo
falta validar el criterio formal (≥5× a 500K, 60 fps @ 1M) en GPU mid
real — el bench corrió en llvmpipe.
### Trigger
No empezar hasta tener un caller real que mida una falla concreta.
El candidato natural es cosmos (starfield Gaia o sky-survey overlay).
Hasta entonces, el item queda acá en este SDD como decisión arquitectónica
tomada — todas las apps saben que el techo existe y que la salida
está diseñada.
### No-objetivos explícitos
- **No** reemplazar vello. Coexisten — vello para vector/text/AA fino,
GPU directo para volumen.
- **No** hacer un layer de abstracción tipo Skia. El trait `Canvas` de
pineal y el `paint_with` de llimphi son la abstracción; no se agrega
más arriba.
- **No** soportar texto en el backend GPU directo. Texto siempre por
vello+parley; si una vista mezcla millones de puntos + labels, hace
`gpu_paint_with` para los puntos y un `paint_with` superpuesto para
los labels.