Skip to main content

beyonder_gpu/
pipeline.rs

1//! wgpu render pipeline for rectangles (the primitive for all block UI).
2
3use wgpu::util::DeviceExt;
4
5/// A single rectangle draw command.
6#[repr(C)]
7#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
8pub struct RectInstance {
9    /// [x, y, width, height] in screen pixels.
10    pub rect: [f32; 4],
11    /// RGBA fill color.
12    pub color: [f32; 4],
13    /// Corner radius in pixels (0 = sharp).
14    pub corner_radius: f32,
15    /// Border width in pixels (0 = no border).
16    pub border_width: f32,
17    /// RGBA border color.
18    pub border_color: [f32; 4],
19}
20
21impl RectInstance {
22    pub fn filled(x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) -> Self {
23        Self {
24            rect: [x, y, w, h],
25            color,
26            corner_radius: 0.0,
27            border_width: 0.0,
28            border_color: [0.0; 4],
29        }
30    }
31
32    pub fn with_border(mut self, width: f32, color: [f32; 4]) -> Self {
33        self.border_width = width;
34        self.border_color = color;
35        self
36    }
37
38    pub fn with_radius(mut self, radius: f32) -> Self {
39        self.corner_radius = radius;
40        self
41    }
42}
43
44/// Global push constants for the rect pipeline (screen size for NDC conversion).
45#[repr(C)]
46#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
47pub struct RectUniforms {
48    /// Screen dimensions [width, height].
49    pub screen_size: [f32; 2],
50    pub _pad: [f32; 2],
51}
52
53pub struct RectPipeline {
54    pub pipeline: wgpu::RenderPipeline,
55    pub uniform_buffer: wgpu::Buffer,
56    pub uniform_bind_group: wgpu::BindGroup,
57    pub instance_buffer: wgpu::Buffer,
58    pub instance_capacity: u32,
59}
60
61const MAX_RECTS: u32 = 8192;
62
63impl RectPipeline {
64    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
65        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
66            label: Some("rect_shader"),
67            source: wgpu::ShaderSource::Wgsl(RECT_SHADER.into()),
68        });
69
70        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
71            label: Some("rect_uniforms"),
72            contents: bytemuck::bytes_of(&RectUniforms {
73                screen_size: [800.0, 600.0],
74                _pad: [0.0; 2],
75            }),
76            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
77        });
78
79        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
80            label: Some("rect_bgl"),
81            entries: &[wgpu::BindGroupLayoutEntry {
82                binding: 0,
83                visibility: wgpu::ShaderStages::VERTEX,
84                ty: wgpu::BindingType::Buffer {
85                    ty: wgpu::BufferBindingType::Uniform,
86                    has_dynamic_offset: false,
87                    min_binding_size: None,
88                },
89                count: None,
90            }],
91        });
92
93        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
94            label: Some("rect_bg"),
95            layout: &bind_group_layout,
96            entries: &[wgpu::BindGroupEntry {
97                binding: 0,
98                resource: uniform_buffer.as_entire_binding(),
99            }],
100        });
101
102        let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
103            label: Some("rect_instances"),
104            size: (std::mem::size_of::<RectInstance>() as u64) * MAX_RECTS as u64,
105            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
106            mapped_at_creation: false,
107        });
108
109        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
110            label: Some("rect_pipeline_layout"),
111            bind_group_layouts: &[&bind_group_layout],
112            push_constant_ranges: &[],
113        });
114
115        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
116            label: Some("rect_pipeline"),
117            layout: Some(&pipeline_layout),
118            vertex: wgpu::VertexState {
119                module: &shader,
120                entry_point: Some("vs_main"),
121                buffers: &[wgpu::VertexBufferLayout {
122                    array_stride: std::mem::size_of::<RectInstance>() as u64,
123                    step_mode: wgpu::VertexStepMode::Instance,
124                    attributes: &wgpu::vertex_attr_array![
125                        0 => Float32x4,  // rect
126                        1 => Float32x4,  // color
127                        2 => Float32,    // corner_radius
128                        3 => Float32,    // border_width
129                        4 => Float32x4,  // border_color
130                    ],
131                }],
132                compilation_options: wgpu::PipelineCompilationOptions::default(),
133            },
134            fragment: Some(wgpu::FragmentState {
135                module: &shader,
136                entry_point: Some("fs_main"),
137                targets: &[Some(wgpu::ColorTargetState {
138                    format: surface_format,
139                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
140                    write_mask: wgpu::ColorWrites::ALL,
141                })],
142                compilation_options: wgpu::PipelineCompilationOptions::default(),
143            }),
144            primitive: wgpu::PrimitiveState {
145                topology: wgpu::PrimitiveTopology::TriangleStrip,
146                ..Default::default()
147            },
148            depth_stencil: None,
149            multisample: wgpu::MultisampleState::default(),
150            multiview: None,
151            cache: None,
152        });
153
154        Self {
155            pipeline,
156            uniform_buffer,
157            uniform_bind_group,
158            instance_buffer,
159            instance_capacity: MAX_RECTS,
160        }
161    }
162
163    pub fn update_screen_size(&self, queue: &wgpu::Queue, width: f32, height: f32) {
164        queue.write_buffer(
165            &self.uniform_buffer,
166            0,
167            bytemuck::bytes_of(&RectUniforms {
168                screen_size: [width, height],
169                _pad: [0.0; 2],
170            }),
171        );
172    }
173
174    pub fn upload_instances(&self, queue: &wgpu::Queue, instances: &[RectInstance]) {
175        if instances.is_empty() {
176            return;
177        }
178        queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances));
179    }
180
181    pub fn draw<'rp>(&'rp self, pass: &mut wgpu::RenderPass<'rp>, count: u32) {
182        if count == 0 {
183            return;
184        }
185        pass.set_pipeline(&self.pipeline);
186        pass.set_bind_group(0, &self.uniform_bind_group, &[]);
187        pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
188        // 4 vertices per quad (triangle strip), 1 instance per rect
189        pass.draw(0..4, 0..count);
190    }
191}
192
193const RECT_SHADER: &str = r#"
194struct Uniforms {
195    screen_size: vec2<f32>,
196    _pad: vec2<f32>,
197};
198@group(0) @binding(0) var<uniform> u: Uniforms;
199
200struct Instance {
201    @location(0) rect: vec4<f32>,          // x, y, w, h in pixels
202    @location(1) color: vec4<f32>,
203    @location(2) corner_radius: f32,
204    @location(3) border_width: f32,
205    @location(4) border_color: vec4<f32>,
206};
207
208struct Vs {
209    @builtin(position) pos: vec4<f32>,
210    @location(0) uv: vec2<f32>,
211    @location(1) color: vec4<f32>,
212    @location(2) size: vec2<f32>,
213    @location(3) corner_radius: f32,
214    @location(4) border_width: f32,
215    @location(5) border_color: vec4<f32>,
216};
217
218@vertex
219fn vs_main(@builtin(vertex_index) vi: u32, inst: Instance) -> Vs {
220    // Triangle strip: 4 vertices of a quad
221    let uv = vec2<f32>(f32(vi & 1u), f32((vi >> 1u) & 1u));
222    let px = inst.rect.x + uv.x * inst.rect.z;
223    let py = inst.rect.y + uv.y * inst.rect.w;
224    // Convert pixel coords to NDC (Y flipped)
225    let ndc = vec2<f32>(
226        (px / u.screen_size.x) * 2.0 - 1.0,
227        1.0 - (py / u.screen_size.y) * 2.0,
228    );
229    var v: Vs;
230    v.pos = vec4<f32>(ndc, 0.0, 1.0);
231    v.uv = uv;
232    v.color = inst.color;
233    v.size = inst.rect.zw;
234    v.corner_radius = inst.corner_radius;
235    v.border_width = inst.border_width;
236    v.border_color = inst.border_color;
237    return v;
238}
239
240@fragment
241fn fs_main(v: Vs) -> @location(0) vec4<f32> {
242    // Rounded corners via SDF
243    let half = v.size * 0.5;
244    let p = v.uv * v.size - half;
245    let r = v.corner_radius;
246    let q = abs(p) - half + vec2<f32>(r, r);
247    let dist = length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
248    if dist > 1.0 { discard; }
249
250    // Border
251    if v.border_width > 0.0 && dist > -v.border_width {
252        return v.border_color;
253    }
254
255    return v.color;
256}
257"#;