1use 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 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
133struct 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 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: 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 });
191 pass.set_pipeline(&gpu.pipeline);
192 pass.set_bind_group(0, &gpu.bind_group, &[]);
193 pass.set_vertex_buffer(0, gpu.instances.slice(..));
194 pass.draw(0..6, 0..POINTS);
195 }
196}
197
198impl DemoGpu {
199 fn new(device: &wgpu::Device) -> Self {
200 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
201 label: Some("gpu_paint_demo-shader"),
202 source: wgpu::ShaderSource::Wgsl(WGSL.into()),
203 });
204
205 let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
206 label: Some("gpu_paint_demo-bgl"),
207 entries: &[wgpu::BindGroupLayoutEntry {
208 binding: 0,
209 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
210 ty: wgpu::BindingType::Buffer {
211 ty: wgpu::BufferBindingType::Uniform,
212 has_dynamic_offset: false,
213 min_binding_size: None,
214 },
215 count: None,
216 }],
217 });
218
219 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
220 label: Some("gpu_paint_demo-pl"),
221 bind_group_layouts: &[&bind_layout],
222 push_constant_ranges: &[],
223 });
224
225 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
226 label: Some("gpu_paint_demo-pipe"),
227 layout: Some(&pipeline_layout),
228 vertex: wgpu::VertexState {
229 module: &shader,
230 entry_point: Some("vs"),
231 compilation_options: Default::default(),
232 buffers: &[wgpu::VertexBufferLayout {
233 array_stride: 4,
234 step_mode: wgpu::VertexStepMode::Instance,
235 attributes: &[wgpu::VertexAttribute {
236 format: wgpu::VertexFormat::Uint32,
237 offset: 0,
238 shader_location: 0,
239 }],
240 }],
241 },
242 primitive: wgpu::PrimitiveState {
243 topology: wgpu::PrimitiveTopology::TriangleList,
244 strip_index_format: None,
245 front_face: wgpu::FrontFace::Ccw,
246 cull_mode: None,
247 unclipped_depth: false,
248 polygon_mode: wgpu::PolygonMode::Fill,
249 conservative: false,
250 },
251 depth_stencil: None,
252 multisample: wgpu::MultisampleState::default(),
253 fragment: Some(wgpu::FragmentState {
254 module: &shader,
255 entry_point: Some("fs"),
256 compilation_options: Default::default(),
257 targets: &[Some(wgpu::ColorTargetState {
258 format: TARGET_FORMAT,
259 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
260 write_mask: wgpu::ColorWrites::ALL,
261 })],
262 }),
263 multiview: None,
264 cache: None,
265 });
266
267 let mut idx_bytes = Vec::with_capacity((POINTS as usize) * 4);
269 for i in 0..POINTS {
270 idx_bytes.extend_from_slice(&i.to_ne_bytes());
271 }
272 let instances = device.create_buffer(&wgpu::BufferDescriptor {
273 label: Some("gpu_paint_demo-inst"),
274 size: idx_bytes.len() as u64,
275 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
276 mapped_at_creation: false,
277 });
278 let _ = idx_bytes; let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
290 label: Some("gpu_paint_demo-u"),
291 size: 32,
292 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
293 mapped_at_creation: false,
294 });
295
296 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
297 label: Some("gpu_paint_demo-bg"),
298 layout: &bind_layout,
299 entries: &[wgpu::BindGroupEntry {
300 binding: 0,
301 resource: uniforms.as_entire_binding(),
302 }],
303 });
304
305 Self {
306 pipeline,
307 instances,
308 uniforms,
309 bind_group,
310 }
311 }
312}
313
314const WGSL: &str = r#"
319struct Uniforms {
320 rect: vec4<f32>, // x, y, w, h en pixels del frame
321 seed: u32,
322 _pad0: u32,
323 _pad1: u32,
324 _pad2: u32,
325};
326
327@group(0) @binding(0) var<uniform> u: Uniforms;
328
329struct V2F {
330 @builtin(position) pos: vec4<f32>,
331 @location(0) color: vec4<f32>,
332};
333
334fn hash(x: u32) -> u32 {
335 var v = x ^ 2747636419u;
336 v = v * 2654435769u;
337 v = v ^ (v >> 16u);
338 v = v * 2654435769u;
339 v = v ^ (v >> 16u);
340 v = v * 2654435769u;
341 return v;
342}
343
344// La resolución real del frame no la conoce el shader sin un uniform
345// adicional. Como aproximación robusta, asumimos que el callback se
346// llama sobre un viewport "default" 960×540 (tamaño inicial del demo)
347// y dejamos que rect.x/y/w/h centren los puntos dentro del canvas.
348// El tamaño real del frame se debería pasar por uniforms en una versión
349// no-demo — Fase 2/3 del SDD lo formaliza vía `GpuBatch`.
350const FRAME_W: f32 = 960.0;
351const FRAME_H: f32 = 540.0;
352
353@vertex
354fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> V2F {
355 var corners = array<vec2<f32>, 6>(
356 vec2<f32>(-1.0, -1.0),
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 );
363 let off = corners[vid] * 1.5; // quad de 3 pixels lado
364
365 let h1 = hash(iid ^ u.seed);
366 let h2 = hash(h1);
367 let h3 = hash(h2);
368
369 let fx = f32(h1 & 0xFFFFu) / 65535.0;
370 let fy = f32(h2 & 0xFFFFu) / 65535.0;
371
372 let px = u.rect.x + fx * u.rect.z + off.x;
373 let py = u.rect.y + fy * u.rect.w + off.y;
374
375 let ndc = vec2<f32>(
376 px / FRAME_W * 2.0 - 1.0,
377 1.0 - py / FRAME_H * 2.0,
378 );
379
380 let r = f32( h3 & 0xFFu) / 255.0;
381 let g = f32((h3 >> 8u) & 0xFFu) / 255.0;
382 let b = f32((h3 >> 16u) & 0xFFu) / 255.0;
383
384 var out: V2F;
385 out.pos = vec4<f32>(ndc, 0.0, 1.0);
386 out.color = vec4<f32>(r, g, b, 0.85);
387 return out;
388}
389
390@fragment
391fn fs(in: V2F) -> @location(0) vec4<f32> {
392 return in.color;
393}
394"#;