Skip to main content

astrelis_geometry/
renderer.rs

1//! Geometry renderer for 2D shapes and paths.
2//!
3//! Provides GPU-accelerated rendering of tessellated geometry.
4
5use crate::gpu_types::{FillInstance, ProjectionUniform, StrokeInstance};
6use crate::instance_buffer::InstanceBuffer;
7use crate::pipeline::{
8    create_fill_pipeline, create_projection_bind_group_layout, create_stroke_pipeline,
9};
10use crate::vertex::{FillVertex, StrokeVertex, TessellatedMesh};
11use crate::{FillRule, Path, Shape, Stroke, Style, Tessellator};
12use astrelis_core::profiling::profile_scope;
13use astrelis_render::wgpu::util::DeviceExt;
14use astrelis_render::{Color, GraphicsContext, RenderWindow, Renderer, Viewport, wgpu};
15use glam::Vec2;
16use std::sync::Arc;
17
18/// Configuration for creating a [`GeometryRenderer`].
19///
20/// # Example
21///
22/// ```rust,no_run
23/// # use astrelis_geometry::GeometryRendererDescriptor;
24/// # use astrelis_render::wgpu;
25/// // Create descriptor from a window (recommended)
26/// // let desc = GeometryRendererDescriptor::from_window(&window);
27///
28/// // Or configure manually
29/// let desc = GeometryRendererDescriptor {
30///     name: "Shapes".to_string(),
31///     surface_format: wgpu::TextureFormat::Bgra8UnormSrgb,
32///     depth_format: None,
33/// };
34/// ```
35#[derive(Clone, Debug)]
36pub struct GeometryRendererDescriptor {
37    /// Name for the renderer (used in pipeline labels for debugging/profiling).
38    pub name: String,
39    /// Surface texture format. Must match the render target.
40    pub surface_format: wgpu::TextureFormat,
41    /// Depth format for z-ordering. `None` disables depth testing.
42    pub depth_format: Option<wgpu::TextureFormat>,
43}
44
45impl Default for GeometryRendererDescriptor {
46    fn default() -> Self {
47        Self {
48            name: "Geometry".to_string(),
49            surface_format: wgpu::TextureFormat::Bgra8UnormSrgb,
50            depth_format: None,
51        }
52    }
53}
54
55impl GeometryRendererDescriptor {
56    /// Create descriptor from a [`RenderWindow`], inheriting its format configuration.
57    ///
58    /// This is the **recommended** way to create a descriptor as it ensures
59    /// pipeline-renderpass format compatibility automatically.
60    pub fn from_window(window: &RenderWindow) -> Self {
61        Self {
62            name: "Geometry".to_string(),
63            surface_format: window.surface_format(),
64            depth_format: window.depth_format(),
65        }
66    }
67
68    /// Set the renderer name (used in pipeline labels).
69    pub fn with_name(mut self, name: impl Into<String>) -> Self {
70        self.name = name.into();
71        self
72    }
73
74    /// Enable depth testing with the specified format.
75    pub fn with_depth(mut self, format: wgpu::TextureFormat) -> Self {
76        self.depth_format = Some(format);
77        self
78    }
79
80    /// Disable depth testing.
81    pub fn without_depth(mut self) -> Self {
82        self.depth_format = None;
83        self
84    }
85}
86
87/// A scissor rectangle for clipping.
88#[derive(Debug, Clone, Copy)]
89pub struct ScissorRect {
90    pub x: u32,
91    pub y: u32,
92    pub width: u32,
93    pub height: u32,
94}
95
96impl ScissorRect {
97    /// Create a new scissor rect.
98    pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
99        Self {
100            x,
101            y,
102            width,
103            height,
104        }
105    }
106
107    /// Create from floating point coordinates (will be rounded).
108    pub fn from_f32(x: f32, y: f32, width: f32, height: f32) -> Self {
109        Self {
110            x: x.max(0.0) as u32,
111            y: y.max(0.0) as u32,
112            width: width.max(0.0) as u32,
113            height: height.max(0.0) as u32,
114        }
115    }
116}
117
118/// A draw command for the geometry renderer.
119#[derive(Debug)]
120enum DrawCommand {
121    /// Fill a tessellated mesh.
122    Fill {
123        mesh: TessellatedMesh<FillVertex>,
124        color: Color,
125        offset: Vec2,
126    },
127    /// Stroke a tessellated mesh.
128    Stroke {
129        mesh: TessellatedMesh<StrokeVertex>,
130        color: Color,
131        width: f32,
132        offset: Vec2,
133    },
134    /// Set scissor rect for clipping.
135    SetScissor(ScissorRect),
136    /// Reset scissor to full viewport.
137    ResetScissor,
138}
139
140/// GPU-accelerated geometry renderer.
141///
142/// Renders 2D shapes and paths using tessellation and instanced rendering.
143pub struct GeometryRenderer {
144    context: Arc<GraphicsContext>,
145    renderer: Renderer,
146
147    /// Current configuration (stored for reconfigure and descriptor access).
148    descriptor: GeometryRendererDescriptor,
149
150    // Pipelines
151    fill_pipeline: wgpu::RenderPipeline,
152    stroke_pipeline: wgpu::RenderPipeline,
153
154    // Bind groups
155    projection_bind_group_layout: wgpu::BindGroupLayout,
156    projection_bind_group: wgpu::BindGroup,
157    projection_buffer: wgpu::Buffer,
158
159    // Tessellator
160    tessellator: Tessellator,
161
162    // Draw commands for current frame
163    draw_commands: Vec<DrawCommand>,
164
165    // Buffers (rebuilt each frame for simplicity)
166    fill_vertex_buffer: Option<wgpu::Buffer>,
167    fill_index_buffer: Option<wgpu::Buffer>,
168    fill_instances: InstanceBuffer<FillInstance>,
169
170    stroke_vertex_buffer: Option<wgpu::Buffer>,
171    stroke_index_buffer: Option<wgpu::Buffer>,
172    stroke_instances: InstanceBuffer<StrokeInstance>,
173}
174
175impl GeometryRenderer {
176    /// Create a new geometry renderer with default configuration.
177    pub fn new(context: Arc<GraphicsContext>) -> Self {
178        Self::with_descriptor(context, GeometryRendererDescriptor::default())
179    }
180
181    /// Create renderer from a [`RenderWindow`], matching its format configuration.
182    ///
183    /// This is the **recommended** constructor as it ensures the renderer's pipelines
184    /// are compatible with the window's render pass configuration.
185    pub fn from_window(context: Arc<GraphicsContext>, window: &RenderWindow) -> Self {
186        Self::with_descriptor(context, GeometryRendererDescriptor::from_window(window))
187    }
188
189    /// Create renderer with explicit configuration.
190    pub fn with_descriptor(
191        context: Arc<GraphicsContext>,
192        descriptor: GeometryRendererDescriptor,
193    ) -> Self {
194        let renderer = Renderer::new(context.clone());
195
196        // Create projection buffer
197        let projection_buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
198            label: Some(&format!("{} Projection Buffer", descriptor.name)),
199            size: std::mem::size_of::<ProjectionUniform>() as u64,
200            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
201            mapped_at_creation: false,
202        });
203
204        // Create bind group layout and bind group
205        let projection_bind_group_layout = create_projection_bind_group_layout(&renderer);
206        let projection_bind_group = renderer.create_bind_group(
207            Some(&format!("{} Projection Bind Group", descriptor.name)),
208            &projection_bind_group_layout,
209            &[wgpu::BindGroupEntry {
210                binding: 0,
211                resource: projection_buffer.as_entire_binding(),
212            }],
213        );
214
215        // Create pipelines with descriptor configuration
216        let fill_pipeline = create_fill_pipeline(
217            &renderer,
218            &projection_bind_group_layout,
219            descriptor.surface_format,
220            descriptor.depth_format,
221            &descriptor.name,
222        );
223        let stroke_pipeline = create_stroke_pipeline(
224            &renderer,
225            &projection_bind_group_layout,
226            descriptor.surface_format,
227            descriptor.depth_format,
228            &descriptor.name,
229        );
230
231        // Create instance buffers
232        let fill_instances = InstanceBuffer::new(
233            context.device(),
234            Some(&format!("{} Fill Instances", descriptor.name)),
235            256,
236        );
237        let stroke_instances = InstanceBuffer::new(
238            context.device(),
239            Some(&format!("{} Stroke Instances", descriptor.name)),
240            256,
241        );
242
243        Self {
244            context,
245            renderer,
246            descriptor,
247            fill_pipeline,
248            stroke_pipeline,
249            projection_bind_group_layout,
250            projection_bind_group,
251            projection_buffer,
252            tessellator: Tessellator::new(),
253            draw_commands: Vec::new(),
254            fill_vertex_buffer: None,
255            fill_index_buffer: None,
256            fill_instances,
257            stroke_vertex_buffer: None,
258            stroke_index_buffer: None,
259            stroke_instances,
260        }
261    }
262
263    /// Get the current renderer configuration.
264    pub fn descriptor(&self) -> &GeometryRendererDescriptor {
265        &self.descriptor
266    }
267
268    /// Reconfigure the renderer with new format settings.
269    ///
270    /// This recreates all pipelines with the new configuration.
271    /// Buffers and non-format-dependent resources are preserved.
272    ///
273    /// # Use Case
274    ///
275    /// When a window is moved to a different monitor, the surface format
276    /// may change. Call this method to update the renderer to match.
277    pub fn reconfigure(&mut self, descriptor: GeometryRendererDescriptor) {
278        // Skip if formats haven't changed (optimization)
279        if self.descriptor.surface_format == descriptor.surface_format
280            && self.descriptor.depth_format == descriptor.depth_format
281        {
282            // Only update name if that changed
283            self.descriptor.name = descriptor.name;
284            return;
285        }
286
287        self.descriptor = descriptor;
288
289        // Recreate pipelines with new formats
290        self.fill_pipeline = create_fill_pipeline(
291            &self.renderer,
292            &self.projection_bind_group_layout,
293            self.descriptor.surface_format,
294            self.descriptor.depth_format,
295            &self.descriptor.name,
296        );
297        self.stroke_pipeline = create_stroke_pipeline(
298            &self.renderer,
299            &self.projection_bind_group_layout,
300            self.descriptor.surface_format,
301            self.descriptor.depth_format,
302            &self.descriptor.name,
303        );
304    }
305
306    /// Reconfigure from a window, inheriting its format configuration.
307    ///
308    /// Convenience method equivalent to:
309    /// ```rust,ignore
310    /// renderer.reconfigure(GeometryRendererDescriptor::from_window(window));
311    /// ```
312    pub fn reconfigure_from_window(&mut self, window: &RenderWindow) {
313        self.reconfigure(
314            GeometryRendererDescriptor::from_window(window).with_name(self.descriptor.name.clone()),
315        );
316    }
317
318    /// Set the tessellation tolerance.
319    pub fn set_tolerance(&mut self, tolerance: f32) {
320        self.tessellator.tolerance = tolerance;
321    }
322
323    /// Clear all draw commands.
324    pub fn clear(&mut self) {
325        self.draw_commands.clear();
326    }
327
328    /// Draw a shape with the given style.
329    pub fn draw_shape(&mut self, shape: &Shape, style: &Style) {
330        let path = shape.to_path();
331        self.draw_path(&path, style);
332    }
333
334    /// Draw a path with the given style.
335    pub fn draw_path(&mut self, path: &Path, style: &Style) {
336        // Handle fill
337        if let Some(fill) = &style.fill
338            && let Some(color) = fill.effective_color()
339        {
340            let mesh = self.tessellator.tessellate_fill(path, fill.rule);
341            if !mesh.is_empty() {
342                self.draw_commands.push(DrawCommand::Fill {
343                    mesh,
344                    color,
345                    offset: style.transform.translation(),
346                });
347            }
348        }
349
350        // Handle stroke
351        if let Some(stroke) = &style.stroke
352            && stroke.is_visible()
353            && let Some(color) = stroke.effective_color()
354        {
355            let mesh = self.tessellator.tessellate_stroke(path, stroke);
356            if !mesh.is_empty() {
357                self.draw_commands.push(DrawCommand::Stroke {
358                    mesh,
359                    color,
360                    width: stroke.width,
361                    offset: style.transform.translation(),
362                });
363            }
364        }
365    }
366
367    /// Draw a filled rectangle.
368    pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: Color) {
369        let mesh = self.tessellator.tessellate_rect_fill(position, size);
370        self.draw_commands.push(DrawCommand::Fill {
371            mesh,
372            color,
373            offset: Vec2::ZERO,
374        });
375    }
376
377    /// Draw a filled circle.
378    pub fn draw_circle(&mut self, center: Vec2, radius: f32, color: Color) {
379        profile_scope!("draw_circle");
380        let shape = Shape::circle(center, radius);
381        let style = Style::fill_color(color);
382        self.draw_shape(&shape, &style);
383    }
384
385    /// Draw a line.
386    pub fn draw_line(&mut self, start: Vec2, end: Vec2, width: f32, color: Color) {
387        profile_scope!("draw_line");
388        let mesh = self.tessellator.tessellate_line(start, end, width);
389        self.draw_commands.push(DrawCommand::Fill {
390            mesh,
391            color,
392            offset: Vec2::ZERO,
393        });
394    }
395
396    /// Draw a stroked rectangle.
397    pub fn draw_rect_stroke(&mut self, position: Vec2, size: Vec2, stroke: &Stroke) {
398        let shape = Shape::rect(position, size);
399        let style = Style::new().with_stroke(stroke.clone());
400        self.draw_shape(&shape, &style);
401    }
402
403    /// Draw a stroked circle.
404    pub fn draw_circle_stroke(&mut self, center: Vec2, radius: f32, stroke: &Stroke) {
405        let shape = Shape::circle(center, radius);
406        let style = Style::new().with_stroke(stroke.clone());
407        self.draw_shape(&shape, &style);
408    }
409
410    /// Draw a filled shape directly.
411    pub fn draw_shape_fill(&mut self, shape: &Shape, color: Color) {
412        let style = Style::fill_color(color);
413        self.draw_shape(shape, &style);
414    }
415
416    /// Draw a stroked shape directly.
417    pub fn draw_shape_stroke(&mut self, shape: &Shape, stroke: &Stroke) {
418        let style = Style::new().with_stroke(stroke.clone());
419        self.draw_shape(shape, &style);
420    }
421
422    /// Draw a path with fill only.
423    pub fn draw_path_fill(&mut self, path: &Path, color: Color, fill_rule: FillRule) {
424        let mesh = self.tessellator.tessellate_fill(path, fill_rule);
425        if !mesh.is_empty() {
426            self.draw_commands.push(DrawCommand::Fill {
427                mesh,
428                color,
429                offset: Vec2::ZERO,
430            });
431        }
432    }
433
434    /// Draw a path with stroke only.
435    pub fn draw_path_stroke(&mut self, path: &Path, stroke: &Stroke) {
436        profile_scope!("draw_path_stroke");
437        if stroke.is_visible()
438            && let Some(color) = stroke.effective_color()
439        {
440            let mesh = self.tessellator.tessellate_stroke(path, stroke);
441            if !mesh.is_empty() {
442                self.draw_commands.push(DrawCommand::Stroke {
443                    mesh,
444                    color,
445                    width: stroke.width,
446                    offset: Vec2::ZERO,
447                });
448            }
449        }
450    }
451
452    /// Set a scissor rectangle to clip subsequent drawing.
453    ///
454    /// All geometry drawn after this call will be clipped to the specified rectangle.
455    /// Call `reset_scissor()` to restore full viewport rendering.
456    pub fn set_scissor(&mut self, scissor: ScissorRect) {
457        self.draw_commands.push(DrawCommand::SetScissor(scissor));
458    }
459
460    /// Reset scissor to full viewport (no clipping).
461    pub fn reset_scissor(&mut self) {
462        self.draw_commands.push(DrawCommand::ResetScissor);
463    }
464
465    /// Render all queued geometry.
466    pub fn render(&mut self, pass: &mut wgpu::RenderPass, viewport: Viewport) {
467        profile_scope!("geometry_render_total");
468
469        if self.draw_commands.is_empty() {
470            return;
471        }
472
473        let num_commands = self.draw_commands.len();
474        tracing::trace!("Rendering {} draw commands", num_commands);
475
476        // Update projection
477        let logical_size = viewport.to_logical();
478        let physical_size = viewport.size; // Already physical
479        let projection = ProjectionUniform::orthographic(logical_size.width, logical_size.height);
480        self.renderer.queue().write_buffer(
481            &self.projection_buffer,
482            0,
483            bytemuck::cast_slice(&[projection]),
484        );
485
486        // Scale factor for converting logical to physical coordinates
487        let scale = viewport.scale_factor.0 as f32;
488
489        // Collect all geometry data first
490        let mut fill_vertices: Vec<FillVertex> = Vec::new();
491        let mut fill_indices: Vec<u32> = Vec::new();
492        let mut fill_instance_data: Vec<FillInstance> = Vec::new();
493
494        let mut stroke_vertices: Vec<StrokeVertex> = Vec::new();
495        let mut stroke_indices: Vec<u32> = Vec::new();
496        let mut stroke_instance_data: Vec<StrokeInstance> = Vec::new();
497
498        // Build a list of render operations with scissor state
499        #[derive(Debug)]
500        enum RenderOp {
501            SetScissor(u32, u32, u32, u32), // x, y, w, h in physical pixels
502            ResetScissor,
503            DrawFill {
504                index_start: u32,
505                index_count: u32,
506                instance_idx: u32,
507            },
508            DrawStroke {
509                index_start: u32,
510                index_count: u32,
511                instance_idx: u32,
512            },
513        }
514
515        let mut ops: Vec<RenderOp> = Vec::new();
516
517        profile_scope!("collect_geometry");
518        for cmd in &self.draw_commands {
519            match cmd {
520                DrawCommand::Fill {
521                    mesh,
522                    color,
523                    offset,
524                } => {
525                    let vertex_offset = fill_vertices.len() as u32;
526                    let index_start = fill_indices.len() as u32;
527
528                    fill_vertices.extend_from_slice(&mesh.vertices);
529                    fill_indices.extend(mesh.indices.iter().map(|i| i + vertex_offset));
530
531                    let instance_idx = fill_instance_data.len() as u32;
532                    fill_instance_data.push(FillInstance::new(
533                        offset.x,
534                        offset.y,
535                        [color.r, color.g, color.b, color.a],
536                    ));
537
538                    ops.push(RenderOp::DrawFill {
539                        index_start,
540                        index_count: mesh.indices.len() as u32,
541                        instance_idx,
542                    });
543                }
544                DrawCommand::Stroke {
545                    mesh,
546                    color,
547                    width,
548                    offset,
549                } => {
550                    let vertex_offset = stroke_vertices.len() as u32;
551                    let index_start = stroke_indices.len() as u32;
552
553                    stroke_vertices.extend_from_slice(&mesh.vertices);
554                    stroke_indices.extend(mesh.indices.iter().map(|i| i + vertex_offset));
555
556                    let instance_idx = stroke_instance_data.len() as u32;
557                    stroke_instance_data.push(StrokeInstance::new(
558                        offset.x,
559                        offset.y,
560                        [color.r, color.g, color.b, color.a],
561                        *width,
562                    ));
563
564                    ops.push(RenderOp::DrawStroke {
565                        index_start,
566                        index_count: mesh.indices.len() as u32,
567                        instance_idx,
568                    });
569                }
570                DrawCommand::SetScissor(scissor) => {
571                    // Convert logical coordinates to physical pixels
572                    let x = (scissor.x as f32 * scale) as u32;
573                    let y = (scissor.y as f32 * scale) as u32;
574                    let w = (scissor.width as f32 * scale) as u32;
575                    let h = (scissor.height as f32 * scale) as u32;
576                    ops.push(RenderOp::SetScissor(x, y, w, h));
577                }
578                DrawCommand::ResetScissor => {
579                    ops.push(RenderOp::ResetScissor);
580                }
581            }
582        }
583
584        // Create/update fill buffers
585        {
586            profile_scope!("create_fill_buffers");
587            if !fill_vertices.is_empty() {
588                self.fill_vertex_buffer = Some(self.context.device().create_buffer_init(
589                    &wgpu::util::BufferInitDescriptor {
590                        label: Some("Fill Vertex Buffer"),
591                        contents: bytemuck::cast_slice(&fill_vertices),
592                        usage: wgpu::BufferUsages::VERTEX,
593                    },
594                ));
595                self.fill_index_buffer = Some(self.context.device().create_buffer_init(
596                    &wgpu::util::BufferInitDescriptor {
597                        label: Some("Fill Index Buffer"),
598                        contents: bytemuck::cast_slice(&fill_indices),
599                        usage: wgpu::BufferUsages::INDEX,
600                    },
601                ));
602                self.fill_instances
603                    .set_instances(self.context.device(), fill_instance_data);
604                self.fill_instances.upload_dirty(self.renderer.queue());
605            }
606        }
607
608        // Create/update stroke buffers
609        {
610            profile_scope!("create_stroke_buffers");
611            if !stroke_vertices.is_empty() {
612                self.stroke_vertex_buffer = Some(self.context.device().create_buffer_init(
613                    &wgpu::util::BufferInitDescriptor {
614                        label: Some("Stroke Vertex Buffer"),
615                        contents: bytemuck::cast_slice(&stroke_vertices),
616                        usage: wgpu::BufferUsages::VERTEX,
617                    },
618                ));
619                self.stroke_index_buffer = Some(self.context.device().create_buffer_init(
620                    &wgpu::util::BufferInitDescriptor {
621                        label: Some("Stroke Index Buffer"),
622                        contents: bytemuck::cast_slice(&stroke_indices),
623                        usage: wgpu::BufferUsages::INDEX,
624                    },
625                ));
626                self.stroke_instances
627                    .set_instances(self.context.device(), stroke_instance_data);
628                self.stroke_instances.upload_dirty(self.renderer.queue());
629            }
630        }
631
632        // Track current pipeline state
633        let mut fill_pipeline_bound = false;
634        let mut stroke_pipeline_bound = false;
635
636        // Execute render operations in order
637        profile_scope!("execute_draw_ops");
638        for op in ops {
639            match op {
640                RenderOp::SetScissor(x, y, w, h) => {
641                    pass.set_scissor_rect(x, y, w, h);
642                }
643                RenderOp::ResetScissor => {
644                    pass.set_scissor_rect(
645                        0,
646                        0,
647                        physical_size.width as u32,
648                        physical_size.height as u32,
649                    );
650                }
651                RenderOp::DrawFill {
652                    index_start,
653                    index_count,
654                    instance_idx,
655                } => {
656                    if let (Some(vbo), Some(ibo)) =
657                        (&self.fill_vertex_buffer, &self.fill_index_buffer)
658                    {
659                        if !fill_pipeline_bound {
660                            pass.set_pipeline(&self.fill_pipeline);
661                            pass.set_bind_group(0, &self.projection_bind_group, &[]);
662                            pass.set_vertex_buffer(0, vbo.slice(..));
663                            pass.set_vertex_buffer(1, self.fill_instances.buffer().slice(..));
664                            pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
665                            fill_pipeline_bound = true;
666                            stroke_pipeline_bound = false;
667                        }
668                        pass.draw_indexed(
669                            index_start..(index_start + index_count),
670                            0,
671                            instance_idx..(instance_idx + 1),
672                        );
673                    }
674                }
675                RenderOp::DrawStroke {
676                    index_start,
677                    index_count,
678                    instance_idx,
679                } => {
680                    if let (Some(vbo), Some(ibo)) =
681                        (&self.stroke_vertex_buffer, &self.stroke_index_buffer)
682                    {
683                        if !stroke_pipeline_bound {
684                            pass.set_pipeline(&self.stroke_pipeline);
685                            pass.set_bind_group(0, &self.projection_bind_group, &[]);
686                            pass.set_vertex_buffer(0, vbo.slice(..));
687                            pass.set_vertex_buffer(1, self.stroke_instances.buffer().slice(..));
688                            pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
689                            stroke_pipeline_bound = true;
690                            fill_pipeline_bound = false;
691                        }
692                        pass.draw_indexed(
693                            index_start..(index_start + index_count),
694                            0,
695                            instance_idx..(instance_idx + 1),
696                        );
697                    }
698                }
699            }
700        }
701
702        // Clear commands for next frame
703        self.draw_commands.clear();
704    }
705}