use oxiui_core::UiError;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RingBufferStats {
pub total_allocations: u64,
pub wrap_count: u64,
pub grow_count: u64,
pub capacity_bytes: usize,
pub cursor_bytes: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct RingAllocation {
pub offset: u64,
pub size: u64,
}
pub struct RingBuffer {
pub buffer: wgpu::Buffer,
cursor: usize,
alignment: u64,
stats: RingBufferStats,
}
impl RingBuffer {
const MIN_CAPACITY: usize = 64 * 1024;
pub fn new(device: &wgpu::Device, initial_bytes: usize, alignment: u64) -> Self {
let capacity = initial_bytes.max(Self::MIN_CAPACITY).next_power_of_two();
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("oxiui-render-wgpu ring buffer"),
size: capacity as u64,
usage: wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::INDEX
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let stats = RingBufferStats {
capacity_bytes: capacity,
..Default::default()
};
Self {
buffer,
cursor: 0,
alignment: alignment.max(1),
stats,
}
}
pub fn reset(&mut self) {
self.cursor = 0;
}
pub fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &[u8],
) -> Result<RingAllocation, UiError> {
let size = data.len();
if size == 0 {
return Ok(RingAllocation { offset: 0, size: 0 });
}
let aligned_size = align_up(size as u64, self.alignment) as usize;
if self.cursor + aligned_size > self.stats.capacity_bytes {
self.cursor = 0;
self.stats.wrap_count += 1;
}
if aligned_size > self.stats.capacity_bytes {
self.grow(device, aligned_size)?;
}
let offset = self.cursor as u64;
queue.write_buffer(&self.buffer, offset, data);
self.cursor += aligned_size;
self.stats.total_allocations += 1;
self.stats.cursor_bytes = self.cursor;
Ok(RingAllocation {
offset,
size: size as u64,
})
}
pub fn grow(&mut self, device: &wgpu::Device, min_size: usize) -> Result<(), UiError> {
let new_cap = (self.stats.capacity_bytes * 2)
.max(min_size.next_power_of_two())
.max(Self::MIN_CAPACITY);
let new_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("oxiui-render-wgpu ring buffer (grown)"),
size: new_cap as u64,
usage: wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::INDEX
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.buffer = new_buf;
self.cursor = 0;
self.stats.capacity_bytes = new_cap;
self.stats.grow_count += 1;
self.stats.cursor_bytes = 0;
Ok(())
}
pub fn stats(&self) -> RingBufferStats {
let mut s = self.stats;
s.cursor_bytes = self.cursor;
s
}
pub fn capacity(&self) -> usize {
self.stats.capacity_bytes
}
pub fn cursor(&self) -> usize {
self.cursor
}
}
#[inline]
fn align_up(n: u64, align: u64) -> u64 {
let a = align.max(1);
n.div_ceil(a) * a
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn align_up_rounds_correctly() {
assert_eq!(align_up(0, 4), 0);
assert_eq!(align_up(1, 4), 4);
assert_eq!(align_up(4, 4), 4);
assert_eq!(align_up(5, 4), 8);
assert_eq!(align_up(256, 256), 256);
assert_eq!(align_up(257, 256), 512);
}
#[test]
fn ring_buffer_stats_default() {
let s = RingBufferStats::default();
assert_eq!(s.total_allocations, 0);
assert_eq!(s.wrap_count, 0);
assert_eq!(s.grow_count, 0);
}
#[test]
fn ring_allocation_size_preserved() {
let alloc = RingAllocation {
offset: 128,
size: 56,
};
assert_eq!(alloc.offset, 128);
assert_eq!(alloc.size, 56);
}
}