use std::sync::{Arc, OnceLock};
use llimphi_ui::llimphi_hal::wgpu;
use llimphi_ui::llimphi_layout::taffy::{
prelude::{auto, length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent, Rect as TaffyRect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::{App, Handle, PaintRect, View};
const POINTS: u32 = 250_000;
const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
#[derive(Clone)]
enum Msg {
Bump,
}
struct GpuDemo;
impl App for GpuDemo {
type Model = u32;
type Msg = Msg;
fn title() -> &'static str {
"llimphi · gpu_paint_demo"
}
fn init(_: &Handle<Self::Msg>) -> Self::Model {
0
}
fn update(model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
match msg {
Msg::Bump => model.wrapping_add(1),
}
}
fn view(model: &Self::Model) -> View<Self::Msg> {
let title = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(48.0_f32),
},
justify_content: Some(JustifyContent::Center),
align_items: Some(AlignItems::Center),
..Default::default()
})
.text(
format!("gpu_paint_with — {POINTS} puntos GPU directo · seed {model}"),
22.0,
Color::from_rgba8(220, 230, 245, 255),
);
let seed = *model;
let canvas = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: auto(),
},
flex_grow: 1.0,
..Default::default()
})
.fill(Color::from_rgba8(14, 18, 28, 255))
.radius(8.0)
.gpu_paint_with(move |device, queue, encoder, view, rect, _viewport| {
draw_points(device, queue, encoder, view, rect, seed);
})
.on_click(Msg::Bump);
let footer = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(28.0_f32),
},
justify_content: Some(JustifyContent::Center),
align_items: Some(AlignItems::Center),
..Default::default()
})
.text(
"click sobre el canvas → rebobinar el seed",
14.0,
Color::from_rgba8(150, 165, 185, 255),
);
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
gap: Size {
width: length(0.0_f32),
height: length(16.0_f32),
},
padding: TaffyRect {
left: length(24.0_f32),
right: length(24.0_f32),
top: length(16.0_f32),
bottom: length(16.0_f32),
},
..Default::default()
})
.fill(Color::from_rgba8(24, 28, 38, 255))
.children(vec![title, canvas, footer])
}
}
fn main() {
llimphi_ui::run::<GpuDemo>();
}
struct DemoGpu {
pipeline: wgpu::RenderPipeline,
instances: wgpu::Buffer,
uniforms: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
fn shared() -> &'static OnceLock<Arc<DemoGpu>> {
static SLOT: OnceLock<Arc<DemoGpu>> = OnceLock::new();
&SLOT
}
fn draw_points(
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
rect: PaintRect,
seed: u32,
) {
let gpu = shared()
.get_or_init(|| Arc::new(DemoGpu::new(device)))
.clone();
let uniforms = [rect.x, rect.y, rect.w, rect.h, f32::from_bits(seed), 0.0, 0.0, 0.0];
let mut bytes = Vec::with_capacity(32);
for v in uniforms {
bytes.extend_from_slice(&v.to_ne_bytes());
}
queue.write_buffer(&gpu.uniforms, 0, &bytes);
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("gpu_paint_demo-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&gpu.pipeline);
pass.set_bind_group(0, &gpu.bind_group, &[]);
pass.set_vertex_buffer(0, gpu.instances.slice(..));
pass.draw(0..6, 0..POINTS);
}
}
impl DemoGpu {
fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("gpu_paint_demo-shader"),
source: wgpu::ShaderSource::Wgsl(WGSL.into()),
});
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("gpu_paint_demo-bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("gpu_paint_demo-pl"),
bind_group_layouts: &[&bind_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("gpu_paint_demo-pipe"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[wgpu::VertexBufferLayout {
array_stride: 4,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Uint32,
offset: 0,
shader_location: 0,
}],
}],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: TARGET_FORMAT,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
cache: None,
});
let mut idx_bytes = Vec::with_capacity((POINTS as usize) * 4);
for i in 0..POINTS {
idx_bytes.extend_from_slice(&i.to_ne_bytes());
}
let instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("gpu_paint_demo-inst"),
size: idx_bytes.len() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let _ = idx_bytes;
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("gpu_paint_demo-u"),
size: 32,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("gpu_paint_demo-bg"),
layout: &bind_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
}],
});
Self {
pipeline,
instances,
uniforms,
bind_group,
}
}
}
const WGSL: &str = r#"
struct Uniforms {
rect: vec4<f32>, // x, y, w, h en pixels del frame
seed: u32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct V2F {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
};
fn hash(x: u32) -> u32 {
var v = x ^ 2747636419u;
v = v * 2654435769u;
v = v ^ (v >> 16u);
v = v * 2654435769u;
v = v ^ (v >> 16u);
v = v * 2654435769u;
return v;
}
// La resolución real del frame no la conoce el shader sin un uniform
// adicional. Como aproximación robusta, asumimos que el callback se
// llama sobre un viewport "default" 960×540 (tamaño inicial del demo)
// y dejamos que rect.x/y/w/h centren los puntos dentro del canvas.
// El tamaño real del frame se debería pasar por uniforms en una versión
// no-demo — Fase 2/3 del SDD lo formaliza vía `GpuBatch`.
const FRAME_W: f32 = 960.0;
const FRAME_H: f32 = 540.0;
@vertex
fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> V2F {
var corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, 1.0),
);
let off = corners[vid] * 1.5; // quad de 3 pixels lado
let h1 = hash(iid ^ u.seed);
let h2 = hash(h1);
let h3 = hash(h2);
let fx = f32(h1 & 0xFFFFu) / 65535.0;
let fy = f32(h2 & 0xFFFFu) / 65535.0;
let px = u.rect.x + fx * u.rect.z + off.x;
let py = u.rect.y + fy * u.rect.w + off.y;
let ndc = vec2<f32>(
px / FRAME_W * 2.0 - 1.0,
1.0 - py / FRAME_H * 2.0,
);
let r = f32( h3 & 0xFFu) / 255.0;
let g = f32((h3 >> 8u) & 0xFFu) / 255.0;
let b = f32((h3 >> 16u) & 0xFFu) / 255.0;
var out: V2F;
out.pos = vec4<f32>(ndc, 0.0, 1.0);
out.color = vec4<f32>(r, g, b, 0.85);
return out;
}
@fragment
fn fs(in: V2F) -> @location(0) vec4<f32> {
return in.color;
}
"#;