Skip to main content

optic_render/handles/
instance.rs

1use optic_core::{OpticError, OpticErrorKind, OpticResult};
2use cgmath::{InnerSpace, Matrix4, Vector3, Vector4};
3
4use crate::asset::attr::{
5    ATTRInfo, ATTRName, ColATTR, CustomATTR, DataType, Pos2DATTR, Pos3DATTR, Rot2DATTR, Rot3DATTR,
6    Scale2DATTR, Scale3DATTR,
7};
8
9// ── Attribute helper for interleaved byte-level access ─────────────────────
10
11/// Describes one attribute within the interleaved instance stride: its byte
12/// offset within one instance's data, its byte size (elem_count * byte_count),
13/// and its ATTRInfo for GL binding.
14#[derive(Clone, Debug)]
15struct AttrSlot {
16    offset: usize,
17    size: usize,
18    info: ATTRInfo,
19}
20
21fn build_slots(
22    pos: Option<&Pos3DATTR>,
23    rot: Option<&Rot3DATTR>,
24    scale: Option<&Scale3DATTR>,
25    col: Option<&ColATTR>,
26    custom: &[CustomATTR],
27) -> Vec<AttrSlot> {
28    let mut slots = Vec::new();
29    let mut offset = 0usize;
30
31    let mut push = |info: ATTRInfo| {
32        let size = info.elem_count * info.byte_count;
33        slots.push(AttrSlot { offset, size, info });
34        offset += size;
35    };
36
37    if let Some(a) = pos {
38        if !a.is_empty() {
39            push(a.info.clone());
40        }
41    }
42    if let Some(a) = rot {
43        if !a.is_empty() {
44            push(a.info.clone());
45        }
46    }
47    if let Some(a) = scale {
48        if !a.is_empty() {
49            push(a.info.clone());
50        }
51    }
52    if let Some(a) = col {
53        if !a.is_empty() {
54            push(a.info.clone());
55        }
56    }
57    for c in custom {
58        push(c.info.clone());
59    }
60
61    slots
62}
63
64fn interleave(slots: &[AttrSlot], attrs: &[&[u8]], count: usize) -> Vec<u8> {
65    let stride: usize = slots.iter().map(|s| s.size).sum();
66    let mut buf = vec![0u8; count * stride];
67    for i in 0..count {
68        for (slot, data) in slots.iter().zip(attrs.iter()) {
69            let start = i * slot.size;
70            let end = start + slot.size;
71            let dst = i * stride + slot.offset;
72            buf[dst..dst + slot.size].copy_from_slice(&data[start..end]);
73        }
74    }
75    buf
76}
77
78// ── InstanceKind ──────────────────────────────────────────────────────────
79
80#[derive(Clone, Debug)]
81pub(crate) struct CustomSlot {
82    pub name: String,
83    pub byte_offset: usize,
84    pub byte_size: usize,
85    pub typ: optic_core::ATTRType,
86    pub elem_count: u32,
87}
88
89#[derive(Clone, Debug)]
90pub(crate) struct InstanceKind {
91    pub has_pos: bool,
92    pub has_rot: bool,
93    pub has_scale: bool,
94    pub has_col: bool,
95    pub custom_offsets: Vec<CustomSlot>,
96}
97
98// ── InstanceDesc3D ────────────────────────────────────────────────────────
99
100/// Descriptor for preparing 3D instance data before uploading it to the GPU.
101///
102/// An instance buffer packs per-instance attributes (position, rotation, scale,
103/// colour, and custom attributes) into a single interleaved GPU buffer. This
104/// type collects attribute data on the CPU side and interleaves them when
105/// [`ship`](InstanceDesc3D::ship) is called.
106///
107/// # Attribute layout
108///
109/// Attributes appear in this fixed order within the interleaved stride:
110///
111/// 1. `pos` — 3 × `f32` (12 bytes)
112/// 2. `rot` — 4 × `f32` quaternion (16 bytes)
113/// 3. `scale` — 3 × `f32` (12 bytes)
114/// 4. `col` — 4 × `f32` RGBA (16 bytes)
115/// 5. custom attributes, in insertion order
116///
117/// Any of these may be omitted (left empty). The stride shrinks accordingly.
118///
119/// # Example
120///
121/// ```
122/// # use optic_render::handles::InstanceDesc3D;
123/// # use cgmath::Vector3;
124/// let mut desc = InstanceDesc3D::empty();
125///
126/// for i in 0..100 {
127///     desc.pos_attr.push([i as f32 * 2.0, 0.0, 0.0]);
128///     desc.col_attr.push([1.0, 0.0, 0.0, 1.0]);
129/// }
130///
131/// // desc.ship() transfers the interleaved data to a GPU buffer.
132/// ```
133pub struct InstanceDesc3D {
134    pub pos_attr: Pos3DATTR,
135    pub rot_attr: Rot3DATTR,
136    pub scale_attr: Scale3DATTR,
137    pub col_attr: ColATTR,
138    pub cus_attrs: Vec<CustomATTR>,
139}
140
141impl InstanceDesc3D {
142    /// Creates an empty descriptor with no attributes.
143    ///
144    /// Push per-instance data into the individual attribute fields before calling
145    /// [`ship`](InstanceDesc3D::ship).
146    pub fn empty() -> Self {
147        Self {
148            pos_attr: Pos3DATTR::empty(),
149            rot_attr: Rot3DATTR::empty(),
150            scale_attr: Scale3DATTR::empty(),
151            col_attr: ColATTR::empty(),
152            cus_attrs: Vec::new(),
153        }
154    }
155
156    /// Builds a descriptor from an array of 3D positions.
157    ///
158    /// All other attribute fields remain empty.
159    pub fn from_positions(positions: &[Vector3<f32>]) -> Self {
160        let mut desc = Self::empty();
161        for p in positions {
162            desc.pos_attr.push([p.x, p.y, p.z]);
163        }
164        desc
165    }
166
167    /// Decomposes a slice of 4×4 transformation matrices into position, rotation
168    /// (quaternion), and scale attributes.
169    ///
170    /// This is a convenience constructor for users who already have transform
171    /// matrices. It assumes no shear and extracts a unit quaternion from the
172    /// upper-left 3×3 sub-matrix.
173    ///
174    /// # Panics
175    ///
176    /// May produce degenerate quaternions when a scale component is zero.
177    pub fn from_transforms(transforms: &[Matrix4<f32>]) -> Self {
178        let mut desc = Self::empty();
179        for m in transforms {
180            desc.pos_attr.push([m[3][0], m[3][1], m[3][2]]);
181
182            let sx = Vector3::new(m[0][0], m[1][0], m[2][0]).magnitude();
183            let sy = Vector3::new(m[0][1], m[1][1], m[2][1]).magnitude();
184            let sz = Vector3::new(m[0][2], m[1][2], m[2][2]).magnitude();
185            desc.scale_attr.push([sx, sy, sz]);
186
187            let r00 = m[0][0] / sx;
188            let r01 = m[0][1] / sy;
189            let r02 = m[0][2] / sz;
190            let r10 = m[1][0] / sx;
191            let r11 = m[1][1] / sy;
192            let r12 = m[1][2] / sz;
193            let r20 = m[2][0] / sx;
194            let r21 = m[2][1] / sy;
195            let r22 = m[2][2] / sz;
196
197            let trace = r00 + r11 + r22;
198            if trace > 0.0 {
199                let s = (trace + 1.0).sqrt() * 2.0;
200                desc.rot_attr.push([(r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s, s / 4.0]);
201            } else if r00 > r11 && r00 > r22 {
202                let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
203                desc.rot_attr.push([s / 4.0, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s]);
204            } else if r11 > r22 {
205                let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
206                desc.rot_attr.push([(r01 + r10) / s, s / 4.0, (r12 + r21) / s, (r02 - r20) / s]);
207            } else {
208                let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
209                desc.rot_attr.push([(r02 + r20) / s, (r12 + r21) / s, s / 4.0, (r10 - r01) / s]);
210            }
211        }
212        desc
213    }
214
215    /// Appends a custom (user-defined) attribute.
216    ///
217    /// Custom attribute names must not collide with the reserved names `iPos`,
218    /// `iRot`, `iScale`, or `iColor`, and must be unique among themselves.
219    ///
220    /// Returns `&mut self` for chaining.
221    pub fn attach_custom_attr(&mut self, attr: CustomATTR) -> &mut Self {
222        self.cus_attrs.push(attr);
223        self
224    }
225
226    /// Interleaves all non-empty attributes and uploads them to a new GPU buffer.
227    ///
228    /// Returns an [`InstanceBuffer`] ready for use in instanced draws.
229    ///
230    /// # Errors
231    ///
232    /// - Returns an error if all attributes are empty (no data to upload).
233    /// - Returns an error if non-empty attributes have mismatched element counts.
234    /// - Returns an error if custom attribute names collide with reserved names
235    ///   or each other.
236    pub fn ship(&self) -> OpticResult<InstanceBuffer> {
237        let count = self.resolve_count();
238        let has_any_attr = self.pos_attr.is_empty()
239            && self.rot_attr.is_empty()
240            && self.scale_attr.is_empty()
241            && self.col_attr.is_empty()
242            && self.cus_attrs.is_empty();
243
244        if has_any_attr {
245            return Err(OpticError::new(
246                OpticErrorKind::Custom,
247                "cannot ship an instance buffer with zero attributes populated",
248            ));
249        }
250
251        self.verify_counts()?;
252        self.verify_custom_names()?;
253
254        let slots = build_slots(
255            Some(&self.pos_attr),
256            Some(&self.rot_attr),
257            Some(&self.scale_attr),
258            Some(&self.col_attr),
259            &self.cus_attrs,
260        );
261
262        let stride: usize = slots.iter().map(|s| s.size).sum();
263        let instance_count = count.unwrap_or(0);
264
265        let mut raw: Vec<&[u8]> = Vec::new();
266        if !self.pos_attr.is_empty() {
267            raw.push(self.pos_attr.data.as_bytes());
268        }
269        if !self.rot_attr.is_empty() {
270            raw.push(self.rot_attr.data.as_bytes());
271        }
272        if !self.scale_attr.is_empty() {
273            raw.push(self.scale_attr.data.as_bytes());
274        }
275        if !self.col_attr.is_empty() {
276            raw.push(self.col_attr.data.as_bytes());
277        }
278        for c in &self.cus_attrs {
279            raw.push(&c.data);
280        }
281
282        let cpu_mirror = if instance_count > 0 {
283            interleave(&slots, &raw, instance_count)
284        } else {
285            Vec::new()
286        };
287
288        let layouts: Vec<(ATTRInfo, u32)> = slots.iter().enumerate().map(|(i, s)| (s.info.clone(), i as u32)).collect();
289
290        let mut custom_offsets = Vec::new();
291        for c in &self.cus_attrs {
292            let off = slots.iter()
293                .position(|s| s.info.name == c.info.name)
294                .map(|idx| slots[idx].offset)
295                .unwrap_or(0);
296            custom_offsets.push(CustomSlot {
297                name: match &c.info.name { ATTRName::Custom(n) => n.clone(), _ => String::new() },
298                byte_offset: off,
299                byte_size: c.info.elem_count * c.info.byte_count,
300                typ: c.info.typ.clone(),
301                elem_count: c.info.elem_count as u32,
302            });
303        }
304
305        let kind = InstanceKind {
306            has_pos: !self.pos_attr.is_empty(),
307            has_rot: !self.rot_attr.is_empty(),
308            has_scale: !self.scale_attr.is_empty(),
309            has_col: !self.col_attr.is_empty(),
310            custom_offsets,
311        };
312
313        let buf_id = create_instance_buffer();
314        if !cpu_mirror.is_empty() {
315            upload_instance_data(buf_id, &cpu_mirror);
316        }
317
318        let capacity = if instance_count > 0 { instance_count as u32 } else { 0 };
319
320        Ok(InstanceBuffer {
321            buf_id,
322            capacity,
323            count: instance_count as u32,
324            stride: stride as u32,
325            layouts,
326            cpu_mirror,
327            kind,
328        })
329    }
330
331    fn resolve_count(&self) -> Option<usize> {
332        for attr in [&self.pos_attr.data as &dyn AsCount, &self.rot_attr.data, &self.scale_attr.data, &self.col_attr.data] {
333            if !attr.is_empty() {
334                return Some(attr.len());
335            }
336        }
337        for c in &self.cus_attrs {
338            if !c.is_empty() {
339                let elem_size = c.info.elem_count * c.info.byte_count;
340                return Some(c.data.len() / elem_size);
341            }
342        }
343        None
344    }
345
346    fn verify_counts(&self) -> OpticResult<()> {
347        let count = match self.resolve_count() {
348            Some(c) => c,
349            None => return Ok(()),
350        };
351
352        macro_rules! check {
353            ($attr:expr, $name:expr) => {
354                if !$attr.is_empty() && $attr.data.len() != count {
355                    return Err(OpticError::new(
356                        OpticErrorKind::Custom,
357                        &format!(
358                            "instance attribute count mismatch: {} has {} elements, expected {}",
359                            $name,
360                            $attr.data.len(),
361                            count
362                        ),
363                    ));
364                }
365            };
366        }
367        check!(self.pos_attr, "pos_attr");
368        check!(self.rot_attr, "rot_attr");
369        check!(self.scale_attr, "scale_attr");
370        check!(self.col_attr, "col_attr");
371
372        for c in &self.cus_attrs {
373            let elem_size = c.info.elem_count * c.info.byte_count;
374            let c_count = if elem_size > 0 { c.data.len() / elem_size } else { 0 };
375            if c_count != count {
376                let name = match &c.info.name { ATTRName::Custom(n) => n.clone(), _ => "unknown".into() };
377                return Err(OpticError::new(
378                    OpticErrorKind::Custom,
379                    &format!(
380                        "instance attribute count mismatch: custom attr \"{name}\" has {c_count} elements, expected {count}"
381                    ),
382                ));
383            }
384        }
385
386        Ok(())
387    }
388
389    fn verify_custom_names(&self) -> OpticResult<()> {
390        let reserved = ["iPos", "iRot", "iScale", "iColor"];
391        for c in &self.cus_attrs {
392            let name = match &c.info.name {
393                ATTRName::Custom(n) => n.as_str(),
394                _ => continue,
395            };
396            if reserved.contains(&name) {
397                return Err(OpticError::new(
398                    OpticErrorKind::Custom,
399                    &format!("custom attribute name \"{name}\" collides with reserved instance attribute name"),
400                ));
401            }
402        }
403        for i in 0..self.cus_attrs.len() {
404            for j in i + 1..self.cus_attrs.len() {
405                let ni = match &self.cus_attrs[i].info.name { ATTRName::Custom(n) => n, _ => continue };
406                let nj = match &self.cus_attrs[j].info.name { ATTRName::Custom(n) => n, _ => continue };
407                if ni == nj {
408                    return Err(OpticError::new(
409                        OpticErrorKind::Custom,
410                        &format!("duplicate custom attribute name \"{ni}\""),
411                    ));
412                }
413            }
414        }
415        Ok(())
416    }
417}
418
419// ── InstanceDesc2D ────────────────────────────────────────────────────────
420
421/// Descriptor for preparing 2D instance data before uploading it to the GPU.
422///
423/// Like [`InstanceDesc3D`], but uses 2-element vectors for position and scale
424/// and single-precision scalars for rotation (angle in radians). The same
425/// interleaving, validation, and upload semantics apply.
426///
427/// # Attribute layout
428///
429/// 1. `pos` — 2 × `f32` (8 bytes)
430/// 2. `rot` — 1 × `f32` angle (4 bytes)
431/// 3. `scale` — 2 × `f32` (8 bytes)
432/// 4. `col` — 4 × `f32` RGBA (16 bytes)
433/// 5. custom attributes, in insertion order
434pub struct InstanceDesc2D {
435    pub pos_attr: Pos2DATTR,
436    pub rot_attr: Rot2DATTR,
437    pub scale_attr: Scale2DATTR,
438    pub col_attr: ColATTR,
439    pub cus_attrs: Vec<CustomATTR>,
440}
441
442impl InstanceDesc2D {
443    /// Creates an empty 2D descriptor.
444    pub fn empty() -> Self {
445        Self {
446            pos_attr: Pos2DATTR::empty(),
447            rot_attr: Rot2DATTR::empty(),
448            scale_attr: Scale2DATTR::empty(),
449            col_attr: ColATTR::empty(),
450            cus_attrs: Vec::new(),
451        }
452    }
453
454    /// Appends a custom attribute for chaining.
455    pub fn attach_custom_attr(&mut self, attr: CustomATTR) -> &mut Self {
456        self.cus_attrs.push(attr);
457        self
458    }
459
460    /// Interleaves all non-empty 2D attributes and uploads to a new GPU buffer.
461    ///
462    /// # Errors
463    ///
464    /// Same error conditions as [`InstanceDesc3D::ship`].
465    pub fn ship(&self) -> OpticResult<InstanceBuffer> {
466        let has_any_attr = self.pos_attr.is_empty()
467            && self.rot_attr.is_empty()
468            && self.scale_attr.is_empty()
469            && self.col_attr.is_empty()
470            && self.cus_attrs.is_empty();
471
472        if has_any_attr {
473            return Err(OpticError::new(
474                OpticErrorKind::Custom,
475                "cannot ship an instance buffer with zero attributes populated",
476            ));
477        }
478
479        let mut slots = Vec::new();
480        let mut offset = 0usize;
481
482        let mut push = |info: ATTRInfo| {
483            let size = info.elem_count * info.byte_count;
484            slots.push(AttrSlot { offset, size, info: info.clone() });
485            offset += size;
486        };
487
488        if !self.pos_attr.is_empty() { push(self.pos_attr.info.clone()); }
489        if !self.rot_attr.is_empty() { push(self.rot_attr.info.clone()); }
490        if !self.scale_attr.is_empty() { push(self.scale_attr.info.clone()); }
491        if !self.col_attr.is_empty() { push(self.col_attr.info.clone()); }
492        for c in &self.cus_attrs { push(c.info.clone()); }
493
494        let stride = offset;
495        let count = self.resolve_count();
496        let instance_count = count.unwrap_or(0);
497
498        let mut raw: Vec<&[u8]> = Vec::new();
499        if !self.pos_attr.is_empty() { raw.push(self.pos_attr.data.as_bytes()); }
500        if !self.rot_attr.is_empty() { raw.push(self.rot_attr.data.as_bytes()); }
501        if !self.scale_attr.is_empty() { raw.push(self.scale_attr.data.as_bytes()); }
502        if !self.col_attr.is_empty() { raw.push(self.col_attr.data.as_bytes()); }
503        for c in &self.cus_attrs { raw.push(&c.data); }
504
505        let cpu_mirror = if instance_count > 0 {
506            interleave(&slots, &raw, instance_count)
507        } else {
508            Vec::new()
509        };
510
511        let layouts: Vec<(ATTRInfo, u32)> = slots.iter().enumerate().map(|(i, s)| (s.info.clone(), i as u32)).collect();
512
513        let mut custom_offsets = Vec::new();
514        for c in &self.cus_attrs {
515            let off = slots.iter()
516                .position(|s| s.info.name == c.info.name)
517                .map(|idx| slots[idx].offset)
518                .unwrap_or(0);
519            custom_offsets.push(CustomSlot {
520                name: match &c.info.name { ATTRName::Custom(n) => n.clone(), _ => String::new() },
521                byte_offset: off,
522                byte_size: c.info.elem_count * c.info.byte_count,
523                typ: c.info.typ.clone(),
524                elem_count: c.info.elem_count as u32,
525            });
526        }
527
528        let kind = InstanceKind {
529            has_pos: !self.pos_attr.is_empty(),
530            has_rot: !self.rot_attr.is_empty(),
531            has_scale: !self.scale_attr.is_empty(),
532            has_col: !self.col_attr.is_empty(),
533            custom_offsets,
534        };
535
536        let buf_id = create_instance_buffer();
537        if !cpu_mirror.is_empty() {
538            upload_instance_data(buf_id, &cpu_mirror);
539        }
540
541        let capacity = if instance_count > 0 { instance_count as u32 } else { 0 };
542
543        Ok(InstanceBuffer {
544            buf_id,
545            capacity,
546            count: instance_count as u32,
547            stride: stride as u32,
548            layouts,
549            cpu_mirror,
550            kind,
551        })
552    }
553
554    fn resolve_count(&self) -> Option<usize> {
555        for attr in [&self.pos_attr.data as &dyn AsCount, &self.col_attr.data] {
556            if !attr.is_empty() {
557                return Some(attr.len());
558            }
559        }
560        for c in &self.cus_attrs {
561            if !c.is_empty() {
562                let elem_size = c.info.elem_count * c.info.byte_count;
563                return Some(c.data.len() / elem_size);
564            }
565        }
566        None
567    }
568}
569
570// ── InstanceBuffer ────────────────────────────────────────────────────────
571
572/// A GPU buffer of interleaved per-instance attributes with a CPU-side mirror
573/// for random access.
574///
575/// Created by [`InstanceDesc3D::ship`] or [`InstanceDesc2D::ship`]. Each
576/// instance is a single packed row of attributes (position, rotation, scale,
577/// colour, and custom data). The buffer is designed for use with
578/// `glDrawArraysInstanced` / `glDrawElementsInstanced`.
579///
580/// # CPU mirror
581///
582/// An [`InstanceBuffer`] keeps a complete CPU copy of all instance data. This
583/// enables reads and partial writes without a GPU round-trip. Every mutating
584/// method writes through to both the CPU mirror and the GPU buffer
585/// transparently.
586///
587/// | Read method | Source | Latency |
588/// |---|---|---|
589/// | [`get_position`], [`get_color`], [`get_instance`] | CPU mirror | Instant |
590/// | _Read from GPU_ | Not supported | — |
591///
592/// # Growth
593///
594/// The buffer grows or shrinks in place via [`set_instance_count`]. New slots
595/// are filled with defaults:
596///
597/// | Attribute | Default |
598/// |---|---|
599/// | Position | `(0, 0, 0)` |
600/// | Rotation | Identity quaternion `(0, 0, 0, 1)` |
601/// | Scale | `(1, 1, 1)` |
602/// | Colour | White `(1, 1, 1, 1)` |
603///
604/// # Convenience methods
605///
606/// | You want to… | Use |
607/// |---|---|
608/// | Move an instance | [`set_position`] |
609/// | Rotate an instance | [`set_rotation`] |
610/// | Scale an instance | [`set_scale`] |
611/// | Recolour an instance | [`set_color`] |
612/// | Write raw bytes | [`update_instance`] |
613///
614/// # Example
615///
616/// ```ignore
617/// use optic_render::handles::{InstanceBuffer, InstanceDesc3D};
618/// use cgmath::Vector3;
619///
620/// // Create 100 red instances along the x-axis
621/// let mut desc = InstanceDesc3D::empty();
622/// for i in 0..100 {
623///     desc.pos_attr.push([i as f32 * 2.0, 0.0, 0.0]);
624///     desc.col_attr.push([1.0, 0.0, 0.0, 1.0]);
625/// }
626/// let mut buffer = desc.ship()?;
627///
628/// // Re-position instance 5
629/// buffer.set_position(5, Vector3::new(20.0, 0.0, 0.0))?;
630///
631/// // Add 50 more instances at the end
632/// buffer.set_instance_count(150);
633/// ```
634pub struct InstanceBuffer {
635    pub(crate) buf_id: u32,
636    pub(crate) capacity: u32,
637    pub(crate) count: u32,
638    pub(crate) stride: u32,
639    pub layouts: Vec<(ATTRInfo, u32)>,
640    pub(crate) cpu_mirror: Vec<u8>,
641    pub(crate) kind: InstanceKind,
642}
643
644impl InstanceBuffer {
645    /// Returns the number of active instances.
646    pub fn count(&self) -> u32 { self.count }
647
648    /// Returns the total capacity (allocated slots, may be larger than count).
649    pub fn capacity(&self) -> u32 { self.capacity }
650
651    /// Updates a single attribute of one instance by attribute index.
652    ///
653    /// This is the lowest-level update — it writes raw bytes into both the CPU
654    /// mirror and the GPU buffer in one operation. The `attr_index` refers to
655    /// the attribute's position in the interleaved layout (0 = first attribute).
656    ///
657    /// For convenience wrappers, see:
658    ///
659    /// | Attribute | Convenience method |
660    /// |---|---|
661    /// | Position (3D) | [`set_position`](Self::set_position) |
662    /// | Rotation (3D) | [`set_rotation`](Self::set_rotation) |
663    /// | Scale (3D) | [`set_scale`](Self::set_scale) |
664    /// | Colour | [`set_color`](Self::set_color) |
665    /// | Custom (by name) | [`update_custom`](Self::update_custom) |
666    ///
667    /// # Type safety
668    ///
669    /// `value` must be a [`DataType`] whose byte count, element count, and
670    /// format exactly match the attribute's declared type. A mismatch produces
671    /// a descriptive error at runtime.
672    ///
673    /// # Errors
674    ///
675    /// - `index` >= `count` — instance index out of bounds.
676    /// - `attr_index` out of range — invalid attribute slot.
677    /// - `D`'s type parameters do not match the attribute slot.
678    pub fn update_instance<D: DataType>(&mut self, index: u32, attr_index: usize, value: D) -> OpticResult<()> {
679        if index >= self.count {
680            return Err(OpticError::new(OpticErrorKind::Custom, &format!("instance index {index} out of bounds (count: {})", self.count)));
681        }
682        if attr_index >= self.layouts.len() {
683            return Err(OpticError::new(OpticErrorKind::Custom, &format!("attr index {attr_index} out of bounds (layout count: {})", self.layouts.len())));
684        }
685
686        let slot_info = &self.layouts[attr_index].0;
687        if slot_info.byte_count != D::BYTE_COUNT || slot_info.elem_count != D::ELEM_COUNT || slot_info.typ != D::ATTR_FORMAT {
688            return Err(OpticError::new(
689                OpticErrorKind::Custom,
690                &format!(
691                    "type mismatch: attribute {} expects {:?}[{}], got {:?}[{}]",
692                    slot_info.name.as_string(),
693                    slot_info.typ,
694                    slot_info.elem_count,
695                    D::ATTR_FORMAT,
696                    D::ELEM_COUNT,
697                ),
698            ));
699        }
700
701        let bytes = value.u8ify();
702        let off = index as usize * self.stride as usize + self.compute_attr_offset(attr_index);
703        let size = slot_info.elem_count * slot_info.byte_count;
704
705        if bytes.len() != size {
706            return Err(OpticError::new(OpticErrorKind::Custom, &format!(
707                "value byte size {} does not match attribute size {}", bytes.len(), size
708            )));
709        }
710
711        self.cpu_mirror[off..off + size].copy_from_slice(&bytes);
712        subfill_instance_data(self.buf_id, off, &bytes);
713
714        Ok(())
715    }
716
717    /// Reads a single attribute of one instance from the CPU mirror.
718    ///
719    /// This is the lowest-level read. It copies bytes from the CPU mirror and
720    /// deserialises them into `D`. The GPU buffer is **not** touched — once an
721    /// instance buffer is shipped, data flows from the CPU mirror to the GPU,
722    /// never the other way.
723    ///
724    /// For convenience readers, see [`get_position`](Self::get_position),
725    /// [`get_rotation`](Self::get_rotation), [`get_scale`](Self::get_scale),
726    /// [`get_color`](Self::get_color), and [`get_custom`](Self::get_custom).
727    ///
728    /// # Errors
729    ///
730    /// Same as [`update_instance`](Self::update_instance).
731    pub fn get_instance<D: DataType>(&self, index: u32, attr_index: usize) -> OpticResult<D> {
732        if index >= self.count {
733            return Err(OpticError::new(OpticErrorKind::Custom, &format!("instance index {index} out of bounds (count: {})", self.count)));
734        }
735        if attr_index >= self.layouts.len() {
736            return Err(OpticError::new(OpticErrorKind::Custom, &format!("attr index {attr_index} out of bounds (layout count: {})", self.layouts.len())));
737        }
738
739        let slot_info = &self.layouts[attr_index].0;
740        if slot_info.byte_count != D::BYTE_COUNT || slot_info.elem_count != D::ELEM_COUNT || slot_info.typ != D::ATTR_FORMAT {
741            return Err(OpticError::new(
742                OpticErrorKind::Custom,
743                &format!(
744                    "type mismatch: attribute {} expects {:?}[{}], got {:?}[{}]",
745                    slot_info.name.as_string(),
746                    slot_info.typ,
747                    slot_info.elem_count,
748                    D::ATTR_FORMAT,
749                    D::ELEM_COUNT,
750                ),
751            ));
752        }
753
754        let off = index as usize * self.stride as usize + self.compute_attr_offset(attr_index);
755        let size = slot_info.elem_count * slot_info.byte_count;
756        let raw = &self.cpu_mirror[off..off + size];
757
758        let d = deserialize::<D>(raw);
759        Ok(d)
760    }
761
762    /// Updates a custom attribute of one instance by name.
763    ///
764    /// Use this when you defined a custom attribute via
765    /// [`InstanceDesc3D::attach_custom_attr`] or
766    /// [`InstanceDesc2D::attach_custom_attr`] and ship with that descriptor.
767    /// The attribute is looked up by name (not by index), making this robust
768    /// against layout reordering.
769    ///
770    /// # Errors
771    ///
772    /// - No custom attribute with that name exists.
773    /// - `D`'s type parameters do not match the attribute's declared format.
774    pub fn update_custom<D: DataType>(&mut self, index: u32, name: &str, value: D) -> OpticResult<()> {
775        let slot = self.kind.custom_offsets.iter().find(|s| s.name == name)
776            .ok_or_else(|| OpticError::new(OpticErrorKind::Custom, &format!("custom attribute \"{name}\" not found")))?;
777
778        if slot.byte_size != D::BYTE_COUNT || slot.elem_count as usize != D::ELEM_COUNT || slot.typ != D::ATTR_FORMAT {
779            return Err(OpticError::new(
780                OpticErrorKind::Custom,
781                &format!(
782                    "type mismatch for custom attribute \"{name}\": expected {:?}[{}], got {:?}[{}]",
783                    slot.typ, slot.elem_count, D::ATTR_FORMAT, D::ELEM_COUNT,
784                ),
785            ));
786        }
787
788        let bytes = value.u8ify();
789        let off = index as usize * self.stride as usize + slot.byte_offset;
790
791        self.cpu_mirror[off..off + slot.byte_size].copy_from_slice(&bytes);
792        subfill_instance_data(self.buf_id, off, &bytes);
793
794        Ok(())
795    }
796
797    /// Reads a custom attribute of one instance by name.
798    ///
799    /// Does **not** read back from the GPU.
800    ///
801    /// # Errors
802    ///
803    /// Same as [`update_custom`](InstanceBuffer::update_custom).
804    pub fn get_custom<D: DataType>(&self, index: u32, name: &str) -> OpticResult<D> {
805        let slot = self.kind.custom_offsets.iter().find(|s| s.name == name)
806            .ok_or_else(|| OpticError::new(OpticErrorKind::Custom, &format!("custom attribute \"{name}\" not found")))?;
807
808        if slot.byte_size != D::BYTE_COUNT || slot.elem_count as usize != D::ELEM_COUNT || slot.typ != D::ATTR_FORMAT {
809            return Err(OpticError::new(
810                OpticErrorKind::Custom,
811                &format!(
812                    "type mismatch for custom attribute \"{name}\": expected {:?}[{}], got {:?}[{}]",
813                    slot.typ, slot.elem_count, D::ATTR_FORMAT, D::ELEM_COUNT,
814                ),
815            ));
816        }
817
818        let off = index as usize * self.stride as usize + slot.byte_offset;
819        let raw = &self.cpu_mirror[off..off + slot.byte_size];
820        Ok(deserialize::<D>(raw))
821    }
822
823    /// Sets the position of a single instance in world space.
824    ///
825    /// This is a typed convenience over [`update_instance`](Self::update_instance).
826    /// It automatically resolves `attr_index = 0` and converts the `cgmath`
827    /// vector to the raw `[f32; 3]` the GPU expects.
828    ///
829    /// # Errors
830    ///
831    /// Returns an error if the buffer has no position attribute (i.e. the
832    /// descriptor that created it did not push to `pos_attr`).
833    pub fn set_position(&mut self, index: u32, pos: Vector3<f32>) -> OpticResult<()> {
834        if !self.kind.has_pos {
835            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no position attribute"));
836        }
837        let attr_index = 0;
838        self.update_instance(index, attr_index, [pos.x, pos.y, pos.z])
839    }
840
841    /// Returns the position of a single instance from the CPU mirror.
842    ///
843    /// The counterpart to [`set_position`](Self::set_position). Reads raw bytes
844    /// from the CPU mirror and wraps them back into a `cgmath::Vector3`.
845    ///
846    /// # Errors
847    ///
848    /// Returns an error if the buffer has no position attribute.
849    pub fn get_position(&self, index: u32) -> OpticResult<Vector3<f32>> {
850        if !self.kind.has_pos {
851            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no position attribute"));
852        }
853        let arr: [f32; 3] = self.get_instance(index, 0)?;
854        Ok(Vector3::new(arr[0], arr[1], arr[2]))
855    }
856
857    /// Sets the rotation quaternion of a single instance.
858    ///
859    /// The quaternion is stored as `[x, y, z, w]` in the interleaved buffer.
860    /// Use `cgmath::Quaternion::new(w, x, y, z)` to construct the value, then
861    /// pass `.v` (a `Vector4`) or a raw `Vector4` to this method.
862    ///
863    /// # Attribute-index resolution
864    ///
865    /// The method skips past the position attribute if present:
866    ///
867    /// | Layout | attr_index passed to [`update_instance`] |
868    /// |---|---|
869    /// | Position + Rotation | 1 |
870    /// | Rotation only | 0 |
871    ///
872    /// # Errors
873    ///
874    /// Returns an error if the buffer has no rotation attribute.
875    pub fn set_rotation(&mut self, index: u32, rot: Vector4<f32>) -> OpticResult<()> {
876        if !self.kind.has_rot {
877            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no rotation attribute"));
878        }
879        let attr_index = if self.kind.has_pos { 1 } else { 0 };
880        self.update_instance(index, attr_index, [rot.x, rot.y, rot.z, rot.w])
881    }
882
883    /// Returns the rotation quaternion of a single instance.
884    ///
885    /// The counterpart to [`set_rotation`](Self::set_rotation). Uses the same
886    /// attribute-index resolution logic to locate the correct slot.
887    ///
888    /// # Errors
889    ///
890    /// Returns an error if the buffer has no rotation attribute.
891    pub fn get_rotation(&self, index: u32) -> OpticResult<Vector4<f32>> {
892        if !self.kind.has_rot {
893            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no rotation attribute"));
894        }
895        let attr_index = if self.kind.has_pos { 1 } else { 0 };
896        let arr: [f32; 4] = self.get_instance(index, attr_index)?;
897        Ok(Vector4::new(arr[0], arr[1], arr[2], arr[3]))
898    }
899
900    /// Sets the scale of a single instance.
901    ///
902    /// Each component is applied independently — use a uniform scale like
903    /// `Vector3::new(2.0, 2.0, 2.0)` for isotropic scaling or vary components
904    /// for non-uniform stretching.
905    ///
906    /// # Attribute-index resolution
907    ///
908    /// Skips past position and rotation attributes if present.
909    ///
910    /// # Errors
911    ///
912    /// Returns an error if the buffer has no scale attribute.
913    pub fn set_scale(&mut self, index: u32, scale: Vector3<f32>) -> OpticResult<()> {
914        if !self.kind.has_scale {
915            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no scale attribute"));
916        }
917        let attr_index = if self.kind.has_pos { 1 } else { 0 };
918        let attr_index = if self.kind.has_rot { attr_index + 1 } else { attr_index };
919        self.update_instance(index, attr_index, [scale.x, scale.y, scale.z])
920    }
921
922    /// Returns the scale of a single instance.
923    ///
924    /// The counterpart to [`set_scale`](Self::set_scale). Uses the same
925    /// attribute-index resolution logic.
926    ///
927    /// # Errors
928    ///
929    /// Returns an error if the buffer has no scale attribute.
930    pub fn get_scale(&self, index: u32) -> OpticResult<Vector3<f32>> {
931        if !self.kind.has_scale {
932            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no scale attribute"));
933        }
934        let mut attr_index = 0u32;
935        if self.kind.has_pos { attr_index += 1; }
936        if self.kind.has_rot { attr_index += 1; }
937        let actual_idx = attr_index as usize;
938        let arr: [f32; 3] = self.get_instance(index, actual_idx)?;
939        Ok(Vector3::new(arr[0], arr[1], arr[2]))
940    }
941
942    /// Sets the colour of a single instance.
943    ///
944    /// Accepts an [`optic_core::RGBA`] value constructed with
945    /// [`RGBA::new(r, g, b, a)`](optic_core::RGBA::new) or from an integer hex
946    /// like [`RGBA::from(0xFF8800FF)`](optic_core::RGBA#impl-From<u32>).
947    ///
948    /// # Attribute-index resolution
949    ///
950    /// Skips past position, rotation, and scale attributes if present.
951    ///
952    /// # Errors
953    ///
954    /// Returns an error if the buffer has no colour attribute.
955    pub fn set_color(&mut self, index: u32, color: optic_core::RGBA) -> OpticResult<()> {
956        if !self.kind.has_col {
957            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no color attribute"));
958        }
959        let rgba = [color.0, color.1, color.2, color.3];
960        let mut attr_index = 0u32;
961        if self.kind.has_pos { attr_index += 1; }
962        if self.kind.has_rot { attr_index += 1; }
963        if self.kind.has_scale { attr_index += 1; }
964        self.update_instance(index, attr_index as usize, rgba)
965    }
966
967    /// Returns the colour of a single instance.
968    ///
969    /// The counterpart to [`set_color`](Self::set_color). Uses the same
970    /// attribute-index resolution logic. Returns an [`optic_core::RGBA`].
971    ///
972    /// # Errors
973    ///
974    /// Returns an error if the buffer has no colour attribute.
975    pub fn get_color(&self, index: u32) -> OpticResult<optic_core::RGBA> {
976        if !self.kind.has_col {
977            return Err(OpticError::new(OpticErrorKind::Custom, "instance buffer has no color attribute"));
978        }
979        let mut attr_index = 0u32;
980        if self.kind.has_pos { attr_index += 1; }
981        if self.kind.has_rot { attr_index += 1; }
982        if self.kind.has_scale { attr_index += 1; }
983        let rgba: [f32; 4] = self.get_instance(index, attr_index as usize)?;
984        Ok(optic_core::RGBA(rgba[0], rgba[1], rgba[2], rgba[3]))
985    }
986
987    // ── Growth / shrink ──────────────────────────────────────────────────────
988
989    /// Resizes the instance count, filling new slots with defaults.
990    ///
991    /// Use this to add or remove instances at the end of the buffer without
992    /// creating a new descriptor and re-shipping. New slots are filled with
993    /// sensible defaults:
994    ///
995    /// | Attribute | Default |
996    /// |---|---|
997    /// | Position | `(0, 0, 0)` |
998    /// | Rotation | Identity quaternion `(0, 0, 0, 1)` |
999    /// | Scale | `(1, 1, 1)` |
1000    /// | Colour | White `(1, 1, 1, 1)` |
1001    ///
1002    /// # Capacity
1003    ///
1004    /// If `new_count` exceeds the current capacity the allocation doubles each
1005    /// time (amortized O(1) growth). If `new_count` is smaller, excess
1006    /// instances become inaccessible but memory is **not** reclaimed — call
1007    /// [`shrink_to_fit`](Self::shrink_to_fit) if the buffer is persistently
1008    /// oversized.
1009    ///
1010    /// # Example
1011    ///
1012    /// ```ignore
1013    /// # let mut buffer: InstanceBuffer = desc.ship()?;
1014    /// buffer.set_instance_count(200);  // grow from 100 to 200
1015    /// buffer.set_instance_count(50);   // shrink: last 150 are inaccessible
1016    /// buffer.shrink_to_fit();          // release GPU memory
1017    /// ```
1018    pub fn set_instance_count(&mut self, new_count: u32) {
1019        if new_count > self.capacity {
1020            let new_cap = new_count.max(self.capacity * 2);
1021            self.reserve_internal(new_cap);
1022        }
1023        if new_count > self.count {
1024            let old_count = self.count as usize;
1025            let new_count_usize = new_count as usize;
1026            let stride = self.stride as usize;
1027            self.cpu_mirror.resize(new_count_usize * stride, 0u8);
1028            let default_slot = self.make_default_instance_bytes();
1029            for i in old_count..new_count_usize {
1030                let off = i * stride;
1031                self.cpu_mirror[off..off + stride].copy_from_slice(&default_slot);
1032            }
1033        }
1034        self.count = new_count;
1035        upload_instance_data(self.buf_id, &self.cpu_mirror);
1036    }
1037
1038    /// Reserves capacity for `additional` extra instances without changing the
1039    /// active count. Useful before a batch of [`push_raw`](Self::push_raw)
1040    /// calls to avoid repeated reallocations.
1041    pub fn reserve(&mut self, additional: u32) {
1042        let needed = self.count + additional;
1043        if needed > self.capacity {
1044            let new_cap = needed.max(self.capacity * 2);
1045            self.reserve_internal(new_cap);
1046        }
1047    }
1048
1049    /// Shrinks the GPU allocation to exactly fit the current instance count.
1050    ///
1051    /// Use after a large [`set_instance_count`](Self::set_instance_count) shrink
1052    /// to free GPU memory. Calling this frequently (e.g. every frame) may
1053    /// cause performance churn.
1054    pub fn shrink_to_fit(&mut self) {
1055        if self.count < self.capacity {
1056            let new_cap = self.count;
1057            self.capacity = new_cap;
1058            realloc_instance_buffer(self.buf_id, self.cpu_mirror.len());
1059        }
1060    }
1061
1062    /// Appends a raw, pre-interleaved instance at the end of the buffer.
1063    ///
1064    /// Use this when you have already packed instance bytes (e.g. from reading
1065    /// a binary file or from a previous buffer's CPU mirror). For structured
1066    /// appends, prefer [`set_instance_count`](Self::set_instance_count) plus
1067    /// the typed setters.
1068    ///
1069    /// # Errors
1070    ///
1071    /// Returns an error if `bytes.len()` does not match `self.stride`.
1072    pub fn push_raw(&mut self, bytes: &[u8]) -> OpticResult<u32> {
1073        if bytes.len() != self.stride as usize {
1074            return Err(OpticError::new(
1075                OpticErrorKind::Custom,
1076                &format!("push_raw byte count {} does not match instance stride {}", bytes.len(), self.stride),
1077            ));
1078        }
1079        let idx = self.count;
1080        self.set_instance_count(self.count + 1);
1081        let off = idx as usize * self.stride as usize;
1082        self.cpu_mirror[off..off + self.stride as usize].copy_from_slice(bytes);
1083        subfill_instance_data(self.buf_id, off, bytes);
1084        Ok(idx)
1085    }
1086
1087    /// Removes the instance at `index` by swapping it with the last instance
1088    /// (unordered, O(1)).
1089    ///
1090    /// This is the fastest removal — it simply copies the last instance over
1091    /// the target and decrements the count. Instance ordering is **not**
1092    /// preserved. Use [`remove_instance_ordered`](Self::remove_instance_ordered)
1093    /// if index stability matters.
1094    ///
1095    /// # Errors
1096    ///
1097    /// Returns an error if `index >= count`.
1098    pub fn remove_instance(&mut self, index: u32) -> OpticResult<()> {
1099        if index >= self.count {
1100            return Err(OpticError::new(OpticErrorKind::Custom, &format!("remove index {index} out of bounds (count: {})", self.count)));
1101        }
1102        let last = self.count - 1;
1103        if index != last {
1104            let stride = self.stride as usize;
1105            let dst = index as usize * stride;
1106            let src = last as usize * stride;
1107            let len = stride;
1108            self.cpu_mirror.copy_within(src..src + len, dst);
1109            subfill_instance_data(self.buf_id, dst, &self.cpu_mirror[dst..dst + len]);
1110        }
1111        self.count = last;
1112        Ok(())
1113    }
1114
1115    /// Removes the instance at `index` while preserving the order of remaining
1116    /// instances (O(n)).
1117    ///
1118    /// Shifts all subsequent instances down by one. Prefer the O(1) unordered
1119    /// [`remove_instance`](Self::remove_instance) when order does not matter.
1120    pub fn remove_instance_ordered(&mut self, index: u32) -> OpticResult<()> {
1121        if index >= self.count {
1122            return Err(OpticError::new(OpticErrorKind::Custom, &format!("remove index {index} out of bounds (count: {})", self.count)));
1123        }
1124        let stride = self.stride as usize;
1125        let dst = index as usize * stride;
1126        let end = (self.count - 1) as usize * stride;
1127        let len = end - dst;
1128        if len > 0 {
1129            self.cpu_mirror.copy_within(dst + stride..end + stride, dst);
1130        }
1131        self.count -= 1;
1132        upload_instance_data(self.buf_id, &self.cpu_mirror[..self.count as usize * stride]);
1133        Ok(())
1134    }
1135
1136    // ── Whole-buffer / ranged updates ────────────────────────────────────────
1137
1138    /// Replaces the entire buffer's data from a 3D instance descriptor.
1139    ///
1140    /// This is a full re-ship: the old GPU buffer handle is replaced and the
1141    /// old handle is **leaked**. If you need explicit GPU-side cleanup, free
1142    /// the old handle via `glDeleteBuffers` before calling this method.
1143    ///
1144    /// Use this when the attribute layout has changed (e.g. added or removed
1145    /// a custom attribute) so the old interleaved format is incompatible.
1146    pub fn write_all(&mut self, desc: &InstanceDesc3D) -> OpticResult<()> {
1147        let new_buf = desc.ship()?;
1148        self.buf_id = new_buf.buf_id;
1149        self.capacity = new_buf.capacity;
1150        self.count = new_buf.count;
1151        self.stride = new_buf.stride;
1152        self.layouts = new_buf.layouts;
1153        self.cpu_mirror = new_buf.cpu_mirror;
1154        self.kind = new_buf.kind;
1155        Ok(())
1156    }
1157
1158    /// Overwrites a contiguous range of instances with raw interleaved bytes.
1159    ///
1160    /// Useful when you have pre-computed instance data externally (e.g. a
1161    /// particle system updating all particles each frame). The byte slice must
1162    /// be aligned to `stride` boundaries.
1163    ///
1164    /// # Errors
1165    ///
1166    /// - `bytes.len()` is not a multiple of `stride`.
1167    /// - The range `[start, start + instance_count)` exceeds `self.count`.
1168    pub fn write_range(&mut self, start: u32, bytes: &[u8]) -> OpticResult<()> {
1169        let stride = self.stride as usize;
1170        if bytes.len() % stride != 0 {
1171            return Err(OpticError::new(
1172                OpticErrorKind::Custom,
1173                &format!("write_range byte count {} is not a multiple of stride {}", bytes.len(), stride),
1174            ));
1175        }
1176        let instance_count = bytes.len() / stride;
1177        if start + instance_count as u32 > self.count {
1178            return Err(OpticError::new(
1179                OpticErrorKind::Custom,
1180                "write_range extends past the current instance count",
1181            ));
1182        }
1183        let off = start as usize * stride;
1184        self.cpu_mirror[off..off + bytes.len()].copy_from_slice(bytes);
1185        subfill_instance_data(self.buf_id, off, bytes);
1186        Ok(())
1187    }
1188
1189    // ── Internal helpers ─────────────────────────────────────────────────────
1190
1191    fn compute_attr_offset(&self, attr_index: usize) -> usize {
1192        let mut offset = 0usize;
1193        for i in 0..attr_index {
1194            let si = &self.layouts[i].0;
1195            offset += si.elem_count * si.byte_count;
1196        }
1197        offset
1198    }
1199
1200    fn reserve_internal(&mut self, new_cap: u32) {
1201        let old_size = self.cpu_mirror.len();
1202        let new_size = new_cap as usize * self.stride as usize;
1203        self.cpu_mirror.resize(new_size, 0u8);
1204        let stride = self.stride as usize;
1205        let default_slot = self.make_default_instance_bytes();
1206        for i in (old_size / stride)..new_cap as usize {
1207            let off = i * stride;
1208            self.cpu_mirror[off..off + stride].copy_from_slice(&default_slot);
1209        }
1210        self.capacity = new_cap;
1211        realloc_instance_buffer(self.buf_id, new_size);
1212        upload_instance_data(self.buf_id, &self.cpu_mirror);
1213    }
1214
1215    fn make_default_instance_bytes(&self) -> Vec<u8> {
1216        let stride = self.stride as usize;
1217        let mut bytes = vec![0u8; stride];
1218        if self.kind.has_pos {
1219            // pos = (0,0,0) is already zero
1220        }
1221        if self.kind.has_rot {
1222            let off = if self.kind.has_pos { 12 } else { 0 };
1223            bytes[off + 12..off + 16].copy_from_slice(&1.0f32.to_le_bytes());
1224        }
1225        if self.kind.has_scale {
1226            let mut off = 0usize;
1227            if self.kind.has_pos { off += 12; }
1228            if self.kind.has_rot { off += 16; }
1229            bytes[off..off + 4].copy_from_slice(&1.0f32.to_le_bytes());
1230            bytes[off + 4..off + 8].copy_from_slice(&1.0f32.to_le_bytes());
1231            bytes[off + 8..off + 12].copy_from_slice(&1.0f32.to_le_bytes());
1232        }
1233        if self.kind.has_col {
1234            let mut off = 0usize;
1235            if self.kind.has_pos { off += 12; }
1236            if self.kind.has_rot { off += 16; }
1237            if self.kind.has_scale { off += 12; }
1238            for i in 0..4 {
1239                bytes[off + i * 4..off + (i + 1) * 4].copy_from_slice(&1.0f32.to_le_bytes());
1240            }
1241        }
1242        bytes
1243    }
1244}
1245
1246// ── GL helpers ─────────────────────────────────────────────────────────────
1247
1248fn create_instance_buffer() -> u32 {
1249    let mut id = 0u32;
1250    unsafe { gl::GenBuffers(1, &mut id); }
1251    id
1252}
1253
1254fn upload_instance_data(id: u32, data: &[u8]) {
1255    unsafe {
1256        gl::BindBuffer(gl::ARRAY_BUFFER, id);
1257        gl::BufferData(
1258            gl::ARRAY_BUFFER,
1259            data.len() as gl::types::GLsizeiptr,
1260            data.as_ptr() as *const std::ffi::c_void,
1261            gl::DYNAMIC_DRAW,
1262        );
1263    }
1264}
1265
1266fn subfill_instance_data(id: u32, offset: usize, data: &[u8]) {
1267    unsafe {
1268        gl::BindBuffer(gl::ARRAY_BUFFER, id);
1269        gl::BufferSubData(
1270            gl::ARRAY_BUFFER,
1271            offset as isize,
1272            data.len() as isize,
1273            data.as_ptr() as *const std::ffi::c_void,
1274        );
1275    }
1276}
1277
1278fn realloc_instance_buffer(id: u32, size: usize) {
1279    unsafe {
1280        gl::BindBuffer(gl::ARRAY_BUFFER, id);
1281        gl::BufferData(
1282            gl::ARRAY_BUFFER,
1283            size as gl::types::GLsizeiptr,
1284            std::ptr::null(),
1285            gl::DYNAMIC_DRAW,
1286        );
1287    }
1288}
1289
1290// ── Count helper trait ─────────────────────────────────────────────────────
1291
1292trait AsCount {
1293    fn len(&self) -> usize;
1294    fn is_empty(&self) -> bool;
1295}
1296
1297impl<T> AsCount for Vec<T> {
1298    fn len(&self) -> usize { self.len() }
1299    fn is_empty(&self) -> bool { self.is_empty() }
1300}
1301
1302impl AsCount for CustomATTR {
1303    fn len(&self) -> usize {
1304        if self.info.elem_count == 0 || self.info.byte_count == 0 { return 0; }
1305        self.data.len() / (self.info.elem_count * self.info.byte_count)
1306    }
1307    fn is_empty(&self) -> bool { self.data.is_empty() }
1308}
1309
1310// ── Deserialize helper ─────────────────────────────────────────────────────
1311
1312fn deserialize<D: DataType>(bytes: &[u8]) -> D {
1313    unsafe {
1314        let ptr = bytes.as_ptr() as *const D;
1315        std::ptr::read_unaligned(ptr)
1316    }
1317}
1318
1319// ── Raw byte access for typed attrs ────────────────────────────────────────
1320
1321trait AsBytes {
1322    fn as_bytes(&self) -> &[u8];
1323}
1324
1325impl AsBytes for Vec<[f32; 3]> {
1326    fn as_bytes(&self) -> &[u8] {
1327        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 12) }
1328    }
1329}
1330
1331impl AsBytes for Vec<[f32; 4]> {
1332    fn as_bytes(&self) -> &[u8] {
1333        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 16) }
1334    }
1335}
1336
1337impl AsBytes for Vec<[f32; 2]> {
1338    fn as_bytes(&self) -> &[u8] {
1339        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 8) }
1340    }
1341}
1342
1343impl AsBytes for Vec<f32> {
1344    fn as_bytes(&self) -> &[u8] {
1345        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 4) }
1346    }
1347}