tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 124Markdown 82TOML 78Shell 12149 Sutichaykuna

⎇ main llimphi-ui/examples/stress_boundary.rs Rust · 182 siq'ikuna · 5.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

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}