ranim_render/primitives/
vitems.rs

1use crate::utils::{WgpuContext, WgpuVecBuffer};
2use bytemuck::{Pod, Zeroable};
3use glam::{Vec3, Vec4};
4use ranim_core::{
5    components::{rgba::Rgba, width::Width},
6    core_item::vitem::{VItem, vitem_normal_from_points},
7};
8
9/// Per-item metadata stored in a GPU buffer.
10/// Tells shaders where each VItem's data lives in the merged buffers.
11#[repr(C)]
12#[derive(Debug, Default, Clone, Copy, Pod, Zeroable)]
13pub struct ItemInfo {
14    /// Offset into the merged points buffer
15    pub point_offset: u32,
16    /// Number of points for this item
17    pub point_count: u32,
18    /// Offset into the merged attribute buffers (fill_rgbas, stroke_rgbas, stroke_widths)
19    pub attr_offset: u32,
20    /// Number of attributes (= point_count.div_ceil(2))
21    pub attr_count: u32,
22}
23
24/// Per-item plane data (normal + origin), stored as array of structs.
25/// The origin is the first point of the item (used by vertex shader).
26/// basis_u/basis_v are generated deterministically from the normal in the shader.
27#[repr(C)]
28#[derive(Debug, Default, Clone, Copy, Pod, Zeroable)]
29pub struct PlaneData {
30    pub normal: Vec4, // xyz = normal, w = pad
31    pub origin: Vec4, // xyz = first point, w = pad
32}
33
34/// Merged GPU buffers for all VItems in a frame.
35///
36/// Instead of one set of buffers per VItem, all data is packed into
37/// contiguous arrays with an index table (`item_infos`) that tells
38/// shaders where each item's data lives.
39pub struct VItemsBuffer {
40    /// Per-item metadata: offsets and counts
41    pub(crate) item_infos_buffer: WgpuVecBuffer<ItemInfo>,
42    /// Per-item plane data (normal + origin for vertex shader)
43    pub(crate) planes_buffer: WgpuVecBuffer<PlaneData>,
44    /// Per-item clip boxes (5 i32 each: min_x, max_x, min_y, max_y, max_w)
45    pub(crate) clip_boxes_buffer: WgpuVecBuffer<i32>,
46
47    /// Merged 3D points from all VItems
48    pub(crate) points3d_buffer: WgpuVecBuffer<Vec4>,
49    /// Merged 2D projected points (written by compute shader)
50    pub(crate) points2d_buffer: WgpuVecBuffer<Vec4>,
51    /// Merged fill colors
52    pub(crate) fill_rgbas_buffer: WgpuVecBuffer<Rgba>,
53    /// Merged stroke colors
54    pub(crate) stroke_rgbas_buffer: WgpuVecBuffer<Rgba>,
55    /// Merged stroke widths
56    pub(crate) stroke_widths_buffer: WgpuVecBuffer<Width>,
57
58    /// Number of items
59    pub(crate) item_count: u32,
60    /// Total number of points across all items
61    pub(crate) total_points: u32,
62
63    /// Compute bind group (recreated when buffers resize)
64    pub(crate) compute_bind_group: Option<wgpu::BindGroup>,
65    /// Render bind group (recreated when buffers resize)
66    pub(crate) render_bind_group: Option<wgpu::BindGroup>,
67}
68
69impl VItemsBuffer {
70    pub fn new(ctx: &WgpuContext) -> Self {
71        // Start with empty buffers (minimum size 1 to avoid zero-size buffer)
72        let storage_rw = wgpu::BufferUsages::STORAGE
73            | wgpu::BufferUsages::COPY_DST
74            | wgpu::BufferUsages::COPY_SRC;
75        let storage_ro = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST;
76
77        Self {
78            item_infos_buffer: WgpuVecBuffer::new(ctx, Some("Merged ItemInfos"), storage_ro, 1),
79            planes_buffer: WgpuVecBuffer::new(ctx, Some("Merged Planes"), storage_ro, 1),
80            clip_boxes_buffer: WgpuVecBuffer::new(ctx, Some("Merged ClipBoxes"), storage_rw, 5),
81            points3d_buffer: WgpuVecBuffer::new(ctx, Some("Merged Points3D"), storage_ro, 1),
82            points2d_buffer: WgpuVecBuffer::new(ctx, Some("Merged Points2D"), storage_rw, 1),
83            fill_rgbas_buffer: WgpuVecBuffer::new(ctx, Some("Merged FillRgbas"), storage_ro, 1),
84            stroke_rgbas_buffer: WgpuVecBuffer::new(ctx, Some("Merged StrokeRgbas"), storage_ro, 1),
85            stroke_widths_buffer: WgpuVecBuffer::new(
86                ctx,
87                Some("Merged StrokeWidths"),
88                storage_ro,
89                1,
90            ),
91            item_count: 0,
92            total_points: 0,
93            compute_bind_group: None,
94            render_bind_group: None,
95        }
96    }
97
98    /// Pack all VItems into the merged buffers. Called once per frame.
99    pub fn update(&mut self, ctx: &WgpuContext, vitems: &[VItem]) {
100        if vitems.is_empty() {
101            self.item_count = 0;
102            self.total_points = 0;
103            return;
104        }
105
106        let item_count = vitems.len();
107
108        // Pre-calculate total sizes
109        let total_points: usize = vitems.iter().map(|v| v.points.len()).sum();
110        let total_attrs: usize = vitems.iter().map(|v| v.points.len().div_ceil(2)).sum();
111
112        // Build index table and collect data
113        let mut item_infos = Vec::with_capacity(item_count);
114        let mut planes = Vec::with_capacity(item_count);
115        let mut all_points3d = Vec::with_capacity(total_points);
116        let mut all_fill_rgbas = Vec::with_capacity(total_attrs);
117        let mut all_stroke_rgbas = Vec::with_capacity(total_attrs);
118        let mut all_stroke_widths = Vec::with_capacity(total_attrs);
119
120        let mut point_offset: u32 = 0;
121        let mut attr_offset: u32 = 0;
122
123        for vitem in vitems {
124            let pc = vitem.points.len() as u32;
125            let ac = pc.div_ceil(2);
126
127            item_infos.push(ItemInfo {
128                point_offset,
129                point_count: pc,
130                attr_offset,
131                attr_count: ac,
132            });
133
134            let normal = vitem
135                .normal
136                .unwrap_or_else(|| vitem_normal_from_points(&vitem.points));
137            let origin = Vec3::new(vitem.points[0].x, vitem.points[0].y, vitem.points[0].z);
138            planes.push(PlaneData {
139                normal: Vec4::from((normal, 0.0)),
140                origin: Vec4::from((origin, 0.0)),
141            });
142
143            all_points3d.extend_from_slice(&vitem.points);
144            all_fill_rgbas.extend_from_slice(&vitem.fill_rgbas);
145            all_stroke_rgbas.extend_from_slice(&vitem.stroke_rgbas);
146            all_stroke_widths.extend_from_slice(&vitem.stroke_widths);
147
148            point_offset += pc;
149            attr_offset += ac;
150        }
151
152        // Build clip_boxes initial values: [MAX, MIN, MAX, MIN, 0] per item
153        let mut clip_boxes = Vec::with_capacity(item_count * 5);
154        for _ in 0..item_count {
155            clip_boxes.extend_from_slice(&[i32::MAX, i32::MIN, i32::MAX, i32::MIN, 0]);
156        }
157
158        // Points2d: zeroed, same size as points3d
159        let points2d = vec![Vec4::ZERO; total_points];
160
161        self.item_count = item_count as u32;
162        self.total_points = total_points as u32;
163
164        // Upload all data — track if any buffer was reallocated
165        let mut any_realloc = false;
166        any_realloc |= self.item_infos_buffer.set(ctx, &item_infos);
167        any_realloc |= self.planes_buffer.set(ctx, &planes);
168        any_realloc |= self.clip_boxes_buffer.set(ctx, &clip_boxes);
169        any_realloc |= self.points3d_buffer.set(ctx, &all_points3d);
170        any_realloc |= self.points2d_buffer.set(ctx, &points2d);
171        any_realloc |= self.fill_rgbas_buffer.set(ctx, &all_fill_rgbas);
172        any_realloc |= self.stroke_rgbas_buffer.set(ctx, &all_stroke_rgbas);
173        any_realloc |= self.stroke_widths_buffer.set(ctx, &all_stroke_widths);
174
175        // Recreate bind groups if any buffer was reallocated
176        if any_realloc || self.compute_bind_group.is_none() {
177            self.compute_bind_group = Some(Self::create_compute_bind_group(ctx, self));
178            self.render_bind_group = Some(Self::create_render_bind_group(ctx, self));
179        }
180    }
181
182    pub fn item_count(&self) -> u32 {
183        self.item_count
184    }
185
186    pub fn total_points(&self) -> u32 {
187        self.total_points
188    }
189
190    // MARK: Bind group layouts
191
192    pub fn compute_bind_group_layout(ctx: &WgpuContext) -> wgpu::BindGroupLayout {
193        ctx.device
194            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
195                label: Some("Merged VItem Compute BGL"),
196                entries: &[
197                    // binding 0: item_infos (read-only)
198                    bgl_entry(0, wgpu::ShaderStages::COMPUTE, false),
199                    // binding 1: planes (read-only)
200                    bgl_entry(1, wgpu::ShaderStages::COMPUTE, false),
201                    // binding 2: points3d (read-only)
202                    bgl_entry(2, wgpu::ShaderStages::COMPUTE, false),
203                    // binding 3: stroke_widths (read-only)
204                    bgl_entry(3, wgpu::ShaderStages::COMPUTE, false),
205                    // binding 4: points2d (read-write)
206                    bgl_entry(4, wgpu::ShaderStages::COMPUTE, true),
207                    // binding 5: clip_boxes (read-write)
208                    bgl_entry(5, wgpu::ShaderStages::COMPUTE, true),
209                ],
210            })
211    }
212
213    pub fn render_bind_group_layout(ctx: &WgpuContext) -> wgpu::BindGroupLayout {
214        let vf = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
215        let v = wgpu::ShaderStages::VERTEX;
216        ctx.device
217            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
218                label: Some("Merged VItem Render BGL"),
219                entries: &[
220                    // binding 0: item_infos
221                    bgl_entry(0, vf, false),
222                    // binding 1: planes (normal + origin)
223                    bgl_entry(1, v, false),
224                    // binding 2: clip_boxes
225                    bgl_entry(2, v, false),
226                    // binding 3: points2d
227                    bgl_entry(3, vf, false),
228                    // binding 4: fill_rgbas
229                    bgl_entry(4, vf, false),
230                    // binding 5: stroke_rgbas
231                    bgl_entry(5, vf, false),
232                    // binding 6: stroke_widths
233                    bgl_entry(6, vf, false),
234                ],
235            })
236    }
237
238    fn create_compute_bind_group(ctx: &WgpuContext, this: &Self) -> wgpu::BindGroup {
239        ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
240            label: Some("Merged VItem Compute BG"),
241            layout: &Self::compute_bind_group_layout(ctx),
242            entries: &[
243                bg_entry(0, &this.item_infos_buffer.buffer),
244                bg_entry(1, &this.planes_buffer.buffer),
245                bg_entry(2, &this.points3d_buffer.buffer),
246                bg_entry(3, &this.stroke_widths_buffer.buffer),
247                bg_entry(4, &this.points2d_buffer.buffer),
248                bg_entry(5, &this.clip_boxes_buffer.buffer),
249            ],
250        })
251    }
252
253    fn create_render_bind_group(ctx: &WgpuContext, this: &Self) -> wgpu::BindGroup {
254        ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
255            label: Some("Merged VItem Render BG"),
256            layout: &Self::render_bind_group_layout(ctx),
257            entries: &[
258                bg_entry(0, &this.item_infos_buffer.buffer),
259                bg_entry(1, &this.planes_buffer.buffer),
260                bg_entry(2, &this.clip_boxes_buffer.buffer),
261                bg_entry(3, &this.points2d_buffer.buffer),
262                bg_entry(4, &this.fill_rgbas_buffer.buffer),
263                bg_entry(5, &this.stroke_rgbas_buffer.buffer),
264                bg_entry(6, &this.stroke_widths_buffer.buffer),
265            ],
266        })
267    }
268}
269
270fn bgl_entry(
271    binding: u32,
272    visibility: wgpu::ShaderStages,
273    read_write: bool,
274) -> wgpu::BindGroupLayoutEntry {
275    wgpu::BindGroupLayoutEntry {
276        binding,
277        visibility,
278        ty: wgpu::BindingType::Buffer {
279            ty: wgpu::BufferBindingType::Storage {
280                read_only: !read_write,
281            },
282            has_dynamic_offset: false,
283            min_binding_size: None,
284        },
285        count: None,
286    }
287}
288
289fn bg_entry(binding: u32, buffer: &wgpu::Buffer) -> wgpu::BindGroupEntry<'_> {
290    wgpu::BindGroupEntry {
291        binding,
292        resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),
293    }
294}