tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ main llimphi-ui/examples/gpu_paint_demo.rs Rust · 395 líneas · 13.2 KB Historia Quién tocó qué crudo

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

1//! Demo del hook GPU directo (`View::gpu_paint_with`) — Fase 1 del SDD
2//! `02_ruway/llimphi/SDD.md` §"GPU directo wgpu".
3//!
4//! Pinta una grilla de N puntos coloridos sobre un panel central usando
5//! un pipeline `wgpu` propio (instanced quad), encima de un fondo y
6//! títulos pintados por vello. Valida que:
7//!
8//! - El callback `gpu_paint_with` recibe `(device, queue, encoder,
9//!   view, rect)` con los recursos del runtime.
10//! - El `LoadOp::Load` preserva la pasada vello (el fondo no se borra).
11//! - El submit del encoder ocurre antes del `surface.present` (las
12//!   primitivas GPU son visibles).
13//!
14//! Corre con: `cargo run -p llimphi-ui --example gpu_paint_demo --release`.
15
16use std::sync::{Arc, OnceLock};
17
18use llimphi_ui::llimphi_hal::wgpu;
19use llimphi_ui::llimphi_layout::taffy::{
20    prelude::{auto, length, percent, FlexDirection, Size, Style},
21    AlignItems, JustifyContent, Rect as TaffyRect,
22};
23use llimphi_ui::llimphi_raster::peniko::Color;
24use llimphi_ui::{App, Handle, PaintRect, View};
25
26const POINTS: u32 = 250_000;
27const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
28
29#[derive(Clone)]
30enum Msg {
31    Bump,
32}
33
34struct GpuDemo;
35
36impl App for GpuDemo {
37    type Model = u32;
38    type Msg = Msg;
39
40    fn title() -> &'static str {
41        "llimphi · gpu_paint_demo"
42    }
43
44    fn init(_: &Handle<Self::Msg>) -> Self::Model {
45        0
46    }
47
48    fn update(model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
49        match msg {
50            Msg::Bump => model.wrapping_add(1),
51        }
52    }
53
54    fn view(model: &Self::Model) -> View<Self::Msg> {
55        let title = View::new(Style {
56            size: Size {
57                width: percent(1.0_f32),
58                height: length(48.0_f32),
59            },
60            justify_content: Some(JustifyContent::CENTER),
61            align_items: Some(AlignItems::CENTER),
62            ..Default::default()
63        })
64        .text(
65            format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
66            22.0,
67            Color::from_rgba8(220, 230, 245, 255),
68        );
69
70        // Canvas central: vello pinta el fondo (fill + radius), GPU pinta
71        // la grilla de puntos encima vía gpu_paint_with. El seed del
72        // modelo se mete en el shader vía una rotación trivial — cada
73        // click cambia el patrón. El callback se invoca ya con el
74        // CommandEncoder del frame y la TextureView intermediate.
75        let seed = *model;
76        let canvas = View::new(Style {
77            size: Size {
78                width: percent(1.0_f32),
79                height: auto(),
80            },
81            flex_grow: 1.0,
82            ..Default::default()
83        })
84        .fill(Color::from_rgba8(14, 18, 28, 255))
85        .radius(8.0)
86        .gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
87            draw_points(device, queue, encoder, view, rect, seed);
88        })
89        .on_click(Msg::Bump);
90
91        let footer = View::new(Style {
92            size: Size {
93                width: percent(1.0_f32),
94                height: length(28.0_f32),
95            },
96            justify_content: Some(JustifyContent::CENTER),
97            align_items: Some(AlignItems::CENTER),
98            ..Default::default()
99        })
100        .text(
101            "click sobre el canvas → rebobinar el seed",
102            14.0,
103            Color::from_rgba8(150, 165, 185, 255),
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            gap: Size {
113                width: length(0.0_f32),
114                height: length(16.0_f32),
115            },
116            padding: TaffyRect {
117                left: length(24.0_f32),
118                right: length(24.0_f32),
119                top: length(16.0_f32),
120                bottom: length(16.0_f32),
121            },
122            ..Default::default()
123        })
124        .fill(Color::from_rgba8(24, 28, 38, 255))
125        .children(vec![title, canvas, footer])
126    }
127}
128
129fn main() {
130    llimphi_ui::run::<GpuDemo>();
131}
132
133// ============================================================
134// Lado GPU del demo: pipeline + buffer + draw call.
135// ============================================================
136
137/// Estado compartido del demo a través de los frames. Se construye en
138/// el primer `gpu_paint_with` (cuando ya tenemos device/queue) y se
139/// reutiliza después. Sin esto pagaríamos creación de pipeline + write
140/// del buffer por frame, que es lo que `GpuBatch` resolverá de raíz en
141/// Fase 3.
142struct DemoGpu {
143    pipeline: wgpu::RenderPipeline,
144    instances: wgpu::Buffer,
145    uniforms: wgpu::Buffer,
146    bind_group: wgpu::BindGroup,
147}
148
149fn shared() -> &'static OnceLock<Arc<DemoGpu>> {
150    static SLOT: OnceLock<Arc<DemoGpu>> = OnceLock::new();
151    &SLOT
152}
153
154fn draw_points(
155    device: &wgpu::Device,
156    queue: &wgpu::Queue,
157    encoder: &mut wgpu::CommandEncoder,
158    view: &wgpu::TextureView,
159    rect: PaintRect,
160    seed: u32,
161) {
162    let gpu = shared()
163        .get_or_init(|| Arc::new(DemoGpu::new(device)))
164        .clone();
165
166    // Uniforms: rect + seed → el VS los usa para colocar y colorear.
167    let uniforms = [rect.x, rect.y, rect.w, rect.h, f32::from_bits(seed), 0.0, 0.0, 0.0];
168    let mut bytes = Vec::with_capacity(32);
169    for v in uniforms {
170        bytes.extend_from_slice(&v.to_ne_bytes());
171    }
172    queue.write_buffer(&gpu.uniforms, 0, &bytes);
173
174    {
175        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
176            label: Some("gpu_paint_demo-pass"),
177            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
178                view,
179                resolve_target: None,
180                depth_slice: None,
181                ops: wgpu::Operations {
182                    // Load preserva el fondo vello ya pintado en este frame.
183                    load: wgpu::LoadOp::Load,
184                    store: wgpu::StoreOp::Store,
185                },
186            })],
187            depth_stencil_attachment: None,
188            timestamp_writes: None,
189            occlusion_query_set: None,
190            multiview_mask: None,
191        });
192        pass.set_pipeline(&gpu.pipeline);
193        pass.set_bind_group(0, &gpu.bind_group, &[]);
194        pass.set_vertex_buffer(0, gpu.instances.slice(..));
195        pass.draw(0..6, 0..POINTS);
196    }
197}
198
199impl DemoGpu {
200    fn new(device: &wgpu::Device) -> Self {
201        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
202            label: Some("gpu_paint_demo-shader"),
203            source: wgpu::ShaderSource::Wgsl(WGSL.into()),
204        });
205
206        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
207            label: Some("gpu_paint_demo-bgl"),
208            entries: &[wgpu::BindGroupLayoutEntry {
209                binding: 0,
210                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
211                ty: wgpu::BindingType::Buffer {
212                    ty: wgpu::BufferBindingType::Uniform,
213                    has_dynamic_offset: false,
214                    min_binding_size: None,
215                },
216                count: None,
217            }],
218        });
219
220        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
221            label: Some("gpu_paint_demo-pl"),
222            bind_group_layouts: &[Some(&bind_layout)],
223            immediate_size: 0,
224        });
225
226        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
227            label: Some("gpu_paint_demo-pipe"),
228            layout: Some(&pipeline_layout),
229            vertex: wgpu::VertexState {
230                module: &shader,
231                entry_point: Some("vs"),
232                compilation_options: Default::default(),
233                buffers: &[wgpu::VertexBufferLayout {
234                    array_stride: 4,
235                    step_mode: wgpu::VertexStepMode::Instance,
236                    attributes: &[wgpu::VertexAttribute {
237                        format: wgpu::VertexFormat::Uint32,
238                        offset: 0,
239                        shader_location: 0,
240                    }],
241                }],
242            },
243            primitive: wgpu::PrimitiveState {
244                topology: wgpu::PrimitiveTopology::TriangleList,
245                strip_index_format: None,
246                front_face: wgpu::FrontFace::Ccw,
247                cull_mode: None,
248                unclipped_depth: false,
249                polygon_mode: wgpu::PolygonMode::Fill,
250                conservative: false,
251            },
252            depth_stencil: None,
253            multisample: wgpu::MultisampleState::default(),
254            fragment: Some(wgpu::FragmentState {
255                module: &shader,
256                entry_point: Some("fs"),
257                compilation_options: Default::default(),
258                targets: &[Some(wgpu::ColorTargetState {
259                    format: TARGET_FORMAT,
260                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
261                    write_mask: wgpu::ColorWrites::ALL,
262                })],
263            }),
264            multiview_mask: None,
265            cache: None,
266        });
267
268        // Instance buffer: índice 0..POINTS empaquetado como u32.
269        let mut idx_bytes = Vec::with_capacity((POINTS as usize) * 4);
270        for i in 0..POINTS {
271            idx_bytes.extend_from_slice(&i.to_ne_bytes());
272        }
273        let instances = device.create_buffer(&wgpu::BufferDescriptor {
274            label: Some("gpu_paint_demo-inst"),
275            size: idx_bytes.len() as u64,
276            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
277            mapped_at_creation: false,
278        });
279        // El buffer ya vive el resto del programa — escribimos una vez.
280        // Para esto necesitamos el queue, pero `new` no lo recibe. Lo
281        // mantenemos como "lazy escrito en draw_points la primera vez";
282        // por simplicidad lo escribimos en el primer queue.write_buffer
283        // del flujo de uniforms. Actualmente el shader no usa la
284        // instancia (sólo @builtin(vertex_index) + uniforms + builtin
285        // instance_index), así que el buffer es ignorado — lo dejamos
286        // para que el layout del pipeline siga válido y el día que
287        // queramos meter datos por instancia ya está el slot listo.
288        let _ = idx_bytes; // (no se sube — ver comentario arriba)
289
290        let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
291            label: Some("gpu_paint_demo-u"),
292            size: 32,
293            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
294            mapped_at_creation: false,
295        });
296
297        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
298            label: Some("gpu_paint_demo-bg"),
299            layout: &bind_layout,
300            entries: &[wgpu::BindGroupEntry {
301                binding: 0,
302                resource: uniforms.as_entire_binding(),
303            }],
304        });
305
306        Self {
307            pipeline,
308            instances,
309            uniforms,
310            bind_group,
311        }
312    }
313}
314
315// Hash 32-bit barato (PCG-like) implementado en WGSL para mapear
316// `instance_index + seed` → posición/color sin tocar buffers. Mantiene
317// el demo en una sola draw call con cero CPU work por frame (salvo
318// 32 bytes de uniforms).
319const WGSL: &str = r#"
320struct Uniforms {
321    rect:   vec4<f32>, // x, y, w, h en pixels del frame
322    seed:   u32,
323    _pad0:  u32,
324    _pad1:  u32,
325    _pad2:  u32,
326};
327
328@group(0) @binding(0) var<uniform> u: Uniforms;
329
330struct V2F {
331    @builtin(position) pos: vec4<f32>,
332    @location(0) color: vec4<f32>,
333};
334
335fn hash(x: u32) -> u32 {
336    var v = x ^ 2747636419u;
337    v = v * 2654435769u;
338    v = v ^ (v >> 16u);
339    v = v * 2654435769u;
340    v = v ^ (v >> 16u);
341    v = v * 2654435769u;
342    return v;
343}
344
345// La resolución real del frame no la conoce el shader sin un uniform
346// adicional. Como aproximación robusta, asumimos que el callback se
347// llama sobre un viewport "default" 960×540 (tamaño inicial del demo)
348// y dejamos que rect.x/y/w/h centren los puntos dentro del canvas.
349// El tamaño real del frame se debería pasar por uniforms en una versión
350// no-demo — Fase 2/3 del SDD lo formaliza vía `GpuBatch`.
351const FRAME_W: f32 = 960.0;
352const FRAME_H: f32 = 540.0;
353
354@vertex
355fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> V2F {
356    var corners = array<vec2<f32>, 6>(
357        vec2<f32>(-1.0, -1.0),
358        vec2<f32>( 1.0, -1.0),
359        vec2<f32>( 1.0,  1.0),
360        vec2<f32>(-1.0, -1.0),
361        vec2<f32>( 1.0,  1.0),
362        vec2<f32>(-1.0,  1.0),
363    );
364    let off = corners[vid] * 1.5; // quad de 3 pixels lado
365
366    let h1 = hash(iid ^ u.seed);
367    let h2 = hash(h1);
368    let h3 = hash(h2);
369
370    let fx = f32(h1 & 0xFFFFu) / 65535.0;
371    let fy = f32(h2 & 0xFFFFu) / 65535.0;
372
373    let px = u.rect.x + fx * u.rect.z + off.x;
374    let py = u.rect.y + fy * u.rect.w + off.y;
375
376    let ndc = vec2<f32>(
377        px / FRAME_W * 2.0 - 1.0,
378        1.0 - py / FRAME_H * 2.0,
379    );
380
381    let r = f32( h3        & 0xFFu) / 255.0;
382    let g = f32((h3 >>  8u) & 0xFFu) / 255.0;
383    let b = f32((h3 >> 16u) & 0xFFu) / 255.0;
384
385    var out: V2F;
386    out.pos = vec4<f32>(ndc, 0.0, 1.0);
387    out.color = vec4<f32>(r, g, b, 0.85);
388    return out;
389}
390
391@fragment
392fn fs(in: V2F) -> @location(0) vec4<f32> {
393    return in.color;
394}
395"#;