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 widgets/terminal/examples/dump_terminal.rs Rust · 235 siq'ikuna · 8.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Dump headless de la superficie de terminal **virtualizada** (Fase 1).
2//!
3//! Prueba la invariante central del SDD: **1 millón de líneas, costo de render
4//! constante**. Carga 1 M de renglones en el `Scrollback`, ancla el scroll al
5//! fondo (estilo terminal) y renderiza a PNG sólo la ventana visible. Imprime
6//! cuántas filas se materializaron (debe ser ~40, no un millón) — la evidencia
7//! exigida por el SDD: no afirmar paridad/eficiencia sin render + viewport
8//! medido.
9//!
10//! Uso: `cargo run -p llimphi-widget-terminal --example dump_terminal --release [out.png]`
11
12use std::fs::File;
13use std::io::BufWriter;
14
15use llimphi_ui::llimphi_compositor::{measure_text_node, mount, paint};
16use llimphi_ui::llimphi_hal::{wgpu, Hal};
17use llimphi_ui::llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
18use llimphi_ui::llimphi_layout::{taffy, LayoutTree};
19use llimphi_ui::llimphi_raster::peniko::Color;
20use llimphi_ui::llimphi_raster::{vello, Renderer};
21use llimphi_ui::llimphi_text::{Alignment, Typesetter};
22use llimphi_ui::View;
23use llimphi_widget_terminal::{
24    line_surface, scroll_to_bottom, visible_window, LineStyle, Scrollback, TermMetrics,
25    TermPalette,
26};
27
28const W: u32 = 1100;
29const H: u32 = 720;
30const HEADER_H: f32 = 40.0;
31const TOTAL: usize = 1_000_000;
32const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
33
34fn main() {
35    let out = std::env::args()
36        .nth(1)
37        .unwrap_or_else(|| "terminal.png".to_string());
38    let theme = llimphi_theme::Theme::default();
39    let palette = TermPalette::from_theme(&theme);
40    let metrics = TermMetrics::for_font_size(13.0);
41
42    // 1 M de líneas — el scrollback "infinito". Sin cap (limit 0) para que las
43    // numere todas; ~30 MB de texto, acotado y O(1) por la Capa 0.
44    let mut store = Scrollback::new(0);
45    for i in 0..TOTAL {
46        store.push_line(&format!(
47            "fila {i:>7}  ::  lorem ipsum dolor sit amet, payload de salida del comando"
48        ));
49    }
50
51    let viewport_h = H as f32 - HEADER_H;
52    // Anclaje al fondo, como una terminal real tras un flood.
53    let scroll_y = scroll_to_bottom(store.len(), viewport_h, metrics.line_height);
54    let win = visible_window(store.len(), scroll_y, viewport_h, metrics.line_height);
55
56    // Coloreo semántico de muestra inyectado por el "caller": cada 9ª línea
57    // simula stderr (tinte rojo tenue + texto rojo), el resto tinta el prefijo
58    // "fila NNNNNNN" en acento — demuestra runs + bg sin que el widget sepa de
59    // comandos.
60    let accent = theme.accent;
61    let err_fg = theme.fg_destructive;
62    let err_bg = with_alpha(theme.fg_destructive, 0.14);
63    let line_style = move |idx: usize, text: &str| {
64        if idx % 9 == 0 {
65            LineStyle {
66                fg: Some(err_fg),
67                bg: Some(err_bg),
68                ..Default::default()
69            }
70        } else {
71            // Tinta el prefijo "fila NNNNNNN" (hasta el doble espacio).
72            let end = text.find("  ::").unwrap_or(0);
73            LineStyle {
74                runs: vec![(0, end, accent)],
75                ..Default::default()
76            }
77        }
78    };
79
80    let surface = line_surface::<(), _, _>(
81        &store,
82        scroll_y,
83        viewport_h,
84        metrics,
85        &palette,
86        line_style,
87        |_d| (),
88        None,
89    );
90
91    // Header con la evidencia: total vs. filas materializadas.
92    let header = View::new(Style {
93        size: Size {
94            width: percent(1.0_f32),
95            height: length(HEADER_H),
96        },
97        ..Default::default()
98    })
99    .fill(theme.bg_panel_alt)
100    .text_aligned(
101        format!(
102            "scrollback {TOTAL} líneas · materializadas {} (filas {}..{}) · costo constante",
103            win.count(),
104            win.first + 1,
105            win.last,
106        ),
107        13.0,
108        theme.fg_text,
109        Alignment::Start,
110    );
111
112    let root = View::new(Style {
113        flex_direction: FlexDirection::Column,
114        size: Size {
115            width: percent(1.0_f32),
116            height: percent(1.0_f32),
117        },
118        ..Default::default()
119    })
120    .fill(theme.bg_input)
121    .children(vec![header, surface]);
122
123    // Pipeline headless estándar (igual que los dumps de shuma).
124    let mut layout = LayoutTree::new();
125    let mounted = mount(&mut layout, root);
126    let mut ts = Typesetter::new();
127    let computed = {
128        let tmap = &mounted.text_measures;
129        layout
130            .compute_with_measure(mounted.root, (W as f32, H as f32), |nid, known, avail| {
131                match tmap.get(&nid) {
132                    Some(tm) => measure_text_node(&mut ts, tm, known, avail),
133                    None => taffy::Size::ZERO,
134                }
135            })
136            .expect("layout")
137    };
138    let mut scene = vello::Scene::new();
139    paint(&mut scene, &mounted, &computed, &mut ts, None, None);
140
141    let hal = pollster::block_on(Hal::new(None)).expect("hal");
142    let mut renderer = Renderer::new(&hal).expect("renderer");
143    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
144        label: Some("dump-terminal"),
145        size: wgpu::Extent3d {
146            width: W,
147            height: H,
148            depth_or_array_layers: 1,
149        },
150        mip_level_count: 1,
151        sample_count: 1,
152        dimension: wgpu::TextureDimension::D2,
153        format: FMT,
154        usage: wgpu::TextureUsages::STORAGE_BINDING
155            | wgpu::TextureUsages::RENDER_ATTACHMENT
156            | wgpu::TextureUsages::COPY_SRC,
157        view_formats: &[],
158    });
159    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
160    renderer
161        .render_to_view(&hal, &scene, &view, W, H, Color::from_rgba8(18, 18, 24, 255))
162        .expect("render_to_view");
163    write_png(&hal, &target, &out);
164    eprintln!(
165        "dump_terminal: {out} ({W}x{H}) — {TOTAL} líneas, materializadas {} (filas {}..{})",
166        win.count(),
167        win.first + 1,
168        win.last,
169    );
170}
171
172/// Devuelve `c` con la opacidad multiplicada por `alpha`.
173fn with_alpha(c: Color, alpha: f32) -> Color {
174    let rgba = c.to_rgba8();
175    let a = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
176    Color::from_rgba8(rgba.r, rgba.g, rgba.b, a)
177}
178
179fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
180    let unpadded = (W * 4) as usize;
181    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
182    let padded = unpadded.div_ceil(align) * align;
183    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
184        label: Some("readback"),
185        size: (padded * H as usize) as u64,
186        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
187        mapped_at_creation: false,
188    });
189    let mut enc = hal
190        .device
191        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
192    enc.copy_texture_to_buffer(
193        wgpu::TexelCopyTextureInfo {
194            texture: target,
195            mip_level: 0,
196            origin: wgpu::Origin3d::ZERO,
197            aspect: wgpu::TextureAspect::All,
198        },
199        wgpu::TexelCopyBufferInfo {
200            buffer: &buf,
201            layout: wgpu::TexelCopyBufferLayout {
202                offset: 0,
203                bytes_per_row: Some(padded as u32),
204                rows_per_image: Some(H),
205            },
206        },
207        wgpu::Extent3d {
208            width: W,
209            height: H,
210            depth_or_array_layers: 1,
211        },
212    );
213    hal.queue.submit(std::iter::once(enc.finish()));
214    let slice = buf.slice(..);
215    let (tx, rx) = std::sync::mpsc::channel();
216    slice.map_async(wgpu::MapMode::Read, move |r| {
217        let _ = tx.send(r);
218    });
219    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
220    rx.recv().unwrap().unwrap();
221    let data = slice.get_mapped_range();
222    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
223    for row in 0..H as usize {
224        let s = row * padded;
225        pixels.extend_from_slice(&data[s..s + unpadded]);
226    }
227    drop(data);
228    buf.unmap();
229    let file = File::create(path).expect("png");
230    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
231    enc.set_color(png::ColorType::Rgba);
232    enc.set_depth(png::BitDepth::Eight);
233    let mut w = enc.write_header().unwrap();
234    w.write_image_data(&pixels).unwrap();
235}