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