//! stress_idle — **medir antes de tocar** (Iniciativa #1 de `PLAN-MOTOR-PROFUNDO.md`, //! tarea 1). El caso que justificaría las damage regions, aislado: //! //! - un **fondo estático grande** que ocupa toda la ventana y está envuelto en //! `.repaint_boundary(1)`, así el `paint` (walk + encode en CPU) ya está //! resuelto por el Bloque 23 y no ensucia la medición; //! - un **nodo chico que se mueve** cada frame — el spinner/cursor/badge de una //! app quieta. Es lo único que cambia de píxeles. //! //! Lo que queda arriba de la mesa, entonces, es exactamente lo que el damage //! atacaría: **`raster` + `present` de la ventana entera para repintar un //! cuadradito**. El ejemplo imprime al arrancar qué fracción del área se mueve //! de verdad — el techo teórico del ahorro. //! //! ```bash //! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_idle --release //! # el nodo animado más chico o más grande (por lado, en px): //! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_idle --release -- --sprite 16 //! ``` //! //! **El número que importa es en metal.** Acá (llvmpipe / raster por software) //! `raster` mide el costo de una CPU rasterizando, no el de una GPU: sirve para //! ver que el ejemplo hace lo que dice, no para decidir. La corrida que decide //! está en el runbook `[METAL]` de `PLAN-MOTOR-PROFUNDO.md`. //! //! Y el aviso honesto, para que nadie lea de más: aunque el damage se //! implementara, **`vello` re-rasteriza la escena entera** (no expone dirty //! rect), así que el ahorro sólo aparecería en canvases GPU-directos densos. Si //! esta medición en metal dice que `raster + present` es una fracción chica del //! frame, la iniciativa se cierra como "bloqueada por upstream" en vez de //! forzarse — que es lo que el plan mismo pide. use llimphi_ui::llimphi_layout::taffy::prelude::*; use llimphi_ui::llimphi_raster::peniko::Color; use llimphi_ui::{App, Handle, View}; use std::time::Duration; #[derive(Clone)] enum Msg { Tick, } struct Idle; struct Model { frame: u64, /// Lado del nodo animado, en px. sprite: f32, quit_after: u64, renders: std::cell::Cell, } fn arg_u64(name: &str, default: u64) -> u64 { let mut args = std::env::args(); while let Some(a) = args.next() { if a == name { if let Some(v) = args.next() { if let Ok(n) = v.parse::() { return n; } } } } default } /// El fondo: una grilla de celdas de color fijo que llena la ventana y **no /// cambia nunca**. Va dentro de un `repaint_boundary` para que su encode se /// cachee tras el primer frame — si no, el `paint` se comería la medición y el /// ejemplo estaría midiendo el Bloque 23 en vez del raster. fn fondo_estatico() -> View { const FILAS: usize = 24; const COLS: usize = 40; let mut filas = Vec::with_capacity(FILAS); for r in 0..FILAS { let mut celdas = Vec::with_capacity(COLS); for c in 0..COLS { // Degradé diagonal reproducible: nada de aleatorio, para que dos // corridas sean comparables byte a byte. let t = ((r * COLS + c) % 255) as u8; celdas.push( View::new(Style { flex_grow: 1.0, size: Size { width: percent(1.0_f32 / COLS as f32), height: percent(1.0_f32), }, ..Default::default() }) .fill(Color::from_rgba8(20 + t / 8, 24 + t / 6, 40 + t / 4, 255)), ); } filas.push( View::new(Style { flex_direction: FlexDirection::Row, flex_grow: 1.0, size: Size { width: percent(1.0_f32), height: percent(1.0_f32 / FILAS as f32), }, ..Default::default() }) .children(celdas), ); } View::new(Style { flex_direction: FlexDirection::Column, size: Size { width: percent(1.0_f32), height: percent(1.0_f32), }, ..Default::default() }) .children(filas) .repaint_boundary(1) } impl App for Idle { type Model = Model; type Msg = Msg; fn title() -> &'static str { "llimphi · stress_idle" } fn init(handle: &Handle) -> Self::Model { let sprite = arg_u64("--sprite", 48).max(1) as f32; let quit_after = arg_u64("--frames", 240); handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick); // El techo del damage, dicho antes de medir nada: contra una ventana de // 1280×720 (el tamaño con el que arranca el runtime), qué porción se // mueve. Es una cota superior — el rect sucio real incluye el sitio // viejo y el nuevo del sprite. let area = (sprite * sprite) / (1280.0 * 720.0) * 100.0; eprintln!( "[stress_idle] sprite={sprite}px — se mueve {area:.3} % del área \ (a 1280×720). Todo lo demás es fondo estático cacheado. \ LLIMPHI_PERF=1 para el desglose; mirá `raster` y `present`." ); Model { frame: 0, sprite, quit_after, renders: std::cell::Cell::new(0), } } fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle) -> Self::Model { match msg { Msg::Tick => { model.frame = model.frame.wrapping_add(1); if model.quit_after != 0 && model.renders.get() >= model.quit_after { handle.quit(); } } } model } fn view(model: &Self::Model) -> View { model.renders.set(model.renders.get() + 1); // El único píxel que cambia: un cuadrado que recorre la ventana en // diagonal. `absolute` para que no reflowee el fondo (si moviera el // layout, la medición dejaría de ser "sólo raster"). let t = (model.frame % 200) as f32 / 200.0; let sprite = View::new(Style { position: Position::Absolute, inset: Rect { left: percent(0.05_f32 + t * 0.85), top: percent(0.05_f32 + t * 0.85), right: auto(), bottom: auto(), }, size: Size { width: length(model.sprite), height: length(model.sprite), }, ..Default::default() }) .fill(Color::from_rgba8(240, 180, 60, 255)) .radius((model.sprite * 0.25) as f64); View::new(Style { size: Size { width: percent(1.0_f32), height: percent(1.0_f32), }, ..Default::default() }) .fill(Color::from_rgba8(16, 18, 24, 255)) .children(vec![fondo_estatico(), sprite]) } } fn main() { llimphi_ui::run::(); }