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 pub fn render_with_data_transform(
366 &self,
367 pass: &mut wgpu::RenderPass,
368 viewport: Viewport,
369 plot_x: f32,
370 plot_y: f32,
371 plot_width: f32,
372 plot_height: f32,
373 data_x_min: f64,
374 data_x_max: f64,
375 data_y_min: f64,
376 data_y_max: f64,
377 ) {
378 let transform = DataTransform::from_data_range(
379 viewport,
380 crate::transform::DataRangeParams::new(
381 plot_x,
382 plot_y,
383 plot_width,
384 plot_height,
385 data_x_min,
386 data_x_max,
387 data_y_min,
388 data_y_max,
389 ),
390 );
391 self.render_transformed(pass, &transform);
392 }
393
394 fn render_with_uniform(&self, pass: &mut wgpu::RenderPass, transform: &TransformUniform) {
396 profile_scope!("quad_renderer_render");
397
398 if self.instance_count == 0 {
399 return;
400 }
401
402 let Some(instance_buffer) = &self.instance_buffer else {
403 return;
404 };
405
406 self.context.queue().write_buffer(
408 &self.transform_buffer,
409 0,
410 bytemuck::cast_slice(&[*transform]),
411 );
412
413 pass.push_debug_group("QuadRenderer::render");
415 pass.set_pipeline(&self.pipeline);
416 pass.set_bind_group(0, &self.transform_bind_group, &[]);
417 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
418 pass.set_vertex_buffer(1, instance_buffer.slice(..));
419 pass.draw(0..4, 0..self.instance_count);
420 pass.pop_debug_group();
421 }
422}
423
424const QUAD_SHADER: &str = r#"
426struct Transform {
427 projection: mat4x4<f32>,
428 scale: vec2<f32>,
429 offset: vec2<f32>,
430}
431
432@group(0) @binding(0)
433var<uniform> transform: Transform;
434
435struct VertexInput {
436 @location(0) quad_pos: vec2<f32>, // 0-1 range unit quad
437 @location(1) rect_min: vec2<f32>, // data coords
438 @location(2) rect_max: vec2<f32>, // data coords
439 @location(3) color: vec4<f32>,
440}
441
442struct VertexOutput {
443 @builtin(position) position: vec4<f32>,
444 @location(0) color: vec4<f32>,
445}
446
447@vertex
448fn vs_main(input: VertexInput) -> VertexOutput {
449 var output: VertexOutput;
450
451 // Interpolate between min and max based on quad position (0-1)
452 let data_pos = mix(input.rect_min, input.rect_max, input.quad_pos);
453
454 // Transform data coordinates to screen coordinates
455 let screen_pos = data_pos * transform.scale + transform.offset;
456
457 output.position = transform.projection * vec4<f32>(screen_pos, 0.0, 1.0);
458 output.color = input.color;
459
460 return output;
461}
462
463@fragment
464fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
465 return input.color;
466}
467"#;