1use wgpu::util::DeviceExt;
4
5#[repr(C)]
7#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
8pub struct RectInstance {
9 pub rect: [f32; 4],
11 pub color: [f32; 4],
13 pub corner_radius: f32,
15 pub border_width: f32,
17 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#[repr(C)]
46#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
47pub struct RectUniforms {
48 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, 1 => Float32x4, 2 => Float32, 3 => Float32, 4 => Float32x4, ],
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 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"#;