tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! stress_boundary — Bloque 23 (RepaintBoundary). Contraparte de `stress_grid`:
2//! el mismo grid grande, pero **estático** y envuelto en `.repaint_boundary(1)`,
3//! con un header que SÍ cambia cada frame (fuerza el repintado del frame).
4//!
5//! Con el boundary, el subárbol estático se rasteriza una vez y se **reusa**
6//! (append de la sub-escena cacheada) — así el `paint` (walk + encode en CPU,
7//! la etapa dominante a muchos nodos) se desploma, mientras el header se pinta
8//! fresco. `LLIMPHI_PERF` muestra `boundary N hits / 1 miss`.
9//!
10//! ```bash
11//! # sin caché (todo se re-encodea cada frame):
12//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 50000
13//! # con caché (el grid estático se reusa):
14//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_boundary --release -- --nodes 50000
15//! ```
16//! Comparás la etapa `paint µs`: mucho más baja en stress_boundary.
17
18use llimphi_ui::llimphi_layout::taffy::prelude::*;
19use llimphi_ui::llimphi_raster::peniko::Color;
20use llimphi_ui::{App, Handle, View};
21use std::time::Duration;
22
23#[derive(Clone)]
24enum Msg {
25 Tick,
26}
27
28struct Stress;
29
30struct Model {
31 frame: u64,
32 nodes: usize,
33 cols: usize,
34 quit_after: u64,
35 /// Frames REALMENTE renderizados (incrementado en `view`). El auto-quit se
36 /// basa en esto, no en los Ticks: a muchos nodos el render es más lento que
37 /// el tick de 16 ms, y contar Ticks cerraría la app antes de que el
38 /// perfilador (que reporta cada N frames renderizados) imprima una línea.
39 renders: std::cell::Cell<u64>,
40}
41
42fn arg_u64(name: &str, default: u64) -> u64 {
43 let mut args = std::env::args();
44 while let Some(a) = args.next() {
45 if a == name {
46 if let Some(v) = args.next() {
47 if let Ok(n) = v.parse::<u64>() {
48 return n;
49 }
50 }
51 }
52 }
53 default
54}
55
56/// El grid **estático** (colores fijos por índice, NO por frame): idéntico entre
57/// frames → cache-hit del RepaintBoundary tras el primero.
58fn grid_estatico(nodes: usize, cols: usize) -> View<Msg> {
59 let rows = nodes.div_ceil(cols);
60 let mut row_views = Vec::with_capacity(rows);
61 let mut made = 0usize;
62 for _r in 0..rows {
63 let mut cells = Vec::with_capacity(cols);
64 for _c in 0..cols {
65 if made >= nodes {
66 break;
67 }
68 let hue = (made % 255) as u8;
69 cells.push(
70 View::new(Style {
71 size: Size {
72 width: length(6.0_f32),
73 height: length(6.0_f32),
74 },
75 ..Default::default()
76 })
77 .fill(Color::from_rgba8(hue, 90, 200u8.wrapping_sub(hue), 255))
78 .radius(1.0),
79 );
80 made += 1;
81 }
82 row_views.push(
83 View::new(Style {
84 flex_direction: FlexDirection::Row,
85 gap: Size {
86 width: length(1.0_f32),
87 height: length(0.0_f32),
88 },
89 ..Default::default()
90 })
91 .children(cells),
92 );
93 }
94 View::new(Style {
95 flex_direction: FlexDirection::Column,
96 gap: Size {
97 width: length(0.0_f32),
98 height: length(1.0_f32),
99 },
100 ..Default::default()
101 })
102 .children(row_views)
103 // ← el límite de repintado: subárbol estático, cacheable.
104 .repaint_boundary(1)
105}
106
107impl App for Stress {
108 type Model = Model;
109 type Msg = Msg;
110
111 fn title() -> &'static str {
112 "llimphi · stress_boundary"
113 }
114
115 fn init(handle: &Handle<Self::Msg>) -> Self::Model {
116 let nodes = arg_u64("--nodes", 5_000).max(1) as usize;
117 let cols = (nodes as f64).sqrt().ceil() as usize;
118 let quit_after = arg_u64("--frames", 240);
119 handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick);
120 eprintln!(
121 "[stress_boundary] nodes={nodes} cols={cols} — LLIMPHI_PERF=1; \
122 el grid va en un repaint_boundary (cache-hit tras el 1er frame)"
123 );
124 Model {
125 frame: 0,
126 nodes,
127 cols,
128 quit_after,
129 renders: std::cell::Cell::new(0),
130 }
131 }
132
133 fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model {
134 match msg {
135 Msg::Tick => {
136 model.frame = model.frame.wrapping_add(1);
137 if model.quit_after != 0 && model.renders.get() >= model.quit_after {
138 handle.quit();
139 }
140 }
141 }
142 model
143 }
144
145 fn view(model: &Self::Model) -> View<Self::Msg> {
146 model.renders.set(model.renders.get() + 1);
147 // Header que CAMBIA cada frame → obliga a repintar el frame (si no,
148 // la retención de frame entero saltearía todo). Va FUERA del boundary.
149 let header = View::new(Style {
150 size: Size {
151 width: percent(1.0_f32),
152 height: length(28.0_f32),
153 },
154 ..Default::default()
155 })
156 .fill(Color::from_rgba8(30, 34, 44, 255))
157 .text(
158 format!("frame {}", model.frame),
159 16.0,
160 Color::from_rgba8(210, 214, 222, 255),
161 );
162
163 View::new(Style {
164 flex_direction: FlexDirection::Column,
165 size: Size {
166 width: percent(1.0_f32),
167 height: percent(1.0_f32),
168 },
169 gap: Size {
170 width: length(0.0_f32),
171 height: length(4.0_f32),
172 },
173 ..Default::default()
174 })
175 .fill(Color::from_rgba8(16, 18, 24, 255))
176 .children(vec![header, grid_estatico(model.nodes, model.cols)])
177 }
178}
179
180fn main() {
181 llimphi_ui::run::<Stress>();
182}