ranim_render/primitives/
vitems.rs

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