tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-ui/examples/stress_idle.rs
History
Who touched what
raw
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! stress_idle — **medir antes de tocar** (Iniciativa #1 de `PLAN-MOTOR-PROFUNDO.md`,
2//! tarea 1). El caso que justificaría las damage regions, aislado:
3//!
4//! - un **fondo estático grande** que ocupa toda la ventana y está envuelto en
5//! `.repaint_boundary(1)`, así el `paint` (walk + encode en CPU) ya está
6//! resuelto por el Bloque 23 y no ensucia la medición;
7//! - un **nodo chico que se mueve** cada frame — el spinner/cursor/badge de una
8//! app quieta. Es lo único que cambia de píxeles.
9//!
10//! Lo que queda arriba de la mesa, entonces, es exactamente lo que el damage
11//! atacaría: **`raster` + `present` de la ventana entera para repintar un
12//! cuadradito**. El ejemplo imprime al arrancar qué fracción del área se mueve
13//! de verdad — el techo teórico del ahorro.
14//!
15//! ```bash
16//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_idle --release
17//! # el nodo animado más chico o más grande (por lado, en px):
18//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_idle --release -- --sprite 16
19//! ```
20//!
21//! **El número que importa es en metal.** Acá (llvmpipe / raster por software)
22//! `raster` mide el costo de una CPU rasterizando, no el de una GPU: sirve para
23//! ver que el ejemplo hace lo que dice, no para decidir. La corrida que decide
24//! está en el runbook `[METAL]` de `PLAN-MOTOR-PROFUNDO.md`.
25//!
26//! Y el aviso honesto, para que nadie lea de más: aunque el damage se
27//! implementara, **`vello` re-rasteriza la escena entera** (no expone dirty
28//! rect), así que el ahorro sólo aparecería en canvases GPU-directos densos. Si
29//! esta medición en metal dice que `raster + present` es una fracción chica del
30//! frame, la iniciativa se cierra como "bloqueada por upstream" en vez de
31//! forzarse — que es lo que el plan mismo pide.
32
33use llimphi_ui::llimphi_layout::taffy::prelude::*;
34use llimphi_ui::llimphi_raster::peniko::Color;
35use llimphi_ui::{App, Handle, View};
36use std::time::Duration;
37
38#[derive(Clone)]
39enum Msg {
40 Tick,
41}
42
43struct Idle;
44
45struct Model {
46 frame: u64,
47 /// Lado del nodo animado, en px.
48 sprite: f32,
49 quit_after: u64,
50 renders: std::cell::Cell<u64>,
51}
52
53fn arg_u64(name: &str, default: u64) -> u64 {
54 let mut args = std::env::args();
55 while let Some(a) = args.next() {
56 if a == name {
57 if let Some(v) = args.next() {
58 if let Ok(n) = v.parse::<u64>() {
59 return n;
60 }
61 }
62 }
63 }
64 default
65}
66
67/// El fondo: una grilla de celdas de color fijo que llena la ventana y **no
68/// cambia nunca**. Va dentro de un `repaint_boundary` para que su encode se
69/// cachee tras el primer frame — si no, el `paint` se comería la medición y el
70/// ejemplo estaría midiendo el Bloque 23 en vez del raster.
71fn fondo_estatico() -> View<Msg> {
72 const FILAS: usize = 24;
73 const COLS: usize = 40;
74 let mut filas = Vec::with_capacity(FILAS);
75 for r in 0..FILAS {
76 let mut celdas = Vec::with_capacity(COLS);
77 for c in 0..COLS {
78 // Degradé diagonal reproducible: nada de aleatorio, para que dos
79 // corridas sean comparables byte a byte.
80 let t = ((r * COLS + c) % 255) as u8;
81 celdas.push(
82 View::new(Style {
83 flex_grow: 1.0,
84 size: Size {
85 width: percent(1.0_f32 / COLS as f32),
86 height: percent(1.0_f32),
87 },
88 ..Default::default()
89 })
90 .fill(Color::from_rgba8(20 + t / 8, 24 + t / 6, 40 + t / 4, 255)),
91 );
92 }
93 filas.push(
94 View::new(Style {
95 flex_direction: FlexDirection::Row,
96 flex_grow: 1.0,
97 size: Size {
98 width: percent(1.0_f32),
99 height: percent(1.0_f32 / FILAS as f32),
100 },
101 ..Default::default()
102 })
103 .children(celdas),
104 );
105 }
106 View::new(Style {
107 flex_direction: FlexDirection::Column,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 ..Default::default()
113 })
114 .children(filas)
115 .repaint_boundary(1)
116}
117
118impl App for Idle {
119 type Model = Model;
120 type Msg = Msg;
121
122 fn title() -> &'static str {
123 "llimphi · stress_idle"
124 }
125
126 fn init(handle: &Handle<Self::Msg>) -> Self::Model {
127 let sprite = arg_u64("--sprite", 48).max(1) as f32;
128 let quit_after = arg_u64("--frames", 240);
129 handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick);
130 // El techo del damage, dicho antes de medir nada: contra una ventana de
131 // 1280×720 (el tamaño con el que arranca el runtime), qué porción se
132 // mueve. Es una cota superior — el rect sucio real incluye el sitio
133 // viejo y el nuevo del sprite.
134 let area = (sprite * sprite) / (1280.0 * 720.0) * 100.0;
135 eprintln!(
136 "[stress_idle] sprite={sprite}px — se mueve {area:.3} % del área \
137 (a 1280×720). Todo lo demás es fondo estático cacheado. \
138 LLIMPHI_PERF=1 para el desglose; mirá `raster` y `present`."
139 );
140 Model {
141 frame: 0,
142 sprite,
143 quit_after,
144 renders: std::cell::Cell::new(0),
145 }
146 }
147
148 fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model {
149 match msg {
150 Msg::Tick => {
151 model.frame = model.frame.wrapping_add(1);
152 if model.quit_after != 0 && model.renders.get() >= model.quit_after {
153 handle.quit();
154 }
155 }
156 }
157 model
158 }
159
160 fn view(model: &Self::Model) -> View<Self::Msg> {
161 model.renders.set(model.renders.get() + 1);
162 // El único píxel que cambia: un cuadrado que recorre la ventana en
163 // diagonal. `absolute` para que no reflowee el fondo (si moviera el
164 // layout, la medición dejaría de ser "sólo raster").
165 let t = (model.frame % 200) as f32 / 200.0;
166 let sprite = View::new(Style {
167 position: Position::Absolute,
168 inset: Rect {
169 left: percent(0.05_f32 + t * 0.85),
170 top: percent(0.05_f32 + t * 0.85),
171 right: auto(),
172 bottom: auto(),
173 },
174 size: Size {
175 width: length(model.sprite),
176 height: length(model.sprite),
177 },
178 ..Default::default()
179 })
180 .fill(Color::from_rgba8(240, 180, 60, 255))
181 .radius((model.sprite * 0.25) as f64);
182
183 View::new(Style {
184 size: Size {
185 width: percent(1.0_f32),
186 height: percent(1.0_f32),
187 },
188 ..Default::default()
189 })
190 .fill(Color::from_rgba8(16, 18, 24, 255))
191 .children(vec![fondo_estatico(), sprite])
192 }
193}
194
195fn main() {
196 llimphi_ui::run::<Idle>();
197}