use crate::dirty::DirtyRanges;
use astrelis_core::profiling::profile_function;
use astrelis_render::wgpu;
use bytemuck::Pod;
pub struct InstanceBuffer<T: Pod> {
buffer: wgpu::Buffer,
instances: Vec<T>,
capacity: usize,
dirty_ranges: DirtyRanges,
write_count: u64,
}
impl<T: Pod> InstanceBuffer<T> {
pub fn new(device: &wgpu::Device, label: Option<&str>, capacity: usize) -> Self {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label,
size: (capacity * std::mem::size_of::<T>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
buffer,
instances: Vec::with_capacity(capacity),
capacity,
dirty_ranges: DirtyRanges::new(),
write_count: 0,
}
}
pub fn buffer(&self) -> &wgpu::Buffer {
&self.buffer
}
pub fn instances(&self) -> &[T] {
&self.instances
}
pub fn len(&self) -> usize {
self.instances.len()
}
pub fn is_empty(&self) -> bool {
self.instances.is_empty()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn clear(&mut self) {
self.instances.clear();
self.dirty_ranges.clear();
}
pub fn set_instances(&mut self, device: &wgpu::Device, instances: Vec<T>) {
let new_len = instances.len();
if new_len > self.capacity {
self.reallocate(device, new_len.next_power_of_two());
}
self.instances = instances;
if !self.instances.is_empty() {
self.dirty_ranges.mark_dirty(0, self.instances.len());
}
}
pub fn update_range(&mut self, start: usize, new_data: &[T]) {
if new_data.is_empty() || start >= self.instances.len() {
return;
}
let end = (start + new_data.len()).min(self.instances.len());
let actual_len = end - start;
self.instances[start..end].copy_from_slice(&new_data[..actual_len]);
self.dirty_ranges.mark_dirty(start, end);
}
pub fn update_instance(&mut self, index: usize, instance: T) {
if index < self.instances.len() {
self.instances[index] = instance;
self.dirty_ranges.mark_dirty(index, index + 1);
}
}
pub fn append(&mut self, device: &wgpu::Device, new_instances: &[T]) {
let start_idx = self.instances.len();
let new_len = start_idx + new_instances.len();
if new_len > self.capacity {
self.reallocate(device, new_len.next_power_of_two());
}
self.instances.extend_from_slice(new_instances);
self.dirty_ranges.mark_dirty(start_idx, new_len);
}
pub fn upload_dirty(&mut self, queue: &wgpu::Queue) {
profile_function!();
if self.dirty_ranges.is_empty() {
return;
}
let instance_size = std::mem::size_of::<T>() as u64;
for range in self.dirty_ranges.iter() {
let start = range.start;
let end = range.end.min(self.instances.len());
if start >= end {
continue;
}
let offset = (start as u64) * instance_size;
let data = bytemuck::cast_slice(&self.instances[start..end]);
queue.write_buffer(&self.buffer, offset, data);
self.write_count += 1;
}
self.dirty_ranges.clear();
}
pub fn upload_all(&mut self, queue: &wgpu::Queue) {
if self.instances.is_empty() {
return;
}
let data = bytemuck::cast_slice(&self.instances);
queue.write_buffer(&self.buffer, 0, data);
self.write_count += 1;
self.dirty_ranges.clear();
}
pub fn dirty_ranges(&self) -> &DirtyRanges {
&self.dirty_ranges
}
pub fn write_count(&self) -> u64 {
self.write_count
}
fn reallocate(&mut self, device: &wgpu::Device, new_capacity: usize) {
self.buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("UI Instance Buffer (Reallocated)"),
size: (new_capacity * std::mem::size_of::<T>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.capacity = new_capacity;
if !self.instances.is_empty() {
self.dirty_ranges.mark_dirty(0, self.instances.len());
}
}
pub fn stats(&self) -> InstanceBufferStats {
InstanceBufferStats {
instance_count: self.instances.len(),
capacity: self.capacity,
utilization: if self.capacity > 0 {
(self.instances.len() as f32 / self.capacity as f32) * 100.0
} else {
0.0
},
dirty_ranges: self.dirty_ranges.stats().num_ranges,
write_count: self.write_count,
size_bytes: self.instances.len() * std::mem::size_of::<T>(),
capacity_bytes: self.capacity * std::mem::size_of::<T>(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct InstanceBufferStats {
pub instance_count: usize,
pub capacity: usize,
pub utilization: f32,
pub dirty_ranges: usize,
pub write_count: u64,
pub size_bytes: usize,
pub capacity_bytes: usize,
}
pub struct RingInstanceBuffer<T: Pod> {
buffers: Vec<InstanceBuffer<T>>,
current_frame: usize,
frame_count: usize,
}
impl<T: Pod> RingInstanceBuffer<T> {
pub fn new(
device: &wgpu::Device,
label_prefix: &str,
frame_count: usize,
capacity: usize,
) -> Self {
let mut buffers = Vec::with_capacity(frame_count);
for i in 0..frame_count {
let label = format!("{} Frame {}", label_prefix, i);
buffers.push(InstanceBuffer::new(device, Some(&label), capacity));
}
Self {
buffers,
current_frame: 0,
frame_count,
}
}
pub fn current(&self) -> &InstanceBuffer<T> {
&self.buffers[self.current_frame]
}
pub fn current_mut(&mut self) -> &mut InstanceBuffer<T> {
&mut self.buffers[self.current_frame]
}
pub fn advance_frame(&mut self) {
self.current_frame = (self.current_frame + 1) % self.frame_count;
}
pub fn buffers(&self) -> &[InstanceBuffer<T>] {
&self.buffers
}
pub fn frame_index(&self) -> usize {
self.current_frame
}
}
#[cfg(test)]
mod tests {
use super::*;
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
struct TestInstance {
position: [f32; 2],
color: [f32; 4],
}
impl TestInstance {
fn new(x: f32, y: f32, r: f32, g: f32, b: f32, a: f32) -> Self {
Self {
position: [x, y],
color: [r, g, b, a],
}
}
}
#[test]
fn test_instance_tracking() {
let instances = vec![
TestInstance::new(0.0, 0.0, 1.0, 0.0, 0.0, 1.0),
TestInstance::new(10.0, 10.0, 0.0, 1.0, 0.0, 1.0),
];
assert_eq!(instances.len(), 2);
assert_eq!(instances[0].position, [0.0, 0.0]);
}
#[test]
fn test_dirty_range_tracking() {
let mut dirty_ranges = DirtyRanges::new();
dirty_ranges.mark_dirty(0, 5);
dirty_ranges.mark_dirty(10, 15);
assert_eq!(dirty_ranges.len(), 2);
assert_eq!(dirty_ranges.total_dirty_count(), 10);
}
#[test]
fn test_capacity_calculation() {
let capacity = 100;
let instance_size = std::mem::size_of::<TestInstance>();
let buffer_size = capacity * instance_size;
assert_eq!(buffer_size, capacity * 24); }
#[test]
fn test_stats_calculation() {
let instance_count = 75;
let capacity = 100;
let utilization = (instance_count as f32 / capacity as f32) * 100.0;
assert_eq!(utilization, 75.0);
}
}