use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
pub const STAGING_SLOTS: usize = 3;
pub const MIN_SLOT_BYTES: u64 = 64 * 1024;
pub const MAX_SLOT_BYTES: u64 = 32 * 1024 * 1024;
pub const STAGING_USAGE: wgpu::BufferUsages =
wgpu::BufferUsages::MAP_WRITE.union(wgpu::BufferUsages::COPY_SRC);
pub const BUFFER_COPY_ALIGNMENT: u64 = 16;
pub const TEXTURE_COPY_ALIGNMENT: u64 = 512;
#[must_use]
pub fn align_up(value: u64, alignment: u64) -> Option<u64> {
value.checked_next_multiple_of(alignment.max(1))
}
#[must_use]
pub fn slot_capacity(needed: u64) -> u64 {
needed
.checked_next_power_of_two()
.unwrap_or(needed)
.max(MIN_SLOT_BYTES)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotState {
Ready,
Recording,
InFlight,
Abandoned,
Failed,
}
impl SlotState {
const fn to_u8(self) -> u8 {
match self {
Self::Ready => 0,
Self::Recording => 1,
Self::InFlight => 2,
Self::Abandoned => 3,
Self::Failed => 4,
}
}
const fn from_u8(raw: u8) -> Self {
match raw {
0 => Self::Ready,
1 => Self::Recording,
2 => Self::InFlight,
3 => Self::Abandoned,
_ => Self::Failed,
}
}
}
#[derive(Debug, Clone)]
pub struct SlotFence(Arc<AtomicU8>);
impl SlotFence {
#[must_use]
pub fn ready() -> Self {
Self(Arc::new(AtomicU8::new(SlotState::Ready.to_u8())))
}
#[must_use]
pub fn state(&self) -> SlotState {
SlotState::from_u8(self.0.load(Ordering::Acquire))
}
fn set(&self, state: SlotState) {
self.0.store(state.to_u8(), Ordering::Release);
}
#[must_use]
pub fn arm(&self) -> FenceToken {
self.set(SlotState::InFlight);
FenceToken {
fence: Some(Arc::clone(&self.0)),
}
}
}
#[derive(Debug)]
pub struct FenceToken {
fence: Option<Arc<AtomicU8>>,
}
impl FenceToken {
pub fn signal(mut self, mapped: bool) {
if let Some(fence) = self.fence.take() {
let state = if mapped {
SlotState::Ready
} else {
SlotState::Failed
};
fence.store(state.to_u8(), Ordering::Release);
}
}
}
impl Drop for FenceToken {
fn drop(&mut self) {
if let Some(fence) = self.fence.take() {
let _ = fence.compare_exchange(
SlotState::InFlight.to_u8(),
SlotState::Abandoned.to_u8(),
Ordering::AcqRel,
Ordering::Acquire,
);
}
}
}
pub trait StagingAllocator {
type Buffer: Clone;
fn create_mapped(&self, size: u64) -> Self::Buffer;
}
impl StagingAllocator for wgpu::Device {
type Buffer = wgpu::Buffer;
fn create_mapped(&self, size: u64) -> wgpu::Buffer {
self.create_buffer(&wgpu::BufferDescriptor {
label: Some("frust-gpu staging ring slot"),
size,
usage: STAGING_USAGE,
mapped_at_creation: true,
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StagingStats {
pub frames: u64,
pub busy: u64,
pub too_large: u64,
pub buffers_created: u64,
}
#[derive(Debug)]
struct Slot<B> {
buffer: B,
capacity: u64,
fence: SlotFence,
}
#[derive(Debug)]
pub struct StagingRing<B = wgpu::Buffer> {
slots: Vec<Slot<B>>,
max_slots: usize,
max_slot_bytes: u64,
stats: StagingStats,
}
impl<B> Default for StagingRing<B> {
fn default() -> Self {
Self::new()
}
}
impl<B> StagingRing<B> {
#[must_use]
pub fn new() -> Self {
Self::with_limits(STAGING_SLOTS, MAX_SLOT_BYTES)
}
#[must_use]
pub fn with_limits(max_slots: usize, max_slot_bytes: u64) -> Self {
Self {
slots: Vec::with_capacity(max_slots),
max_slots: max_slots.max(1),
max_slot_bytes,
stats: StagingStats::default(),
}
}
#[must_use]
pub fn stats(&self) -> StagingStats {
self.stats
}
#[must_use]
pub fn slot_count(&self) -> usize {
self.slots.len()
}
#[must_use]
pub fn slot(&self, index: usize) -> Option<(u64, SlotState)> {
self.slots
.get(index)
.map(|slot| (slot.capacity, slot.fence.state()))
}
fn has_free_slot(&self) -> bool {
self.slots.len() < self.max_slots
|| self.slots.iter().any(|slot| {
matches!(
slot.fence.state(),
SlotState::Ready | SlotState::Abandoned | SlotState::Failed
)
})
}
}
impl<B: Clone> StagingRing<B> {
pub fn acquire<A>(&mut self, allocator: &A, bytes: u64) -> Option<SlotLease<B>>
where
A: StagingAllocator<Buffer = B>,
{
let bytes = bytes.max(wgpu::COPY_BUFFER_ALIGNMENT);
if bytes > self.max_slot_bytes {
self.stats.too_large = self.stats.too_large.saturating_add(1);
return None;
}
let capacity = slot_capacity(bytes).min(self.max_slot_bytes.max(bytes));
let fitting = self
.slots
.iter()
.position(|slot| slot.fence.state() == SlotState::Ready && slot.capacity >= bytes);
let index = if let Some(index) = fitting {
index
} else if let Some(index) = self.slots.iter().position(|slot| {
matches!(
slot.fence.state(),
SlotState::Ready | SlotState::Abandoned | SlotState::Failed
)
}) {
let slot = self.slots.get_mut(index)?;
slot.buffer = allocator.create_mapped(capacity);
slot.capacity = capacity;
slot.fence = SlotFence::ready();
self.stats.buffers_created = self.stats.buffers_created.saturating_add(1);
index
} else if self.slots.len() < self.max_slots {
self.slots.push(Slot {
buffer: allocator.create_mapped(capacity),
capacity,
fence: SlotFence::ready(),
});
self.stats.buffers_created = self.stats.buffers_created.saturating_add(1);
self.slots.len() - 1
} else {
self.stats.busy = self.stats.busy.saturating_add(1);
return None;
};
let slot = self.slots.get(index)?;
slot.fence.set(SlotState::Recording);
self.stats.frames = self.stats.frames.saturating_add(1);
Some(SlotLease {
index,
buffer: slot.buffer.clone(),
capacity: slot.capacity,
cursor: 0,
fence: slot.fence.clone(),
})
}
}
#[derive(Debug)]
pub struct SlotLease<B> {
index: usize,
buffer: B,
capacity: u64,
cursor: u64,
fence: SlotFence,
}
impl<B> SlotLease<B> {
#[must_use]
pub fn index(&self) -> usize {
self.index
}
#[must_use]
pub fn buffer(&self) -> &B {
&self.buffer
}
#[must_use]
pub fn capacity(&self) -> u64 {
self.capacity
}
#[must_use]
pub fn used(&self) -> u64 {
self.cursor
}
pub fn reserve(&mut self, len: u64, alignment: u64) -> Option<u64> {
let offset = align_up(self.cursor, alignment)?;
let end = offset.checked_add(len)?;
if end > self.capacity {
return None;
}
self.cursor = end;
Some(offset)
}
#[must_use]
pub fn finish(mut self) -> FenceToken {
let token = self.fence.arm();
self.cursor = u64::MAX;
token
}
}
impl<B> Drop for SlotLease<B> {
fn drop(&mut self) {
if self.cursor != u64::MAX && self.fence.state() == SlotState::Recording {
self.fence.set(SlotState::Abandoned);
}
}
}
impl StagingRing<wgpu::Buffer> {
pub fn begin(&mut self, device: &wgpu::Device, bytes: u64) -> Option<StagingFrame> {
if !self.has_free_slot() {
let _ = device.poll(wgpu::PollType::Poll);
}
let lease = self.acquire(device, bytes)?;
let view = lease
.buffer()
.slice(..lease.capacity())
.get_mapped_range_mut();
match view {
Ok(view) => Some(StagingFrame {
lease: Some(lease),
view: Some(view),
}),
Err(_) => None,
}
}
}
#[derive(Debug)]
pub struct StagingFrame {
lease: Option<SlotLease<wgpu::Buffer>>,
view: Option<wgpu::BufferViewMut>,
}
impl StagingFrame {
#[must_use]
pub fn used(&self) -> u64 {
self.lease.as_ref().map_or(0, SlotLease::used)
}
#[must_use]
pub fn slot(&self) -> Option<usize> {
self.lease.as_ref().map(SlotLease::index)
}
pub fn copy_to_buffer(
&mut self,
encoder: &mut wgpu::CommandEncoder,
bytes: &[u8],
dst: &wgpu::Buffer,
dst_offset: u64,
) -> bool {
self.copy_parts_to_buffer(encoder, &[bytes], dst, dst_offset)
}
pub fn copy_parts_to_buffer(
&mut self,
encoder: &mut wgpu::CommandEncoder,
parts: &[&[u8]],
dst: &wgpu::Buffer,
dst_offset: u64,
) -> bool {
let len: u64 = parts.iter().map(|part| part.len() as u64).sum();
if len == 0 {
return true;
}
if !len.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT)
|| !dst_offset.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT)
{
return false;
}
let (Some(lease), Some(view)) = (self.lease.as_mut(), self.view.as_mut()) else {
return false;
};
let Some(offset) = lease.reserve(len, BUFFER_COPY_ALIGNMENT) else {
return false;
};
let Ok(mut start) = usize::try_from(offset) else {
return false;
};
for part in parts {
let end = start + part.len();
view.slice(start..end).copy_from_slice(part);
start = end;
}
encoder.copy_buffer_to_buffer(lease.buffer(), offset, dst, dst_offset, len);
true
}
pub fn copy_to_texture(
&mut self,
encoder: &mut wgpu::CommandEncoder,
dst: wgpu::TexelCopyTextureInfo<'_>,
bytes_per_row: u32,
rows: u32,
extent: wgpu::Extent3d,
fill: impl FnOnce(wgpu::WriteOnly<'_, [u8]>),
) -> bool {
if !bytes_per_row.is_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) {
return false;
}
let len = u64::from(bytes_per_row) * u64::from(rows);
if len == 0 {
return false;
}
let (Some(lease), Some(view)) = (self.lease.as_mut(), self.view.as_mut()) else {
return false;
};
let Some(offset) = lease.reserve(len, TEXTURE_COPY_ALIGNMENT) else {
return false;
};
let (Ok(start), Ok(end)) = (usize::try_from(offset), usize::try_from(offset + len)) else {
return false;
};
fill(view.slice(start..end));
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: lease.buffer(),
layout: wgpu::TexelCopyBufferLayout {
offset,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(rows),
},
},
dst,
extent,
);
true
}
pub fn finish(mut self, encoder: &wgpu::CommandEncoder) {
drop(self.view.take());
if let Some(lease) = self.lease.take() {
lease.buffer().unmap();
let buffer = lease.buffer().clone();
let token = lease.finish();
encoder.map_buffer_on_submit(&buffer, wgpu::MapMode::Write, .., move |result| {
token.signal(result.is_ok());
});
}
}
}
impl Drop for StagingFrame {
fn drop(&mut self) {
drop(self.view.take());
if let Some(lease) = self.lease.take() {
lease.buffer().unmap();
}
}
}