kitgit

tirbofish/dropbear · diff

cacb2b8 · Thribhu K

perf: created a DynamicBuffer to improve performance

Unverified

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index 1e14b0a..0e67fea 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -1,4 +1,4 @@
-use crate::buffer::{ResizableBuffer, UniformBuffer};
+use crate::buffer::{DynamicBuffer, UniformBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
 use dropbear_utils::Dirty;
@@ -42,11 +42,11 @@ pub struct AnimationComponent {
     pub skinning_matrices: Dirty<Vec<Mat4>>,
 
     #[serde(skip)]
-    pub skinning_buffer: Option<ResizableBuffer<Mat4>>,
+    pub skinning_buffer: Option<DynamicBuffer<Mat4>>,
     #[serde(skip)]
-    pub morph_deltas_buffer: Option<ResizableBuffer<f32>>,
+    pub morph_deltas_buffer: Option<DynamicBuffer<f32>>,
     #[serde(skip)]
-    pub morph_weights_buffer: Option<ResizableBuffer<f32>>,
+    pub morph_weights_buffer: Option<DynamicBuffer<f32>>,
     #[serde(skip)]
     pub morph_info_buffer: Option<UniformBuffer<MorphTargetInfo>>,
 
@@ -506,7 +506,7 @@ impl AnimationComponent {
 
         if has_skinning {
             let buffer = self.skinning_buffer.get_or_insert_with(|| {
-                ResizableBuffer::new(
+                DynamicBuffer::new(
                     &graphics.device,
                     self.skinning_matrices.len(),
                     wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
@@ -545,7 +545,7 @@ impl AnimationComponent {
             }
 
             let weights_buffer = self.morph_weights_buffer.get_or_insert_with(|| {
-                ResizableBuffer::new(
+                DynamicBuffer::new(
                     &graphics.device,
                     flat.len().max(1),
                     wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
diff --git a/crates/dropbear-engine/src/billboarding.rs b/crates/dropbear-engine/src/billboarding.rs
index ad5e596..a876971 100644
--- a/crates/dropbear-engine/src/billboarding.rs
+++ b/crates/dropbear-engine/src/billboarding.rs
@@ -1,18 +1,18 @@
 use crate::asset::ASSET_REGISTRY;
-use crate::buffer::UniformBuffer;
+use crate::buffer::{DynamicBuffer, UniformBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::shader::Shader;
 use glam::Mat4;
 use std::sync::Arc;
 use wgpu::MultisampleState;
-use wgpu::util::{BufferInitDescriptor, DeviceExt};
+use wgpu::util::{DeviceExt};
 
 pub struct BillboardPipeline {
     pipeline: wgpu::RenderPipeline,
     transform_buffer: UniformBuffer<Mat4>,
     projection_buffer: UniformBuffer<Mat4>,
-    position_buffer: wgpu::Buffer,
-    tex_coord_buffer: wgpu::Buffer,
+    position_buffer: DynamicBuffer<[f32; 3]>,
+    tex_coord_buffer: DynamicBuffer<[f32; 2]>,
     uniform_bind_group_layout: wgpu::BindGroupLayout,
     sampler: wgpu::Sampler,
 }
@@ -34,19 +34,27 @@ impl BillboardPipeline {
             [-10.0, 10.0, 0.0],
         ];
 
-        let position_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("billboard position buffer"),
-            contents: bytemuck::cast_slice(&positions),
-            usage: wgpu::BufferUsages::VERTEX,
-        });
+        let mut position_buffer = DynamicBuffer::new(
+            &graphics.device,
+            positions.len(),
+            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            "billboard position buffer"
+        );
+        position_buffer.write(&graphics.device, &graphics.queue, &positions);
 
         let tex_coords: [[f32; 2]; 4] = [[1.0, 1.0], [1.0, 0.0], [0.0, 1.0], [0.0, 0.0]];
 
-        let tex_coord_buffer = graphics.device.create_buffer_init(&BufferInitDescriptor {
-            label: Some("billboard tex coords buffer"),
-            contents: bytemuck::cast_slice(&tex_coords),
-            usage: wgpu::BufferUsages::VERTEX,
-        });
+        let mut tex_coord_buffer = DynamicBuffer::new(
+            &graphics.device,
+            tex_coords.len(),
+            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            "billboard tex coords buffer"
+        );
+        tex_coord_buffer.write(
+            &graphics.device,
+            &graphics.queue,
+            bytemuck::cast_slice(&tex_coords)
+        );
 
         let sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor {
             label: Some("billboard sampler"),
@@ -234,8 +242,8 @@ impl BillboardPipeline {
             });
 
         render_pass.set_pipeline(&self.pipeline);
-        render_pass.set_vertex_buffer(0, self.position_buffer.slice(..));
-        render_pass.set_vertex_buffer(1, self.tex_coord_buffer.slice(..));
+        render_pass.set_vertex_buffer(0, self.position_buffer.buffer().slice(..));
+        render_pass.set_vertex_buffer(1, self.tex_coord_buffer.buffer().slice(..));
         render_pass.set_bind_group(0, &uniform_bind_group, &[]);
         render_pass.draw(0..4, 0..1);
     }
diff --git a/crates/dropbear-engine/src/buffer.rs b/crates/dropbear-engine/src/buffer.rs
index cc3f304..1e7354d 100644
--- a/crates/dropbear-engine/src/buffer.rs
+++ b/crates/dropbear-engine/src/buffer.rs
@@ -1,98 +1,34 @@
 //! Vertices and different buffers used for wgpu
 
 use std::marker::PhantomData;
+use std::ops::Range;
 use bytemuck::NoUninit;
 use dropbear_utils::Dirty;
 
-pub trait BufferType {}
+pub trait WritableBuffer<T> {
+    fn write(&self, queue: &wgpu::Queue, value: &T);
+    fn buffer(&self) -> &wgpu::Buffer;
+}
 
-#[derive(Debug, Clone)]
-pub struct ResizableBuffer<T> {
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct UniformBuffer<T> {
     buffer: wgpu::Buffer,
-    capacity: usize,
-    usage: wgpu::BufferUsages,
     label: String,
     _marker: PhantomData<T>,
 }
 
-impl<T> BufferType for ResizableBuffer<T> {}
-
-impl<T: NoUninit> ResizableBuffer<T> {
-    pub fn new(
-        device: &wgpu::Device,
-        initial_capacity: usize,
-        usage: wgpu::BufferUsages,
-        label: &str,
-    ) -> Self {
-        let size = (initial_capacity * std::mem::size_of::<T>()) as wgpu::BufferAddress;
-        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
-            label: Some(label),
-            size: size.max(16),
-            usage,
-            mapped_at_creation: false,
-        });
-
-        log::debug!(
-            "Registered new resizable buffer: {:?} (usage={:?})",
-            label,
-            usage
-        );
-        Self {
-            buffer,
-            capacity: initial_capacity,
-            usage,
-            label: label.to_string(),
-            _marker: PhantomData,
-        }
-    }
-
-    pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) {
+impl<T: NoUninit> WritableBuffer<T> for UniformBuffer<T> {
+    fn write(&self, queue: &wgpu::Queue, value: &T) {
         puffin::profile_function!(&self.label);
-        if data.is_empty() {
-            return;
-        }
-
-        if data.len() > self.capacity {
-            self.capacity = data.len().max(self.capacity * 2);
-
-            let new_size = (self.capacity * std::mem::size_of::<T>()) as wgpu::BufferAddress;
-
-            log::debug!(
-                "Resizing buffer '{}' to hold {} items",
-                self.label,
-                self.capacity
-            );
-
-            self.buffer = device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some(&self.label),
-                size: new_size,
-                usage: self.usage,
-                mapped_at_creation: false,
-            });
-        }
-
-        queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
+        queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
     }
 
-    pub fn buffer(&self) -> &wgpu::Buffer {
+    fn buffer(&self) -> &wgpu::Buffer {
         &self.buffer
     }
-
-    pub fn slice(&self, count: usize) -> wgpu::BufferSlice<'_> {
-        let byte_count = (count * std::mem::size_of::<T>()) as wgpu::BufferAddress;
-        self.buffer.slice(0..byte_count)
-    }
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct UniformBuffer<T> {
-    buffer: wgpu::Buffer,
-    label: String,
-    _marker: PhantomData<T>,
 }
 
-impl<T> BufferType for UniformBuffer<T> {}
-
 impl<T: NoUninit> UniformBuffer<T> {
     pub fn new(device: &wgpu::Device, label: &str) -> Self {
         let size = (std::mem::size_of::<T>() as wgpu::BufferAddress).max(16);
@@ -111,11 +47,6 @@ impl<T: NoUninit> UniformBuffer<T> {
         }
     }
 
-    pub fn write(&self, queue: &wgpu::Queue, value: &T) {
-        puffin::profile_function!(&self.label);
-        queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
-    }
-
     pub fn buffer(&self) -> &wgpu::Buffer {
         &self.buffer
     }
@@ -132,7 +63,16 @@ pub struct StorageBuffer<T> {
     _marker: PhantomData<T>,
 }
 
-impl<T> BufferType for StorageBuffer<T> {}
+impl<T: NoUninit> WritableBuffer<T> for StorageBuffer<T> {
+    fn write(&self, queue: &wgpu::Queue, value: &T) {
+        puffin::profile_function!(self.label());
+        queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value));
+    }
+
+    fn buffer(&self) -> &wgpu::Buffer {
+        &self.buffer
+    }
+}
 
 impl<T: NoUninit> StorageBuffer<T> {
     pub fn new_read_only(device: &wgpu::Device, label: &str) -> Self {
@@ -211,13 +151,13 @@ impl<T: NoUninit> StorageBuffer<T> {
 }
 
 #[derive(Clone, PartialEq)]
-pub struct MutableDataBuffer<T> {
+pub struct MutableDataBuffer<T, B: WritableBuffer<T> = UniformBuffer<T>> {
     data: Dirty<T>,
-    pub buffer: UniformBuffer<T>,
+    pub buffer: B,
 }
 
-impl<T: NoUninit> MutableDataBuffer<T> {
-    pub fn new(data: Dirty<T>, buffer: UniformBuffer<T>) -> Self {
+impl<T: NoUninit, B: WritableBuffer<T>> MutableDataBuffer<T, B> {
+    pub fn new(data: Dirty<T>, buffer: B) -> Self {
         Self {
             data,
             buffer,
@@ -238,3 +178,228 @@ impl<T: NoUninit> MutableDataBuffer<T> {
         self.data.set(value);
     }
 }
+
+pub struct DynamicBuffer<T> {
+    data: Vec<T>,
+    dirty_range: Option<Range<usize>>,
+    buffer: wgpu::Buffer,
+    capacity: usize,
+    usage: wgpu::BufferUsages,
+    label: String,
+    _marker: PhantomData<T>,
+}
+
+impl<T: Clone> Clone for DynamicBuffer<T> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+            dirty_range: self.dirty_range.clone(),
+            buffer: self.buffer.clone(),
+            capacity: self.capacity,
+            usage: self.usage,
+            label: self.label.clone(),
+            _marker: PhantomData,
+        }
+    }
+}
+
+impl<T> std::fmt::Debug for DynamicBuffer<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("DynamicBuffer")
+            .field("label", &self.label)
+            .field("capacity", &self.capacity)
+            .field("len", &self.data.len())
+            .field("dirty_range", &self.dirty_range)
+            .finish()
+    }
+}
+
+impl<T: NoUninit> DynamicBuffer<T> {
+    /// Allocate an empty GPU buffer with `initial_capacity` element slots.
+    pub fn new(
+        device: &wgpu::Device,
+        initial_capacity: usize,
+        usage: wgpu::BufferUsages,
+        label: &str,
+    ) -> Self {
+        let size = ((initial_capacity * std::mem::size_of::<T>()) as wgpu::BufferAddress).max(16);
+        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some(label),
+            size,
+            usage,
+            mapped_at_creation: false,
+        });
+        log::debug!("Registered new dynamic buffer: {:?} (usage={:?})", label, usage);
+        Self {
+            data: Vec::with_capacity(initial_capacity),
+            dirty_range: None,
+            buffer,
+            capacity: initial_capacity,
+            usage,
+            label: label.to_string(),
+            _marker: PhantomData,
+        }
+    }
+
+    /// Create a buffer pre-populated with `data`. The initial contents are immediately
+    /// uploaded via `create_buffer_init`, so no flush is needed after construction.
+    pub fn from_slice(
+        device: &wgpu::Device,
+        data: &[T],
+        usage: wgpu::BufferUsages,
+        label: &str,
+    ) -> Self {
+        use wgpu::util::DeviceExt;
+        let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+            label: Some(label),
+            contents: bytemuck::cast_slice(data),
+            usage,
+        });
+        log::debug!("Registered new dynamic buffer (from_slice): {:?} (usage={:?})", label, usage);
+        Self {
+            capacity: data.len(),
+            data: data.to_vec(),
+            dirty_range: None,
+            buffer,
+            usage,
+            label: label.to_string(),
+            _marker: PhantomData,
+        }
+    }
+
+    pub fn len(&self) -> usize {
+        self.data.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.data.is_empty()
+    }
+
+    /// Borrow the CPU-side data slice.
+    pub fn data(&self) -> &[T] {
+        &self.data
+    }
+
+    /// Consume the buffer and return the owned CPU-side data.
+    pub fn into_data(self) -> Vec<T> {
+        self.data
+    }
+
+    /// Read an element without marking anything dirty.
+    pub fn get(&self, index: usize) -> Option<&T> {
+        self.data.get(index)
+    }
+
+    /// Overwrite a single element and mark it dirty.
+    pub fn update(&mut self, index: usize, value: T) {
+        self.data[index] = value;
+        self.expand_dirty(index..index + 1);
+    }
+
+    /// Overwrite a contiguous slice of elements starting at `start` and mark them dirty.
+    pub fn update_range(&mut self, start: usize, values: &[T])
+    where
+        T: Copy,
+    {
+        let end = start + values.len();
+        self.data[start..end].copy_from_slice(values);
+        self.expand_dirty(start..end);
+    }
+
+    /// Append an element to the CPU buffer and mark the new slot dirty.
+    /// The GPU buffer will be reallocated on the next [`flush`](DynamicBuffer::flush) if needed.
+    pub fn push(&mut self, value: T) {
+        let idx = self.data.len();
+        self.data.push(value);
+        self.expand_dirty(idx..idx + 1);
+    }
+
+    /// Truncate the CPU buffer. Does not shrink the GPU allocation; the GPU buffer
+    /// will simply have unused capacity at the end until re-populated.
+    pub fn truncate(&mut self, len: usize) {
+        self.data.truncate(len);
+        if let Some(ref mut r) = self.dirty_range {
+            r.end = r.end.min(len);
+            if r.start >= r.end {
+                self.dirty_range = None;
+            }
+        }
+    }
+
+    /// Upload only the dirty element range to the GPU.
+    ///
+    /// If the CPU buffer has grown beyond the current GPU capacity the GPU buffer is
+    /// reallocated (doubling strategy) and the full contents are re-uploaded.
+    /// Cheap no-op when nothing is dirty.
+    pub fn flush(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
+        puffin::profile_function!(&self.label);
+        let Some(dirty) = self.dirty_range.take() else { return };
+
+        if self.data.len() > self.capacity {
+            self.capacity = self.data.len().max(self.capacity * 2);
+            let new_size =
+                ((self.capacity * std::mem::size_of::<T>()) as wgpu::BufferAddress).max(16);
+            log::debug!("Reallocating dynamic buffer '{}' to {} elements", self.label, self.capacity);
+            self.buffer = device.create_buffer(&wgpu::BufferDescriptor {
+                label: Some(&self.label),
+                size: new_size,
+                usage: self.usage,
+                mapped_at_creation: false,
+            });
+            queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&self.data));
+        } else {
+            let stride = std::mem::size_of::<T>();
+            let byte_offset = (dirty.start * stride) as wgpu::BufferAddress;
+            queue.write_buffer(
+                &self.buffer,
+                byte_offset,
+                bytemuck::cast_slice(&self.data[dirty]),
+            );
+        }
+    }
+
+    /// Convenience: replace all CPU data with `data` and immediately flush to the GPU.
+    ///
+    /// Equivalent to clearing, extending, marking everything dirty, then calling
+    /// [`flush`](DynamicBuffer::flush). Use this for buffers that are fully rewritten
+    /// every frame (instance buffers, debug geometry, etc.).
+    pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T])
+    where
+        T: Copy,
+    {
+        self.data.clear();
+        self.data.extend_from_slice(data);
+        if !data.is_empty() {
+            self.dirty_range = Some(0..data.len());
+        }
+        self.flush(device, queue);
+    }
+
+    /// The underlying `wgpu::Buffer` — use this when binding to a render pass.
+    pub fn buffer(&self) -> &wgpu::Buffer {
+        &self.buffer
+    }
+
+    /// A `BufferSlice` covering the first `count` elements (by byte length).
+    ///
+    /// Mirrors `ResizableBuffer::slice` for render-pass binding of partially-filled buffers.
+    pub fn slice(&self, count: usize) -> wgpu::BufferSlice<'_> {
+        let byte_len = (count * std::mem::size_of::<T>()) as wgpu::BufferAddress;
+        self.buffer.slice(0..byte_len)
+    }
+
+    /// A `BufferSlice` covering exactly the live elements (`0..len`).
+    pub fn full_slice(&self) -> wgpu::BufferSlice<'_> {
+        let byte_len = (self.data.len() * std::mem::size_of::<T>()) as wgpu::BufferAddress;
+        self.buffer.slice(0..byte_len)
+    }
+
+    fn expand_dirty(&mut self, range: Range<usize>) {
+        self.dirty_range = Some(match self.dirty_range.take() {
+            None => range,
+            Some(existing) => {
+                existing.start.min(range.start)..existing.end.max(range.end)
+            }
+        });
+    }
+}
diff --git a/crates/dropbear-engine/src/camera.rs b/crates/dropbear-engine/src/camera.rs
index 27e55a5..da9b12c 100644
--- a/crates/dropbear-engine/src/camera.rs
+++ b/crates/dropbear-engine/src/camera.rs
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
 use wgpu::Buffer;
 
 use crate::{buffer::UniformBuffer, graphics::SharedGraphicsContext};
