1use crate::capability::{GpuRequirements, RenderCapability};
11use crate::transform::{DataTransform, TransformUniform};
12use crate::{Color, GraphicsContext, Viewport};
13use astrelis_core::profiling::profile_scope;
14use bytemuck::{Pod, Zeroable};
15use glam::Vec2;
16use std::sync::Arc;
17use wgpu::util::DeviceExt;
18
19#[derive(Debug, Clone, Copy)]
25pub struct Quad {
26 pub min: Vec2,
28 pub max: Vec2,
30 pub color: Color,
32}
33
34impl Quad {
35 pub fn new(min: Vec2, max: Vec2, color: Color) -> Self {
36 Self { min, max, color }
37 }
38
39 pub fn from_center(center: Vec2, width: f32, height: f32, color: Color) -> Self {
41 let half = Vec2::new(width * 0.5, height * 0.5);
42 Self {
43 min: center - half,
44 max: center + half,
45 color,
46 }
47 }
48
49 pub fn bar(x_center: f32, width: f32, y_bottom: f32, y_top: f32, color: Color) -> Self {
51 Self {
52 min: Vec2::new(x_center - width * 0.5, y_bottom),
53 max: Vec2::new(x_center + width * 0.5, y_top),
54 color,
55 }
56 }
57}
58
59#[repr(C)]
61#[derive(Debug, Clone, Copy, Pod, Zeroable)]
62struct QuadInstance {
63 min: [f32; 2],
64 max: [f32; 2],
65 color: [f32; 4],
66}
67
68impl QuadInstance {
69 fn new(quad: &Quad) -> Self {
70 Self {
71 min: [quad.min.x, quad.min.y],
72 max: [quad.max.x, quad.max.y],
73 color: [quad.color.r, quad.color.g, quad.color.b, quad.color.a],
74 }
75 }
76}
77
78impl RenderCapability for QuadRenderer {
79 fn requirements() -> GpuRequirements {
80 GpuRequirements::none()
81 }
82
83 fn name() -> &'static str {
84 "QuadRenderer"
85 }
86}
87
88pub struct QuadRenderer {
95 context: Arc<GraphicsContext>,
96 pipeline: wgpu::RenderPipeline,
97 vertex_buffer: wgpu::Buffer,
98 transform_buffer: wgpu::Buffer,
99 transform_bind_group: wgpu::BindGroup,
100 instance_buffer: Option<wgpu::Buffer>,
101 instance_count: u32,
102 pending_quads: Vec<Quad>,
104 data_dirty: bool,
106}
107
108impl QuadRenderer {
109 pub fn new(context: Arc<GraphicsContext>, target_format: wgpu::TextureFormat) -> Self {
114 let transform_buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
116 label: Some("Quad Renderer Transform Buffer"),
117 size: std::mem::size_of::<TransformUniform>() as u64,
118 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
119 mapped_at_creation: false,
120 });
121
122 let bind_group_layout =
124 context
125 .device()
126 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
127 label: Some("Quad Renderer Bind Group Layout"),
128 entries: &[wgpu::BindGroupLayoutEntry {
129 binding: 0,
130 visibility: wgpu::ShaderStages::VERTEX,
131 ty: wgpu::BindingType::Buffer {
132 ty: wgpu::BufferBindingType::Uniform,
133 has_dynamic_offset: false,
134 min_binding_size: None,
135 },
136 count: None,
137 }],
138 });
139
140 let transform_bind_group = context
141 .device()
142 .create_bind_group(&wgpu::BindGroupDescriptor {
143 label: Some("Quad Renderer Transform Bind Group"),
144 layout: &bind_group_layout,
145 entries: &[wgpu::BindGroupEntry {
146 binding: 0,
147 resource: transform_buffer.as_entire_binding(),
148 }],
149 });
150
151 let shader = context
153 .device()
154 .create_shader_module(wgpu::ShaderModuleDescriptor {
155 label: Some("Quad Renderer Shader"),
156 source: wgpu::ShaderSource::Wgsl(QUAD_SHADER.into()),
157 });
158
159 let pipeline_layout =
161 context
162 .device()
163 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
164 label: Some("Quad Renderer Pipeline Layout"),
165 bind_group_layouts: &[&bind_group_layout],
166 push_constant_ranges: &[],
167 });
168
169 let pipeline = context
170 .device()
171 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
172 label: Some("Quad Renderer Pipeline"),
173 layout: Some(&pipeline_layout),
174 vertex: wgpu::VertexState {
175 module: &shader,
176 entry_point: Some("vs_main"),
177 buffers: &[
178 wgpu::VertexBufferLayout {
180 array_stride: 8,
181 step_mode: wgpu::VertexStepMode::Vertex,
182 attributes: &[wgpu::VertexAttribute {
183 format: wgpu::VertexFormat::Float32x2,
184 offset: 0,
185 shader_location: 0,
186 }],
187 },
188 wgpu::VertexBufferLayout {
190 array_stride: std::mem::size_of::<QuadInstance>() as u64,
191 step_mode: wgpu::VertexStepMode::Instance,
192 attributes: &[
193 wgpu::VertexAttribute {
194 format: wgpu::VertexFormat::Float32x2,
195 offset: 0,
196 shader_location: 1,
197 },
198 wgpu::VertexAttribute {
199 format: wgpu::VertexFormat::Float32x2,
200 offset: 8,
201 shader_location: 2,
202 },
203 wgpu::VertexAttribute {
204 format: wgpu::VertexFormat::Float32x4,
205 offset: 16,
206 shader_location: 3,
207 },
208 ],
209 },
210 ],
211 compilation_options: wgpu::PipelineCompilationOptions::default(),
212 },
213 fragment: Some(wgpu::FragmentState {
214 module: &shader,
215 entry_point: Some("fs_main"),
216 targets: &[Some(wgpu::ColorTargetState {
217 format: target_format,
218 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
219 write_mask: wgpu::ColorWrites::ALL,
220 })],
221 compilation_options: wgpu::PipelineCompilationOptions::default(),
222 }),
223 primitive: wgpu::PrimitiveState {
224 topology: wgpu::PrimitiveTopology::TriangleStrip,
225 cull_mode: None,
226 ..Default::default()
227 },
228 depth_stencil: None,
229 multisample: wgpu::MultisampleState::default(),
230 multiview: None,
231 cache: None,
232 });
233
234 let quad_vertices: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
236
237 let vertex_buffer =
238 context
239 .device()
240 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
241 label: Some("Quad Renderer Vertex Buffer"),
242 contents: bytemuck::cast_slice(&quad_vertices),
243 usage: wgpu::BufferUsages::VERTEX,
244 });
245
246 Self {
247 context,
248 pipeline,
249 vertex_buffer,
250 transform_buffer,
251 transform_bind_group,
252 instance_buffer: None,
253 instance_count: 0,
254 pending_quads: Vec::with_capacity(1024),
255 data_dirty: false,
256 }
257 }
258
259 pub fn clear(&mut self) {
261 self.pending_quads.clear();
262 self.data_dirty = true;
263 }
264
265 #[inline]
267 pub fn add_quad(&mut self, min: Vec2, max: Vec2, color: Color) {
268 self.pending_quads.push(Quad::new(min, max, color));
269 self.data_dirty = true;
270 }
271
272 #[inline]
274 pub fn add_bar(&mut self, x_center: f32, width: f32, y_bottom: f32, y_top: f32, color: Color) {
275 self.pending_quads
276 .push(Quad::bar(x_center, width, y_bottom, y_top, color));
277 self.data_dirty = true;
278 }
279
280 #[inline]
282 pub fn add(&mut self, quad: Quad) {
283 self.pending_quads.push(quad);
284 self.data_dirty = true;
285 }
286
287 pub fn quad_count(&self) -> usize {
289 self.pending_quads.len()
290 }
291
292 pub fn prepare(&mut self) {
294 profile_scope!("quad_renderer_prepare");
295
296 if !self.data_dirty {
297 return; }
299
300 if self.pending_quads.is_empty() {
301 self.instance_buffer = None;
302 self.instance_count = 0;
303 self.data_dirty = false;
304 return;
305 }
306
307 tracing::trace!("Uploading {} quads to GPU", self.pending_quads.len());
308
309 let instances: Vec<QuadInstance> = {
311 profile_scope!("convert_instances");
312 self.pending_quads.iter().map(QuadInstance::new).collect()
313 };
314
315 {
317 profile_scope!("create_instance_buffer");
318 self.instance_buffer = Some(self.context.device().create_buffer_init(
319 &wgpu::util::BufferInitDescriptor {
320 label: Some("Quad Renderer Instance Buffer"),
321 contents: bytemuck::cast_slice(&instances),
322 usage: wgpu::BufferUsages::VERTEX,
323 },
324 ));
325 }
326
327 self.instance_count = self.pending_quads.len() as u32;
328 self.data_dirty = false;
329 }
330
331 pub fn render(&self, pass: &mut wgpu::RenderPass, viewport: Viewport) {
333 let transform = DataTransform::identity(viewport);
334 self.render_transformed(pass, &transform);
335 }
336
337 pub fn render_transformed(&self, pass: &mut wgpu::RenderPass, transform: &DataTransform) {
355 self.render_with_uniform(pass, transform.uniform());
356 }
357
358 fn render_with_uniform(&self, pass: &mut wgpu::RenderPass, transform: &TransformUniform) {
360 profile_scope!("quad_renderer_render");
361
362 if self.instance_count == 0 {
363 return;
364 }
365
366 let Some(instance_buffer) = &self.instance_buffer else {
367 return;
368 };
369
370 self.context.queue().write_buffer(
372 &self.transform_buffer,
373 0,
374 bytemuck::cast_slice(&[*transform]),
375 );
376
377 pass.push_debug_group("QuadRenderer::render");
379 pass.set_pipeline(&self.pipeline);
380 pass.set_bind_group(0, &self.transform_bind_group, &[]);
381 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
382 pass.set_vertex_buffer(1, instance_buffer.slice(..));
383 pass.draw(0..4, 0..self.instance_count);
384 pass.pop_debug_group();
385 }
386}
387
388const QUAD_SHADER: &str = r#"
390struct Transform {
391 projection: mat4x4<f32>,
392 scale: vec2<f32>,
393 offset: vec2<f32>,
394}
395
396@group(0) @binding(0)
397var<uniform> transform: Transform;
398
399struct VertexInput {
400 @location(0) quad_pos: vec2<f32>, // 0-1 range unit quad
401 @location(1) rect_min: vec2<f32>, // data coords
402 @location(2) rect_max: vec2<f32>, // data coords
403 @location(3) color: vec4<f32>,
404}
405
406struct VertexOutput {
407 @builtin(position) position: vec4<f32>,
408 @location(0) color: vec4<f32>,
409}
410
411@vertex
412fn vs_main(input: VertexInput) -> VertexOutput {
413 var output: VertexOutput;
414
415 // Interpolate between min and max based on quad position (0-1)
416 let data_pos = mix(input.rect_min, input.rect_max, input.quad_pos);
417
418 // Transform data coordinates to screen coordinates
419 let screen_pos = data_pos * transform.scale + transform.offset;
420
421 output.position = transform.projection * vec4<f32>(screen_pos, 0.0, 1.0);
422 output.color = input.color;
423
424 return output;
425}
426
427@fragment
428fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
429 return input.color;
430}
431"#;