Skip to main content

agpu/
renderer.rs

1//! 2D shape renderer — batches geometry into vertex/index buffers.
2//!
3//! Generates triangle meshes for rects, rounded rects, circles, and
4//! lines. All geometry is collected per frame and flushed in a single
5//! draw call for efficiency.
6
7use crate::context::GpuContext;
8use crate::core::{Color, Position, Rect};
9use crate::paint::{Gradient, GradientStop, ImageHandle, Shadow};
10use crate::types::TextureFormat;
11use crate::vertex::Vertex;
12
13/// Per-frame shape geometry collector.
14pub struct ShapeRenderer {
15    vertices: Vec<Vertex>,
16    indices: Vec<u32>,
17    pipeline: wgpu::RenderPipeline,
18    uniform_buffer: wgpu::Buffer,
19    uniform_bind_group: wgpu::BindGroup,
20    vertex_buffer: wgpu::Buffer,
21    index_buffer: wgpu::Buffer,
22    vertex_capacity: usize,
23    index_capacity: usize,
24    viewport: [f32; 2],
25    sample_count: u32,
26}
27
28/// Number of segments used to approximate a circle / arc.
29const CIRCLE_SEGMENTS: u32 = 32;
30
31/// Initial buffer capacity (in elements).
32const INITIAL_VERTEX_CAPACITY: usize = 4096;
33const INITIAL_INDEX_CAPACITY: usize = 8192;
34
35impl ShapeRenderer {
36    /// Create a new shape renderer with the given GPU context and surface format.
37    pub fn new(
38        gpu: &GpuContext,
39        format: TextureFormat,
40        width: f32,
41        height: f32,
42        sample_count: u32,
43    ) -> Self {
44        let device = gpu.device();
45        // Uniform buffer for viewport size
46        let viewport = [width, height];
47        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
48            label: Some("agpu_uniform"),
49            size: 8, // vec2<f32>
50            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
51            mapped_at_creation: false,
52        });
53
54        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
55            label: Some("agpu_bind_group_layout"),
56            entries: &[wgpu::BindGroupLayoutEntry {
57                binding: 0,
58                visibility: wgpu::ShaderStages::VERTEX,
59                ty: wgpu::BindingType::Buffer {
60                    ty: wgpu::BufferBindingType::Uniform,
61                    has_dynamic_offset: false,
62                    min_binding_size: None,
63                },
64                count: None,
65            }],
66        });
67
68        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
69            label: Some("agpu_bind_group"),
70            layout: &bind_group_layout,
71            entries: &[wgpu::BindGroupEntry {
72                binding: 0,
73                resource: uniform_buffer.as_entire_binding(),
74            }],
75        });
76
77        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
78            label: Some("agpu_pipeline_layout"),
79            bind_group_layouts: &[&bind_group_layout],
80            push_constant_ranges: &[],
81        });
82
83        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
84            label: Some("agpu_shader"),
85            source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
86        });
87
88        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
89            label: Some("agpu_shape_pipeline"),
90            layout: Some(&pipeline_layout),
91            vertex: wgpu::VertexState {
92                module: &shader,
93                entry_point: Some("vs_main"),
94                buffers: &[Vertex::LAYOUT],
95                compilation_options: wgpu::PipelineCompilationOptions::default(),
96            },
97            fragment: Some(wgpu::FragmentState {
98                module: &shader,
99                entry_point: Some("fs_main"),
100                targets: &[Some(wgpu::ColorTargetState {
101                    format,
102                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
103                    write_mask: wgpu::ColorWrites::ALL,
104                })],
105                compilation_options: wgpu::PipelineCompilationOptions::default(),
106            }),
107            primitive: wgpu::PrimitiveState {
108                topology: wgpu::PrimitiveTopology::TriangleList,
109                strip_index_format: None,
110                front_face: wgpu::FrontFace::Ccw,
111                cull_mode: None,
112                polygon_mode: wgpu::PolygonMode::Fill,
113                unclipped_depth: false,
114                conservative: false,
115            },
116            depth_stencil: None,
117            multisample: wgpu::MultisampleState {
118                count: sample_count,
119                mask: !0,
120                alpha_to_coverage_enabled: false,
121            },
122            multiview: None,
123            cache: None,
124        });
125
126        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
127            label: Some("agpu_vertex"),
128            size: (INITIAL_VERTEX_CAPACITY * std::mem::size_of::<Vertex>()) as u64,
129            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
130            mapped_at_creation: false,
131        });
132
133        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
134            label: Some("agpu_index"),
135            size: (INITIAL_INDEX_CAPACITY * std::mem::size_of::<u32>()) as u64,
136            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
137            mapped_at_creation: false,
138        });
139
140        Self {
141            vertices: Vec::with_capacity(INITIAL_VERTEX_CAPACITY),
142            indices: Vec::with_capacity(INITIAL_INDEX_CAPACITY),
143            pipeline,
144            uniform_buffer,
145            uniform_bind_group,
146            vertex_buffer,
147            index_buffer,
148            vertex_capacity: INITIAL_VERTEX_CAPACITY,
149            index_capacity: INITIAL_INDEX_CAPACITY,
150            viewport,
151            sample_count,
152        }
153    }
154
155    /// Clear geometry for a new frame.
156    pub fn begin_frame(&mut self) {
157        self.vertices.clear();
158        self.indices.clear();
159    }
160
161    /// Update the viewport size.
162    pub fn set_viewport(&mut self, width: f32, height: f32) {
163        self.viewport = [width, height];
164    }
165
166    /// Add a filled rectangle.
167    pub fn fill_rect(&mut self, rect: Rect, color: Color, corner_radius: f32) {
168        if corner_radius <= 0.5 {
169            self.push_quad(rect, color);
170        } else {
171            self.push_rounded_rect(rect, color, corner_radius);
172        }
173    }
174
175    /// Add a stroked rectangle.
176    pub fn stroke_rect(&mut self, rect: Rect, color: Color, width: f32, corner_radius: f32) {
177        if corner_radius <= 0.5 {
178            // Top
179            self.push_quad(Rect::new(rect.x, rect.y, rect.width, width), color);
180            // Bottom
181            self.push_quad(
182                Rect::new(rect.x, rect.y + rect.height - width, rect.width, width),
183                color,
184            );
185            // Left
186            self.push_quad(
187                Rect::new(rect.x, rect.y + width, width, rect.height - 2.0 * width),
188                color,
189            );
190            // Right
191            self.push_quad(
192                Rect::new(
193                    rect.x + rect.width - width,
194                    rect.y + width,
195                    width,
196                    rect.height - 2.0 * width,
197                ),
198                color,
199            );
200        } else {
201            self.stroke_rounded_rect(rect, color, width, corner_radius, Color::TRANSPARENT);
202        }
203    }
204
205    /// Stroke a rounded rectangle with proper inner cutout.
206    ///
207    /// When `bg_color` is not transparent, the inner region is filled with that
208    /// color, producing a proper stroked appearance on any background.
209    pub fn stroke_rounded_rect(
210        &mut self,
211        rect: Rect,
212        color: Color,
213        width: f32,
214        corner_radius: f32,
215        bg_color: Color,
216    ) {
217        // Draw outer rounded rect
218        self.push_rounded_rect(rect, color, corner_radius);
219        // Draw inner rounded rect to cut out the fill
220        let inner_radius = (corner_radius - width).max(0.0);
221        let inner = Rect::new(
222            rect.x + width,
223            rect.y + width,
224            rect.width - 2.0 * width,
225            rect.height - 2.0 * width,
226        );
227        if inner.width > 0.0 && inner.height > 0.0 {
228            let fill = if bg_color.a > 0.0 {
229                bg_color
230            } else {
231                // Transparent cutout — use fully transparent to punch through
232                Color::TRANSPARENT
233            };
234            self.push_rounded_rect(inner, fill, inner_radius);
235        }
236    }
237
238    /// Add a filled circle.
239    pub fn fill_circle(&mut self, center: Position, radius: f32, color: Color) {
240        let base = self.vertices.len() as u32;
241        let c = [color.r, color.g, color.b, color.a];
242
243        // Center vertex
244        self.vertices
245            .push(Vertex::new(center.x, center.y, c[0], c[1], c[2], c[3]));
246
247        for i in 0..CIRCLE_SEGMENTS {
248            let angle = (i as f32 / CIRCLE_SEGMENTS as f32) * std::f32::consts::TAU;
249            let x = center.x + radius * angle.cos();
250            let y = center.y + radius * angle.sin();
251            self.vertices
252                .push(Vertex::new(x, y, c[0], c[1], c[2], c[3]));
253        }
254
255        for i in 0..CIRCLE_SEGMENTS {
256            self.indices.push(base); // center
257            self.indices.push(base + 1 + i);
258            self.indices.push(base + 1 + (i + 1) % CIRCLE_SEGMENTS);
259        }
260    }
261
262    /// Add a stroked circle.
263    pub fn stroke_circle(&mut self, center: Position, radius: f32, color: Color, width: f32) {
264        let outer = radius;
265        let inner = (radius - width).max(0.0);
266        let base = self.vertices.len() as u32;
267        let c = [color.r, color.g, color.b, color.a];
268
269        for i in 0..CIRCLE_SEGMENTS {
270            let angle = (i as f32 / CIRCLE_SEGMENTS as f32) * std::f32::consts::TAU;
271            let cos_a = angle.cos();
272            let sin_a = angle.sin();
273            // outer vertex
274            self.vertices.push(Vertex::new(
275                center.x + outer * cos_a,
276                center.y + outer * sin_a,
277                c[0],
278                c[1],
279                c[2],
280                c[3],
281            ));
282            // inner vertex
283            self.vertices.push(Vertex::new(
284                center.x + inner * cos_a,
285                center.y + inner * sin_a,
286                c[0],
287                c[1],
288                c[2],
289                c[3],
290            ));
291        }
292
293        for i in 0..CIRCLE_SEGMENTS {
294            let i0 = base + i * 2;
295            let i1 = base + i * 2 + 1;
296            let i2 = base + ((i + 1) % CIRCLE_SEGMENTS) * 2;
297            let i3 = base + ((i + 1) % CIRCLE_SEGMENTS) * 2 + 1;
298            self.indices.extend_from_slice(&[i0, i2, i1, i1, i2, i3]);
299        }
300    }
301
302    /// Fill a rectangle with a gradient (linear or radial).
303    ///
304    /// Gradient colors are interpolated across the rectangle's vertices.
305    pub fn fill_rect_gradient(&mut self, rect: Rect, gradient: &Gradient, corner_radius: f32) {
306        match gradient {
307            Gradient::Linear { start, end, stops } => {
308                if stops.is_empty() {
309                    return;
310                }
311                if stops.len() == 1 {
312                    self.fill_rect(rect, stops[0].color, corner_radius);
313                    return;
314                }
315                // Compute gradient direction vector
316                let dx = end.x - start.x;
317                let dy = end.y - start.y;
318                let len_sq = dx * dx + dy * dy;
319                if len_sq < 0.0001 {
320                    self.fill_rect(rect, stops[0].color, corner_radius);
321                    return;
322                }
323                // For simplicity, render as a series of thin horizontal/vertical strips
324                // with interpolated colors. Use a quad-based approach.
325                let strip_count = 16u32;
326                for i in 0..strip_count {
327                    let t0 = i as f32 / strip_count as f32;
328                    let t1 = (i + 1) as f32 / strip_count as f32;
329                    let c0 = sample_gradient(stops, t0);
330                    let c1 = sample_gradient(stops, t1);
331                    let avg = Color::rgba(
332                        (c0.r + c1.r) * 0.5,
333                        (c0.g + c1.g) * 0.5,
334                        (c0.b + c1.b) * 0.5,
335                        (c0.a + c1.a) * 0.5,
336                    );
337                    // Determine strip direction from gradient direction
338                    let strip_rect = if dx.abs() >= dy.abs() {
339                        // Horizontal gradient
340                        let x0 = rect.x + rect.width * t0;
341                        let x1 = rect.x + rect.width * t1;
342                        Rect::new(x0, rect.y, x1 - x0, rect.height)
343                    } else {
344                        // Vertical gradient
345                        let y0 = rect.y + rect.height * t0;
346                        let y1 = rect.y + rect.height * t1;
347                        Rect::new(rect.x, y0, rect.width, y1 - y0)
348                    };
349                    self.push_quad(strip_rect, avg);
350                }
351            }
352            Gradient::Radial {
353                center,
354                radius,
355                stops,
356            } => {
357                if stops.is_empty() {
358                    return;
359                }
360                if stops.len() == 1 {
361                    self.fill_rect(rect, stops[0].color, corner_radius);
362                    return;
363                }
364                // Approximate radial gradient as concentric circles
365                let ring_count = 12u32;
366                for i in (0..ring_count).rev() {
367                    let t = (i + 1) as f32 / ring_count as f32;
368                    let r = radius * t;
369                    let color = sample_gradient(stops, t);
370                    self.fill_circle(*center, r, color);
371                }
372            }
373        }
374    }
375
376    /// Draw a shadow behind a rectangle.
377    pub fn shadow_rect(&mut self, rect: Rect, shadow: &Shadow, corner_radius: f32) {
378        let expand = shadow.blur_radius * 0.5;
379        let shadow_rect = Rect::new(
380            rect.x + shadow.offset_x - expand,
381            rect.y + shadow.offset_y - expand,
382            rect.width + expand * 2.0,
383            rect.height + expand * 2.0,
384        );
385        self.fill_rect(shadow_rect, shadow.color, corner_radius + expand);
386    }
387
388    /// Draw an image (placeholder — textures require a separate pipeline).
389    pub fn draw_image(&mut self, _handle: &ImageHandle, _rect: Rect) {
390        // Image rendering requires a texture atlas and separate pipeline.
391        // This is a placeholder; images are a no-op until the texture pipeline
392        // is added in a future version.
393    }
394
395    /// Add a line segment.
396    pub fn line(&mut self, from: Position, to: Position, color: Color, width: f32) {
397        let dx = to.x - from.x;
398        let dy = to.y - from.y;
399        let len = (dx * dx + dy * dy).sqrt().max(0.001);
400        let half = width * 0.5;
401
402        // Perpendicular direction
403        let nx = -dy / len * half;
404        let ny = dx / len * half;
405
406        let base = self.vertices.len() as u32;
407        let c = [color.r, color.g, color.b, color.a];
408
409        self.vertices.push(Vertex::new(
410            from.x + nx,
411            from.y + ny,
412            c[0],
413            c[1],
414            c[2],
415            c[3],
416        ));
417        self.vertices.push(Vertex::new(
418            from.x - nx,
419            from.y - ny,
420            c[0],
421            c[1],
422            c[2],
423            c[3],
424        ));
425        self.vertices
426            .push(Vertex::new(to.x - nx, to.y - ny, c[0], c[1], c[2], c[3]));
427        self.vertices
428            .push(Vertex::new(to.x + nx, to.y + ny, c[0], c[1], c[2], c[3]));
429
430        self.indices
431            .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
432    }
433
434    /// Flush geometry and encode draw commands into the given render pass.
435    pub fn flush(&mut self, gpu: &GpuContext, pass: &mut wgpu::RenderPass<'_>) {
436        if self.indices.is_empty() {
437            return;
438        }
439
440        let device = gpu.device();
441        let queue = gpu.queue();
442
443        // Upload viewport uniform
444        queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&self.viewport));
445
446        // Grow buffers if needed
447        if self.vertices.len() > self.vertex_capacity {
448            self.vertex_capacity = self.vertices.len().next_power_of_two();
449            self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
450                label: Some("agpu_vertex"),
451                size: (self.vertex_capacity * std::mem::size_of::<Vertex>()) as u64,
452                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
453                mapped_at_creation: false,
454            });
455        }
456        if self.indices.len() > self.index_capacity {
457            self.index_capacity = self.indices.len().next_power_of_two();
458            self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
459                label: Some("agpu_index"),
460                size: (self.index_capacity * std::mem::size_of::<u32>()) as u64,
461                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
462                mapped_at_creation: false,
463            });
464        }
465
466        // Upload geometry
467        queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
468        queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
469
470        pass.set_pipeline(&self.pipeline);
471        pass.set_bind_group(0, &self.uniform_bind_group, &[]);
472        pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
473        pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
474        pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
475    }
476
477    /// Current vertex count for statistics.
478    pub fn vertex_count(&self) -> usize {
479        self.vertices.len()
480    }
481
482    /// Current index/triangle count for statistics.
483    pub fn index_count(&self) -> usize {
484        self.indices.len()
485    }
486
487    /// MSAA sample count configured for this renderer.
488    pub fn sample_count(&self) -> u32 {
489        self.sample_count
490    }
491
492    // ── internal helpers ────────────────────────────────────────────
493
494    fn push_quad(&mut self, rect: Rect, color: Color) {
495        let base = self.vertices.len() as u32;
496        let c = [color.r, color.g, color.b, color.a];
497        let x0 = rect.x;
498        let y0 = rect.y;
499        let x1 = rect.x + rect.width;
500        let y1 = rect.y + rect.height;
501
502        self.vertices
503            .push(Vertex::new(x0, y0, c[0], c[1], c[2], c[3]));
504        self.vertices
505            .push(Vertex::new(x1, y0, c[0], c[1], c[2], c[3]));
506        self.vertices
507            .push(Vertex::new(x1, y1, c[0], c[1], c[2], c[3]));
508        self.vertices
509            .push(Vertex::new(x0, y1, c[0], c[1], c[2], c[3]));
510
511        self.indices
512            .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
513    }
514
515    fn push_rounded_rect(&mut self, rect: Rect, color: Color, radius: f32) {
516        let r = radius.min(rect.width * 0.5).min(rect.height * 0.5);
517        let c = [color.r, color.g, color.b, color.a];
518        let base = self.vertices.len() as u32;
519
520        // Center vertex for fan
521        let cx = rect.x + rect.width * 0.5;
522        let cy = rect.y + rect.height * 0.5;
523        self.vertices
524            .push(Vertex::new(cx, cy, c[0], c[1], c[2], c[3]));
525
526        // Generate outline vertices: straight edges + corner arcs
527        let mut outline: Vec<[f32; 2]> = Vec::with_capacity(4 * 8 + 4);
528
529        // Corner centers
530        let corners = [
531            (
532                rect.x + r,
533                rect.y + r,
534                std::f32::consts::PI,
535                std::f32::consts::FRAC_PI_2 * 3.0,
536            ), // top-left
537            (
538                rect.x + rect.width - r,
539                rect.y + r,
540                std::f32::consts::FRAC_PI_2 * 3.0,
541                std::f32::consts::TAU,
542            ), // top-right
543            (
544                rect.x + rect.width - r,
545                rect.y + rect.height - r,
546                0.0,
547                std::f32::consts::FRAC_PI_2,
548            ), // bottom-right
549            (
550                rect.x + r,
551                rect.y + rect.height - r,
552                std::f32::consts::FRAC_PI_2,
553                std::f32::consts::PI,
554            ), // bottom-left
555        ];
556
557        let arc_segments = 8u32;
558        for (corner_x, corner_y, start_angle, end_angle) in &corners {
559            for j in 0..=arc_segments {
560                let t =
561                    *start_angle + (*end_angle - *start_angle) * (j as f32 / arc_segments as f32);
562                outline.push([corner_x + r * t.cos(), corner_y + r * t.sin()]);
563            }
564        }
565
566        // Add outline vertices
567        for pt in &outline {
568            self.vertices
569                .push(Vertex::new(pt[0], pt[1], c[0], c[1], c[2], c[3]));
570        }
571
572        // Triangle fan from center to outline
573        let n = outline.len() as u32;
574        for i in 0..n {
575            self.indices.push(base); // center
576            self.indices.push(base + 1 + i);
577            self.indices.push(base + 1 + (i + 1) % n);
578        }
579    }
580}
581
582/// Linearly interpolate a gradient's color stops at parameter `t` (0.0–1.0).
583fn sample_gradient(stops: &[GradientStop], t: f32) -> Color {
584    let t = t.clamp(0.0, 1.0);
585    if stops.is_empty() {
586        return Color::WHITE;
587    }
588    if stops.len() == 1 || t <= stops[0].offset {
589        return stops[0].color;
590    }
591    if t >= stops[stops.len() - 1].offset {
592        return stops[stops.len() - 1].color;
593    }
594    for i in 1..stops.len() {
595        if t <= stops[i].offset {
596            let prev = &stops[i - 1];
597            let next = &stops[i];
598            let range = next.offset - prev.offset;
599            if range < 0.0001 {
600                return next.color;
601            }
602            let f = (t - prev.offset) / range;
603            return Color::rgba(
604                prev.color.r + (next.color.r - prev.color.r) * f,
605                prev.color.g + (next.color.g - prev.color.g) * f,
606                prev.color.b + (next.color.b - prev.color.b) * f,
607                prev.color.a + (next.color.a - prev.color.a) * f,
608            );
609        }
610    }
611    stops[stops.len() - 1].color
612}