use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::{Duration, Instant};
pub(crate) const CHUNK_SIZE: u64 = teksilo_canvas::live_image::internal::BAND_BYTES;
pub(crate) const IDLE_LIFETIME: Duration = Duration::from_secs(1);
const OFFSET_ALIGNMENT: u64 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u64;
enum ChunkState {
Free,
Writing { used: u64 },
Submitted,
Mapping { mapped: Arc<AtomicU8> },
}
const MAPPING: u8 = 0;
const MAPPED: u8 = 1;
const MAP_FAILED: u8 = 2;
struct Chunk {
buffer: wgpu::Buffer,
size: u64,
state: ChunkState,
last_used: Instant,
}
pub(crate) struct Staged {
pub(crate) buffer: wgpu::Buffer,
pub(crate) offset: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StagingStats {
pub chunks: usize,
pub bytes: u64,
pub created: u64,
pub freed: u64,
}
#[derive(Default)]
pub(crate) struct StagingPool {
chunks: Vec<Chunk>,
created: u64,
freed: u64,
idle_lifetime: Option<Duration>,
fail_next: bool,
}
impl StagingPool {
pub(crate) fn stage(
&mut self,
device: &wgpu::Device,
bytes: u64,
fill: impl FnOnce(&mut wgpu::BufferViewMut),
) -> Option<Staged> {
let now = Instant::now();
let at = match self.room_for(bytes) {
Some(at) => at,
None => {
let _ = device.poll(wgpu::PollType::Poll);
self.take_back();
match self.room_for(bytes) {
Some(at) => at,
None => self.create(device, bytes.max(CHUNK_SIZE), now)?,
}
}
};
let chunk = &mut self.chunks[at];
let offset = match chunk.state {
ChunkState::Writing { used } => used.next_multiple_of(OFFSET_ALIGNMENT),
_ => 0,
};
{
let mut view = chunk
.buffer
.slice(offset..offset + bytes)
.get_mapped_range_mut()
.ok()?;
fill(&mut view);
}
chunk.state = ChunkState::Writing {
used: offset + bytes,
};
chunk.last_used = now;
Some(Staged {
buffer: chunk.buffer.clone(),
offset,
})
}
pub(crate) fn finish_frame(&mut self) {
for chunk in &mut self.chunks {
if matches!(chunk.state, ChunkState::Writing { .. }) {
chunk.buffer.unmap();
chunk.state = ChunkState::Submitted;
}
}
}
pub(crate) fn after_submit(&mut self) -> bool {
for chunk in &mut self.chunks {
if matches!(chunk.state, ChunkState::Submitted) {
let mapped = Arc::new(AtomicU8::new(MAPPING));
let done = Arc::clone(&mapped);
chunk
.buffer
.slice(..)
.map_async(wgpu::MapMode::Write, move |result| {
let state = if result.is_ok() { MAPPED } else { MAP_FAILED };
done.store(state, Ordering::Release);
});
chunk.state = ChunkState::Mapping { mapped };
}
}
self.take_back();
let now = Instant::now();
let lifetime = self.idle_lifetime.unwrap_or(IDLE_LIFETIME);
let before = self.chunks.len();
self.chunks.retain(|chunk| {
!(matches!(chunk.state, ChunkState::Free)
&& now.saturating_duration_since(chunk.last_used) >= lifetime)
});
let freed = before - self.chunks.len();
self.freed += freed as u64;
freed > 0
}
pub(crate) fn stats(&self) -> StagingStats {
StagingStats {
chunks: self.chunks.len(),
bytes: self.chunks.iter().map(|c| c.size).sum(),
created: self.created,
freed: self.freed,
}
}
pub(crate) fn set_idle_lifetime(&mut self, lifetime: Duration) {
self.idle_lifetime = Some(lifetime);
}
pub(crate) fn fail_next(&mut self) {
self.fail_next = true;
}
fn room_for(&self, bytes: u64) -> Option<usize> {
let writing = self.chunks.iter().position(|c| match c.state {
ChunkState::Writing { used } => {
used.next_multiple_of(OFFSET_ALIGNMENT) + bytes <= c.size
}
_ => false,
});
writing.or_else(|| {
self.chunks
.iter()
.position(|c| matches!(c.state, ChunkState::Free) && bytes <= c.size)
})
}
fn take_back(&mut self) {
self.chunks.retain(|chunk| match &chunk.state {
ChunkState::Mapping { mapped } => mapped.load(Ordering::Acquire) != MAP_FAILED,
_ => true,
});
for chunk in &mut self.chunks {
if let ChunkState::Mapping { mapped } = &chunk.state
&& mapped.load(Ordering::Acquire) == MAPPED
{
chunk.state = ChunkState::Free;
}
}
}
pub(crate) fn release_all(&mut self) -> bool {
let held = !self.chunks.is_empty();
self.freed += self.chunks.len() as u64;
self.chunks.clear();
held
}
fn create(&mut self, device: &wgpu::Device, size: u64, now: Instant) -> Option<usize> {
if std::mem::take(&mut self.fail_next) {
return None;
}
let memory = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("live_image_staging"),
size,
usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: true,
});
if pollster::block_on(memory.pop()).is_some() {
return None;
}
self.created += 1;
self.chunks.push(Chunk {
buffer,
size,
state: ChunkState::Free,
last_used: now,
});
Some(self.chunks.len() - 1)
}
}