+use crate::buffer::WritableBuffer;
 
 /// Matrix that converts OpenGL (from [`glam`]) to [`wgpu`] values
 #[rustfmt::skip]
diff --git a/crates/dropbear-engine/src/debug.rs b/crates/dropbear-engine/src/debug.rs
index f3f3323..ace4495 100644
--- a/crates/dropbear-engine/src/debug.rs
+++ b/crates/dropbear-engine/src/debug.rs
@@ -1,4 +1,4 @@
-use crate::buffer::{ResizableBuffer, UniformBuffer};
+use crate::buffer::{DynamicBuffer, UniformBuffer, WritableBuffer};
 use crate::graphics::{CommandEncoder, SharedGraphicsContext};
 use crate::shader::Shader;
 use glam::{Mat4, Quat, Vec3, Vec4};
@@ -15,7 +15,7 @@ use wgpu::{
 pub struct DebugDraw {
     pipeline: Arc<DebugDrawPipeline>,
     vertices: Vec<DebugVertex>,
-    vertex_buffer: ResizableBuffer<DebugVertex>,
+    vertex_buffer: DynamicBuffer<DebugVertex>,
 }
 
 // main parts
@@ -25,7 +25,7 @@ impl DebugDraw {
     pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
         let pipeline = Arc::new(DebugDrawPipeline::new(graphics.clone()));
         let vertices = vec![];
-        let vertex_buffer = ResizableBuffer::new(
+        let vertex_buffer = DynamicBuffer::new(
             &graphics.device,
             1024,
             BufferUsages::VERTEX | BufferUsages::COPY_DST,
@@ -532,7 +532,7 @@ impl DebugDrawPipeline {
         graphics: Arc<SharedGraphicsContext>,
         encoder: &mut CommandEncoder,
         view_proj: Mat4,
-        vertex_buffer: &ResizableBuffer<DebugVertex>,
+        vertex_buffer: &DynamicBuffer<DebugVertex>,
         vertex_count: u32,
     ) {
         // update camera uniform
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 5391c61..e001014 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -1,6 +1,6 @@
 use crate::asset::{ASSET_REGISTRY, Handle};
 use crate::attenuation::{Attenuation, RANGE_50};
-use crate::buffer::{ResizableBuffer, UniformBuffer};
+use crate::buffer::{DynamicBuffer, UniformBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::pipelines::light_cube::InstanceInput;
 use crate::procedural::ProcedurallyGeneratedObject;
@@ -275,7 +275,7 @@ pub struct Light {
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
     pub bind_group: wgpu::BindGroup,
-    pub instance_buffer: ResizableBuffer<InstanceInput>,
+    pub instance_buffer: DynamicBuffer<InstanceInput>,
     pub component: LightComponent,
 }
 
@@ -331,7 +331,7 @@ impl Light {
         )
         .into();
 
-        let mut instance_buffer = ResizableBuffer::new(
+        let mut instance_buffer = DynamicBuffer::new(
             &graphics.device,
             1,
             wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index a5408d5..e2423b1 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -1,5 +1,5 @@
 use crate::asset::{AssetRegistry, Handle};
-use crate::buffer::UniformBuffer;
+use crate::buffer::{DynamicBuffer, MutableDataBuffer, UniformBuffer, WritableBuffer};
 use crate::texture::TextureBuilder;
 use crate::{
     graphics::SharedGraphicsContext,
@@ -39,11 +39,10 @@ pub struct Model {
 // #[derive(Clone)]
 pub struct Mesh {
     pub name: String,
-    pub vertex_buffer: wgpu::Buffer,
-    pub index_buffer: wgpu::Buffer,
+    pub vertex_buffer: DynamicBuffer<ModelVertex>,
+    pub index_buffer: DynamicBuffer<u32>,
     pub num_elements: u32,
     pub material: usize,
-    pub vertices: Vec<ModelVertex>,
     pub morph_deltas_offset: u32,
     pub morph_target_count: u32,
     pub morph_vertex_count: u32,
@@ -1420,29 +1419,24 @@ impl Model {
                 });
             }
 
-            let vertex_buffer =
-                graphics
-                    .device
-                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                        label: Some(&format!("{} Vertex Buffer", model_label)),
-                        contents: bytemuck::cast_slice(&vertices),
-                        usage: wgpu::BufferUsages::VERTEX,
-                    });
+            let vertex_buffer = DynamicBuffer::from_slice(
+                &graphics.device,
+                &vertices,
+                wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                &format!("{} Vertex Buffer", model_label),
+            );
 
-            let index_buffer =
-                graphics
-                    .device
-                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                        label: Some(&format!("{} Index Buffer", model_label)),
-                        contents: bytemuck::cast_slice(&mesh_info.indices),
-                        usage: wgpu::BufferUsages::INDEX,
-                    });
+            let index_buffer = DynamicBuffer::from_slice(
+                &graphics.device,
+                &mesh_info.indices,
+                wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
+                &format!("{} Index Buffer", model_label),
+            );
 
             gpu_meshes.push(Mesh {
                 name: mesh_info.name,
                 vertex_buffer,
                 index_buffer,
-                vertices,
                 num_elements: mesh_info.indices.len() as u32,
                 material: mesh_info.material_index,
                 morph_deltas_offset,
@@ -1563,8 +1557,8 @@ where
         animation_bind_group: &'a wgpu::BindGroup,
         environment_bind_group: &'a wgpu::BindGroup,
     ) {
-        self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
-        self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
+        self.set_vertex_buffer(0, mesh.vertex_buffer.full_slice());
+        self.set_index_buffer(mesh.index_buffer.full_slice(), wgpu::IndexFormat::Uint32);
         self.set_bind_group(0, globals_camera_bind_group, &[]);
         self.set_bind_group(1, &material.bind_group, &[]);
         self.set_bind_group(2, animation_bind_group, &[]);
@@ -1663,8 +1657,8 @@ where
         camera_bind_group: &'a wgpu::BindGroup,
         light_bind_group: &'a wgpu::BindGroup,
     ) {
-        self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
-        self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
+        self.set_vertex_buffer(0, mesh.vertex_buffer.full_slice());
+        self.set_index_buffer(mesh.index_buffer.full_slice(), wgpu::IndexFormat::Uint32);
         self.set_bind_group(0, camera_bind_group, &[]);
         self.set_bind_group(1, light_bind_group, &[]);
         self.draw_indexed(0..mesh.num_elements, 0, instances);
diff --git a/crates/dropbear-engine/src/pipelines/animation.rs b/crates/dropbear-engine/src/pipelines/animation.rs
index b8a4417..70c558b 100644
--- a/crates/dropbear-engine/src/pipelines/animation.rs
+++ b/crates/dropbear-engine/src/pipelines/animation.rs
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 use glam::{Mat4};
 use crate::animation::{MorphTargetInfo, MAX_MORPH_WEIGHTS, MAX_SKINNING_MATRICES};
-use crate::buffer::{StorageBuffer, UniformBuffer};
+use crate::buffer::{StorageBuffer, UniformBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 
 pub struct AnimationDefaults {
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index b6fb7df..d0e5b85 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -1,4 +1,4 @@
-use crate::buffer::UniformBuffer;
+use crate::buffer::{UniformBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 use dropbear_utils::Dirty;
 use std::sync::Arc;
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index bfda81b..4b65a7e 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -1,4 +1,4 @@
-use crate::buffer::StorageBuffer;
+use crate::buffer::{StorageBuffer, WritableBuffer};
 use crate::graphics::SharedGraphicsContext;
 use crate::lighting::{Light, LightArrayUniform, MAX_LIGHTS};
 use crate::model::{ModelVertex, Vertex};
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index 91c8787..b23cbee 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -2,6 +2,7 @@
 //! vertices rather than from a model.
 
 use crate::asset::{AssetRegistry, Handle};
+use crate::buffer::DynamicBuffer;
 use crate::graphics::SharedGraphicsContext;
 use crate::model::ModelVertex;
 use crate::model::{Material, Mesh, Model};
@@ -76,21 +77,19 @@ impl ProcedurallyGeneratedObject {
         let vertices = self.vertices.clone();
         let indices = self.indices.clone();
 
-        let vertex_buffer = graphics
-            .device
-            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some(&format!("{label} Vertex Buffer")),
-                contents: bytemuck::cast_slice(&vertices),
-                usage: wgpu::BufferUsages::VERTEX,
-            });
-
-        let index_buffer = graphics
-            .device
-            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some(&format!("{label} Index Buffer")),
-                contents: bytemuck::cast_slice(&indices),
-                usage: wgpu::BufferUsages::INDEX,
-            });
+        let vertex_buffer = DynamicBuffer::from_slice(
+            &graphics.device,
+            &vertices,
+            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            &format!("{label} Vertex Buffer"),
+        );
+
+        let index_buffer = DynamicBuffer::from_slice(
+            &graphics.device,
+            &indices,
+            wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
+            &format!("{label} Index Buffer"),
+        );
 
         let mesh = Mesh {
             name: label.clone(),
@@ -98,7 +97,6 @@ impl ProcedurallyGeneratedObject {
             index_buffer,
             num_elements: indices.len() as u32,
             material: 0,
-            vertices,
             morph_deltas_offset: 0,
             morph_target_count: 0,
             morph_vertex_count: self.vertices.len() as u32,
diff --git a/crates/eucalyptus-core/src/animation.rs b/crates/eucalyptus-core/src/animation.rs
index 8353514..ced2cd4 100644
--- a/crates/eucalyptus-core/src/animation.rs
+++ b/crates/eucalyptus-core/src/animation.rs
@@ -1,9 +1,6 @@
 use crate::component::{
     Component, ComponentDescriptor, DisabilityFlags, InspectableComponent, SerializedComponent,
 };
-use crate::ptr::WorldPtr;
-use crate::scripting::native::DropbearNativeError;
-use crate::scripting::result::DropbearNativeResult;
 use dropbear_engine::animation::{AnimationComponent, AnimationSettings};
 use dropbear_engine::asset::ASSET_REGISTRY;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer};
@@ -31,9 +28,9 @@ impl Component for AnimationComponent {
     }
 
     fn init(
-        ser: &Self::SerializedForm,
+        ser: &'_ Self::SerializedForm,
         _graphics: Arc<SharedGraphicsContext>,
-    ) -> crate::component::ComponentInitFuture<Self> {
+    ) -> crate::component::ComponentInitFuture<'_, Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
diff --git a/crates/eucalyptus-core/src/physics/collider.rs b/crates/eucalyptus-core/src/physics/collider.rs
index b721137..4d0d2c8 100644
--- a/crates/eucalyptus-core/src/physics/collider.rs
+++ b/crates/eucalyptus-core/src/physics/collider.rs
@@ -146,7 +146,7 @@ impl InspectableComponent for ColliderGroup {
                             let mut max = [f32::NEG_INFINITY; 3];
 
                             for mesh in &model.meshes {
-                                for vertex in &mesh.vertices {
+                                for vertex in mesh.vertex_buffer.data() {
                                     let p = vertex.position;
                                     for i in 0..3 {
                                         min[i] = min[i].min(p[i]);
diff --git a/crates/eucalyptus-core/src/properties.rs b/crates/eucalyptus-core/src/properties.rs
index 3f32b42..dea154b 100644
--- a/crates/eucalyptus-core/src/properties.rs
+++ b/crates/eucalyptus-core/src/properties.rs
@@ -32,7 +32,7 @@ impl Component for CustomProperties {
             internal: false,
             fqtn: "eucalyptus_core::properties::CustomProperties".to_string(),
             type_name: "CustomProperties".to_string(),
-            category: None,
+            category: Some("Properties".to_string()),
             description: Some("Custom properties for an entity".to_string()),
         }
     }
@@ -40,7 +40,7 @@ impl Component for CustomProperties {
     fn init(
         ser: &Self::SerializedForm,
         _graphics: Arc<SharedGraphicsContext>,
-    ) -> ComponentInitFuture<Self> {
+    ) -> ComponentInitFuture<'_, Self> {
         Box::pin(async move { Ok((ser.clone(),)) })
     }
 
diff --git a/crates/eucalyptus-core/src/rendering.rs b/crates/eucalyptus-core/src/rendering.rs
index 16558b0..5eec782 100644
--- a/crates/eucalyptus-core/src/rendering.rs
+++ b/crates/eucalyptus-core/src/rendering.rs
@@ -6,7 +6,7 @@ use glam::{Mat4, Quat, Vec3};
 use dropbear_engine::animation::{AnimationComponent, MorphTargetInfo};
 use dropbear_engine::asset::{Handle, ASSET_REGISTRY};
 use dropbear_engine::billboarding::BillboardPipeline;
-use dropbear_engine::buffer::ResizableBuffer;
+use dropbear_engine::buffer::DynamicBuffer;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
 use dropbear_engine::graphics::{CommandEncoder, InstanceRaw, SharedGraphicsContext};
@@ -177,7 +177,7 @@ impl RendererCommon {
     pub fn prepare_models(
         graphics: &SharedGraphicsContext,
         batches: &HashMap<u64, ModelBatch>,
-        instance_buffer_cache: &mut HashMap<u64, ResizableBuffer<InstanceRaw>>,
+        instance_buffer_cache: &mut HashMap<u64, DynamicBuffer<InstanceRaw>>,
     ) -> (Vec<PreparedModel>, HashMap<u64, Arc<Model>>) {
         puffin::profile_scope!("preparing models");
         let registry = ASSET_REGISTRY.read();
@@ -202,7 +202,7 @@ impl RendererCommon {
 
             let instance_buffer = instance_buffer_cache
                 .entry(*handle_id)
-                .or_insert_with(|| ResizableBuffer::new(
+                .or_insert_with(|| DynamicBuffer::new(
                     &graphics.device,
                     instances.len().max(1),
                     wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
@@ -286,8 +286,8 @@ impl RendererCommon {
         environment_bind_group: &wgpu::BindGroup,
         pipeline: &MainRenderPipeline,
         animation_defaults: &AnimationDefaults,
-        instance_buffer_cache: &HashMap<u64, ResizableBuffer<InstanceRaw>>,
-        animated_instance_buffers: &mut HashMap<Entity, ResizableBuffer<InstanceRaw>>,
+        instance_buffer_cache: &HashMap<u64, DynamicBuffer<InstanceRaw>>,
+        animated_instance_buffers: &mut HashMap<Entity, DynamicBuffer<InstanceRaw>>,
         animated_bind_group_cache: &mut HashMap<Entity, (u64, wgpu::BindGroup)>,
         static_bind_group_cache: &mut HashMap<u64, wgpu::BindGroup>,
         last_morph_info_per_mesh: &mut HashMap<u32, MorphTargetInfo>,
@@ -392,7 +392,7 @@ impl RendererCommon {
 
                 {
                     let buf = animated_instance_buffers.entry(inst.entity).or_insert_with(|| {
-                        ResizableBuffer::new(
+                        DynamicBuffer::new(
                             &graphics.device, 1,
                             wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
                             "animated instance buffer",
diff --git a/crates/eucalyptus-core/src/ser/model.rs b/crates/eucalyptus-core/src/ser/model.rs
index 4560f7c..2893327 100644
--- a/crates/eucalyptus-core/src/ser/model.rs
+++ b/crates/eucalyptus-core/src/ser/model.rs
@@ -1,4 +1,5 @@
 use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
+use dropbear_engine::buffer::DynamicBuffer;
 use dropbear_engine::graphics::SharedGraphicsContext;
 use dropbear_engine::model::{
     AlphaMode, Animation, Material, Mesh, Model, ModelVertex, Node, Skin,
@@ -158,7 +159,7 @@ impl From<Mesh> for EucalyptusMesh {
             name: value.name,
             num_elements: value.num_elements,
             material: value.material,
-            vertices: value.vertices,
+            vertices: value.vertex_buffer.into_data(),
             morph_deltas_offset: value.morph_deltas_offset,
             morph_target_count: value.morph_target_count,
             morph_vertex_count: value.morph_vertex_count,
@@ -169,23 +170,21 @@ impl From<Mesh> for EucalyptusMesh {
 
 impl EucalyptusMesh {
     fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Mesh {
-        let vertex_buffer = graphics
-            .device
-            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some(&format!("{} Vertex Buffer", self.name)),
-                contents: bytemuck::cast_slice(&self.vertices),
-                usage: wgpu::BufferUsages::VERTEX,
-            });
+        let vertex_buffer = DynamicBuffer::from_slice(
+            &graphics.device,
+            &self.vertices,
+            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            &format!("{} Vertex Buffer", self.name),
+        );
 
         let index_count = self.num_elements.min(self.vertices.len() as u32);
         let indices = (0..index_count).collect::<Vec<u32>>();
-        let index_buffer = graphics
-            .device
-            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                label: Some(&format!("{} Index Buffer", self.name)),
-                contents: bytemuck::cast_slice(&indices),
-                usage: wgpu::BufferUsages::INDEX,
-            });
+        let index_buffer = DynamicBuffer::from_slice(
+            &graphics.device,
+            &indices,
+            wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
+            &format!("{} Index Buffer", self.name),
+        );
 
         Mesh {
             name: self.name.clone(),
@@ -193,7 +192,6 @@ impl EucalyptusMesh {
             index_buffer,
             num_elements: index_count,
             material: self.material,
-            vertices: self.vertices.clone(),
             morph_deltas_offset: self.morph_deltas_offset,
             morph_target_count: self.morph_target_count,
             morph_vertex_count: self.morph_vertex_count,
diff --git a/crates/eucalyptus-editor/src/editor/docks/viewport.rs b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
index eac63d0..1383988 100644
--- a/crates/eucalyptus-editor/src/editor/docks/viewport.rs
+++ b/crates/eucalyptus-editor/src/editor/docks/viewport.rs
@@ -450,7 +450,7 @@ fn aabb_for_world_matrix(world_mat: glam::Mat4, mesh: &MeshRenderer) -> (glam::V
             let mut lo = Vec3::splat(f32::INFINITY);
             let mut hi = Vec3::splat(f32::NEG_INFINITY);
             for m in &model.meshes {
-                for v in &m.vertices {
+                for v in m.vertex_buffer.data() {
                     let p = Vec3::from(v.position);
                     lo = lo.min(p);
                     hi = hi.max(p);
diff --git a/crates/eucalyptus-editor/src/editor/input.rs b/crates/eucalyptus-editor/src/editor/input.rs
index 3c82bdf..3d911bd 100644
--- a/crates/eucalyptus-editor/src/editor/input.rs
+++ b/crates/eucalyptus-editor/src/editor/input.rs
@@ -355,14 +355,8 @@ impl Mouse for Editor {
                     .query_one::<(&mut Camera, &CameraComponent)>(active_camera)
                     .get()
             {
-                let frame_scale = if self.dt > 0.0 {
-                    (self.dt as f64 / (1.0 / 60.0)).clamp(0.1, 4.0)
-                } else {
-                    1.0
-                };
-
                 if let Some((dx, dy)) = delta {
-                    camera.track_mouse_delta(dx * frame_scale, dy * frame_scale);
+                    camera.track_mouse_delta(dx, dy);
                     self.input_state.mouse_delta = Some((dx, dy));
                 } else {
                     log_once::warn_once!("Unable to track mouse delta, attempting fallback");
@@ -370,7 +364,7 @@ impl Mouse for Editor {
                     if let Some(old_mouse_pos) = self.input_state.last_mouse_pos {
                         let dx = position.x - old_mouse_pos.0;
                         let dy = position.y - old_mouse_pos.1;
-                        camera.track_mouse_delta(dx * frame_scale, dy * frame_scale);
+                        camera.track_mouse_delta(dx, dy);
                         self.input_state.mouse_delta = Some((dx, dy));
                         log_once::debug_once!("Fallback mouse tracking used");
                     } else {
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index b735abc..58e3452 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -21,7 +21,7 @@ use crossbeam_channel::{Receiver, Sender, unbounded};
 use docks::console::EucalyptusConsole;
 use dropbear_engine::animation::MorphTargetInfo;
 use dropbear_engine::billboarding::BillboardPipeline;
-use dropbear_engine::buffer::ResizableBuffer;
+use dropbear_engine::buffer::DynamicBuffer;
 use dropbear_engine::entity::EntityTransform;
 use dropbear_engine::graphics::InstanceRaw;
 use dropbear_engine::mipmap::MipMapper;
@@ -83,7 +83,7 @@ pub struct Editor {
     pub ui_editor_dock_state: DockState<EditorTabId>,
     pub texture_id: Option<egui::TextureId>,
     pub size: Extent3d,
-    pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>,
+    pub instance_buffer_cache: HashMap<u64, DynamicBuffer<InstanceRaw>>,
     pub color: Color,
 
     pub ui_editor: UiEditor,
@@ -99,7 +99,7 @@ pub struct Editor {
     pub billboard_pipeline: Option<BillboardPipeline>,
     pub kino: Option<KinoState>,
     pub animation_pipeline: Option<AnimationDefaults>,
-    pub(crate) animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>,
+    pub(crate) animated_instance_buffers: HashMap<Entity, DynamicBuffer<InstanceRaw>>,
     pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>,
     pub(crate) static_bind_group_cache: HashMap<u64, wgpu::BindGroup>,
     pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>, // key = morph_deltas_offset
diff --git a/crates/eucalyptus-exports/src/asset/model/ty.rs b/crates/eucalyptus-exports/src/asset/model/ty.rs
index aeded95..ddd61b4 100644
--- a/crates/eucalyptus-exports/src/asset/model/ty.rs
+++ b/crates/eucalyptus-exports/src/asset/model/ty.rs
@@ -22,7 +22,7 @@ pub struct NModelVertex {
 impl ToJObject for NModelVertex {
     fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
         let class = env
-            .load_class(jni_str!("com/dropbear/asset/model/ModelVertex"))
+            .load_class(jni_str!("com.dropbear.asset.model.ModelVertex"))
             .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
 
         let position = self.position.to_jobject(env)?;
@@ -80,6 +80,7 @@ pub struct NMesh {
     pub num_elements: i32,
     pub material_index: i32,
     pub vertices: Vec<NModelVertex>,
+    pub indices: Vec<u32>,
 }
 
 impl ToJObject for NMesh {
@@ -118,7 +119,8 @@ impl From<&Mesh> for NMesh {
             name: mesh.name.clone(),
             num_elements: mesh.num_elements as i32,
             material_index: mesh.material as i32,
-            vertices: mesh.vertices.iter().map(|v| (*v).into()).collect(),
+            vertices: mesh.vertex_buffer.data().iter().map(|v| (*v).into()).collect(),
+            indices: mesh.index_buffer.data().to_vec(),
         }
     }
 }
diff --git a/crates/goanna-gen/README.md b/crates/goanna-gen/README.md
index 9a57538..4c236c5 100644
--- a/crates/goanna-gen/README.md
+++ b/crates/goanna-gen/README.md
@@ -2,4 +2,8 @@
 
 A c header compile-time generator that sits in build.rs. 
 
-When paired with `dropbear_macro`, you can generate c headers and JNI headers easily for interop. 
\ No newline at end of file
+When paired with `dropbear_macro`, you can generate c headers and JNI headers easily for interop. 
+
+> [!NOTE]
+> AI has generated this crate. Yes, throw your rotten tomatoes at me, but I do not know how to generate
+> headers, and it makes my life easier to just get AI to create me a header generator. 
\ No newline at end of file
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 75b11ac..61360b4 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -3,7 +3,7 @@
 use crossbeam_channel::{Receiver, unbounded};
 use dropbear_engine::animation::MorphTargetInfo;
 use dropbear_engine::billboarding::BillboardPipeline;
-use dropbear_engine::buffer::ResizableBuffer;
+use dropbear_engine::buffer::DynamicBuffer;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::future::{FutureHandle, FutureQueue};
 use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
@@ -131,8 +131,8 @@ pub struct PlayMode {
     light_cube_pipeline: Option<LightCubePipeline>,
     main_pipeline: Option<MainRenderPipeline>,
     shader_globals: Option<GlobalsUniform>,
-    instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>,
-    animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>,
+    instance_buffer_cache: HashMap<u64, DynamicBuffer<InstanceRaw>>,
+    animated_instance_buffers: HashMap<Entity, DynamicBuffer<InstanceRaw>>,
     sky_pipeline: Option<SkyPipeline>,
     animation_pipeline: Option<AnimationDefaults>,
     billboard_pipeline: Option<BillboardPipeline>,
diff --git a/include/dropbear.h b/include/dropbear.h
index 6cbf417..e2c231e 100644
--- a/include/dropbear.h
+++ b/include/dropbear.h
@@ -304,11 +304,18 @@ typedef struct NModelVertexArray {
     size_t capacity;
 } NModelVertexArray;
 
+typedef struct u32Array {
+    uint32_t* values;
+    size_t length;
+    size_t capacity;
+} u32Array;
+
 typedef struct NMesh {
     const char* name;
     int32_t num_elements;
     int32_t material_index;
     NModelVertexArray vertices;
+    u32Array indices;
 } NMesh;
 
 typedef struct NMeshArray {
diff --git a/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
index e93890e..e17c45f 100644
--- a/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
+++ b/scripting/commonMain/kotlin/com/dropbear/asset/model/Mesh.kt
@@ -4,5 +4,6 @@ data class Mesh(
     val name: String,
     val numElements: Int,
     val materialIndex: Int,
-    val vertices: List<ModelVertex>
+    val vertices: List<ModelVertex>,
+    val indices: List<UInt>,
 )
\ No newline at end of file
diff --git a/scripting/nativeMain/kotlin/com/dropbear/asset/Model.native.kt b/scripting/nativeMain/kotlin/com/dropbear/asset/Model.native.kt
index ed69c06..f31e0b2 100644
--- a/scripting/nativeMain/kotlin/com/dropbear/asset/Model.native.kt
+++ b/scripting/nativeMain/kotlin/com/dropbear/asset/Model.native.kt
@@ -19,7 +19,13 @@ actual fun Model.getLabel(): String = memScoped {
     if (rc != 0) "" else out.value?.toKString() ?: ""
 }
 
-actual fun Model.getMeshes(): List<Mesh> = emptyList()      // complex C array parsing deferred
+actual fun Model.getMeshes(): List<Mesh> = memScoped {
+    TODO("tbc")
+//
+//    val result = dropbear_asset_model_get_meshes(
+//
+//    )
+}
 actual fun Model.getMaterials(): List<Material> = emptyList()
 actual fun Model.getSkins(): List<Skin> = emptyList()
 actual fun Model.getAnimations(): List<Animation> = emptyList()