1use astrelis_core::profiling::profile_scope;
10use crate::capability::{GpuRequirements, RenderCapability};
11use crate::transform::{DataTransform, TransformUniform};
12use crate::{Color, GraphicsContext, Viewport};
13use bytemuck::{Pod, Zeroable};
14use glam::Vec2;
15use std::sync::Arc;
16use wgpu::util::DeviceExt;
17
18#[derive(Debug, Clone, Copy)]
23pub struct LineSegment {
24 pub start: Vec2,
25 pub end: Vec2,
26 pub width: f32,
27 pub color: Color,
28}
29
30impl LineSegment {
31 pub fn new(start: Vec2, end: Vec2, width: f32, color: Color) -> Self {
32 Self { start, end, width, color }
33 }
34}
35
36#[repr(C)]
38#[derive(Debug, Clone, Copy, Pod, Zeroable)]
39struct LineInstance {
40 start: [f32; 2],
41 end: [f32; 2],
42 width: f32,
43 color: [f32; 4],
44 _padding: [f32; 1],
45}
46
47impl LineInstance {
48 fn new(segment: &LineSegment) -> Self {
49 Self {
50 start: [segment.start.x, segment.start.y],
51 end: [segment.end.x, segment.end.y],
52 width: segment.width,
53 color: [segment.color.r, segment.color.g, segment.color.b, segment.color.a],
54 _padding: [0.0],
55 }
56 }
57}
58
59impl RenderCapability for LineRenderer {
60 fn requirements() -> GpuRequirements {
61 GpuRequirements::none()
62 }
63
64 fn name() -> &'static str {
65 "LineRenderer"
66 }
67}
68
69pub struct LineRenderer {
76 context: Arc<GraphicsContext>,
77 pipeline: wgpu::RenderPipeline,
78 vertex_buffer: wgpu::Buffer,
79 transform_buffer: wgpu::Buffer,
80 transform_bind_group: wgpu::BindGroup,
81 instance_buffer: Option<wgpu::Buffer>,
82 instance_count: u32,
83 pending_segments: Vec<LineSegment>,
85 data_dirty: bool,
87}
88
89impl LineRenderer {
90 pub fn new(context: Arc<GraphicsContext>, target_format: wgpu::TextureFormat) -> Self {
95 let transform_buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
97 label: Some("Line Renderer Transform Buffer"),
98 size: std::mem::size_of::<TransformUniform>() as u64,
99 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
100 mapped_at_creation: false,
101 });
102
103 let bind_group_layout =
105 context
106 .device()
107 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
108 label: Some("Line Renderer Bind Group Layout"),
109 entries: &[wgpu::BindGroupLayoutEntry {
110 binding: 0,
111 visibility: wgpu::ShaderStages::VERTEX,
112 ty: wgpu::BindingType::Buffer {
113 ty: wgpu::BufferBindingType::Uniform,
114 has_dynamic_offset: false,
115 min_binding_size: None,
116 },
117 count: None,
118 }],
119 });
120
121 let transform_bind_group = context.device().create_bind_group(&wgpu::BindGroupDescriptor {
122 label: Some("Line Renderer Transform Bind Group"),
123 layout: &bind_group_layout,
124 entries: &[wgpu::BindGroupEntry {
125 binding: 0,
126 resource: transform_buffer.as_entire_binding(),
127 }],
128 });
129
130 let shader = context
132 .device()
133 .create_shader_module(wgpu::ShaderModuleDescriptor {
134 label: Some("Line Renderer Shader"),
135 source: wgpu::ShaderSource::Wgsl(LINE_SHADER.into()),
136 });
137
138 let pipeline_layout =
140 context
141 .device()
142 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
143 label: Some("Line Renderer Pipeline Layout"),
144 bind_group_layouts: &[&bind_group_layout],
145 push_constant_ranges: &[],
146 });
147
148 let pipeline = context
149 .device()
150 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
151 label: Some("Line Renderer Pipeline"),
152 layout: Some(&pipeline_layout),
153 vertex: wgpu::VertexState {
154 module: &shader,
155 entry_point: Some("vs_main"),
156 buffers: &[
157 wgpu::VertexBufferLayout {
159 array_stride: 8,
160 step_mode: wgpu::VertexStepMode::Vertex,
161 attributes: &[wgpu::VertexAttribute {
162 format: wgpu::VertexFormat::Float32x2,
163 offset: 0,
164 shader_location: 0,
165 }],
166 },
167 wgpu::VertexBufferLayout {
169 array_stride: std::mem::size_of::<LineInstance>() as u64,
170 step_mode: wgpu::VertexStepMode::Instance,
171 attributes: &[
172 wgpu::VertexAttribute {
173 format: wgpu::VertexFormat::Float32x2,
174 offset: 0,
175 shader_location: 1,
176 },
177 wgpu::VertexAttribute {
178 format: wgpu::VertexFormat::Float32x2,
179 offset: 8,
180 shader_location: 2,
181 },
182 wgpu::VertexAttribute {
183 format: wgpu::VertexFormat::Float32,
184 offset: 16,
185 shader_location: 3,
186 },
187 wgpu::VertexAttribute {
188 format: wgpu::VertexFormat::Float32x4,
189 offset: 20,
190 shader_location: 4,
191 },
192 ],
193 },
194 ],
195 compilation_options: wgpu::PipelineCompilationOptions::default(),
196 },
197 fragment: Some(wgpu::FragmentState {
198 module: &shader,
199 entry_point: Some("fs_main"),
200 targets: &[Some(wgpu::ColorTargetState {
201 format: target_format,
202 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
203 write_mask: wgpu::ColorWrites::ALL,
204 })],
205 compilation_options: wgpu::PipelineCompilationOptions::default(),
206 }),
207 primitive: wgpu::PrimitiveState {
208 topology: wgpu::PrimitiveTopology::TriangleStrip,
209 cull_mode: None,
210 ..Default::default()
211 },
212 depth_stencil: None,
213 multisample: wgpu::MultisampleState::default(),
214 multiview: None,
215 cache: None,
216 });
217
218 let quad_vertices: [[f32; 2]; 4] = [
220 [-0.5, -0.5],
221 [0.5, -0.5],
222 [-0.5, 0.5],
223 [0.5, 0.5],
224 ];
225
226 let vertex_buffer = context
227 .device()
228 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
229 label: Some("Line Renderer Vertex Buffer"),
230 contents: bytemuck::cast_slice(&quad_vertices),
231 usage: wgpu::BufferUsages::VERTEX,
232 });
233
234 Self {
235 context,
236 pipeline,
237 vertex_buffer,
238 transform_buffer,
239 transform_bind_group,
240 instance_buffer: None,
241 instance_count: 0,
242 pending_segments: Vec::with_capacity(1024),
243 data_dirty: false,
244 }
245 }
246
247 pub fn clear(&mut self) {
249 self.pending_segments.clear();
250 self.data_dirty = true;
251 }
252
253 #[inline]
255 pub fn add_line(&mut self, start: Vec2, end: Vec2, width: f32, color: Color) {
256 self.pending_segments.push(LineSegment::new(start, end, width, color));
257 self.data_dirty = true;
258 }
259
260 #[inline]
262 pub fn add_segment(&mut self, segment: LineSegment) {
263 self.pending_segments.push(segment);
264 self.data_dirty = true;
265 }
266
267 pub fn segment_count(&self) -> usize {
269 self.pending_segments.len()
270 }
271
272 pub fn prepare(&mut self) {
274 profile_scope!("line_renderer_prepare");
275
276 if !self.data_dirty {
277 return; }
279
280 if self.pending_segments.is_empty() {
281 self.instance_buffer = None;
282 self.instance_count = 0;
283 self.data_dirty = false;
284 return;
285 }
286
287 tracing::trace!("Uploading {} line segments to GPU", self.pending_segments.len());
288
289 let instances: Vec<LineInstance> = {
291 profile_scope!("convert_instances");
292 self.pending_segments.iter().map(LineInstance::new).collect()
293 };
294
295 {
297 profile_scope!("create_instance_buffer");
298 self.instance_buffer = Some(
299 self.context
300 .device()
301 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
302 label: Some("Line Renderer Instance Buffer"),
303 contents: bytemuck::cast_slice(&instances),
304 usage: wgpu::BufferUsages::VERTEX,
305 }),
306 );
307 }
308
309 self.instance_count = self.pending_segments.len() as u32;
310 self.data_dirty = false;
311 }
312
313 pub fn render(&self, pass: &mut wgpu::RenderPass, viewport: Viewport) {
315 let transform = DataTransform::identity(viewport);
316 self.render_transformed(pass, &transform);
317 }
318
319 pub fn render_transformed(&self, pass: &mut wgpu::RenderPass, transform: &DataTransform) {
337 self.render_with_uniform(pass, transform.uniform());
338 }
339
340 pub fn render_with_data_transform(
348 &self,
349 pass: &mut wgpu::RenderPass,
350 viewport: Viewport,
351 plot_x: f32,
352 plot_y: f32,
353 plot_width: f32,
354 plot_height: f32,
355 data_x_min: f64,
356 data_x_max: f64,
357 data_y_min: f64,
358 data_y_max: f64,
359 ) {
360 let transform = DataTransform::from_data_range(
361 viewport,
362 crate::transform::DataRangeParams::new(
363 plot_x,
364 plot_y,
365 plot_width,
366 plot_height,
367 data_x_min,
368 data_x_max,
369 data_y_min,
370 data_y_max,
371 ),
372 );
373 self.render_transformed(pass, &transform);
374 }
375
376 fn render_with_uniform(&self, pass: &mut wgpu::RenderPass, transform: &TransformUniform) {
378 profile_scope!("line_renderer_render");
379
380 if self.instance_count == 0 {
381 return;
382 }
383
384 let Some(instance_buffer) = &self.instance_buffer else {
385 return;
386 };
387
388 self.context.queue().write_buffer(
390 &self.transform_buffer,
391 0,
392 bytemuck::cast_slice(&[*transform]),
393 );
394
395 pass.push_debug_group("LineRenderer::render");
397 pass.set_pipeline(&self.pipeline);
398 pass.set_bind_group(0, &self.transform_bind_group, &[]);
399 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
400 pass.set_vertex_buffer(1, instance_buffer.slice(..));
401 pass.draw(0..4, 0..self.instance_count);
402 pass.pop_debug_group();
403 }
404}
405
406const LINE_SHADER: &str = r#"
408struct Transform {
409 projection: mat4x4<f32>,
410 scale: vec2<f32>,
411 offset: vec2<f32>,
412}
413
414@group(0) @binding(0)
415var<uniform> transform: Transform;
416
417struct VertexInput {
418 @location(0) quad_pos: vec2<f32>,
419 @location(1) line_start: vec2<f32>,
420 @location(2) line_end: vec2<f32>,
421 @location(3) line_width: f32,
422 @location(4) color: vec4<f32>,
423}
424
425struct VertexOutput {
426 @builtin(position) position: vec4<f32>,
427 @location(0) color: vec4<f32>,
428}
429
430@vertex
431fn vs_main(input: VertexInput) -> VertexOutput {
432 var output: VertexOutput;
433
434 // Transform data coordinates to screen coordinates
435 let screen_start = input.line_start * transform.scale + transform.offset;
436 let screen_end = input.line_end * transform.scale + transform.offset;
437
438 // Compute line direction and perpendicular
439 let delta = screen_end - screen_start;
440 let length = length(delta);
441
442 var dir: vec2<f32>;
443 var perp: vec2<f32>;
444 if length < 0.0001 {
445 dir = vec2<f32>(1.0, 0.0);
446 perp = vec2<f32>(0.0, 1.0);
447 } else {
448 dir = delta / length;
449 perp = vec2<f32>(-dir.y, dir.x);
450 }
451
452 // Transform quad to line segment
453 let center = (screen_start + screen_end) * 0.5;
454 let local_x = input.quad_pos.x * length;
455 let local_y = input.quad_pos.y * input.line_width;
456 let world_pos = center + dir * local_x + perp * local_y;
457
458 output.position = transform.projection * vec4<f32>(world_pos, 0.0, 1.0);
459 output.color = input.color;
460
461 return output;
462}
463
464@fragment
465fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
466 return input.color;
467}
468"#;