use std::collections::HashMap;
use std::sync::LazyLock;
use awsm_renderer_core::{
buffers::{BufferDescriptor, BufferUsage},
error::AwsmCoreError,
renderer::AwsmRendererWebGpu,
};
use awsm_renderer_materials::MaterialShaderId;
use crate::buffer::mapped_uploader::MappedUploader;
pub const DEFAULT_CAPACITY_WORDS: u32 = 262_144;
static BUFFER_USAGE: LazyLock<BufferUsage> =
LazyLock::new(|| BufferUsage::new().with_copy_dst().with_storage());
pub struct ExtrasPool {
pub(crate) buffer: web_sys::GpuBuffer,
pub(crate) capacity_words: u32,
next_offset: u32,
shadow: Vec<u32>,
slices: HashMap<(MaterialShaderId, usize), (u32, u32)>,
free_list: HashMap<u32, Vec<u32>>,
dirty_range: Option<(u32, u32)>,
uploader: MappedUploader,
}
impl ExtrasPool {
pub fn new(gpu: &AwsmRendererWebGpu, capacity_words: u32) -> Result<Self, AwsmCoreError> {
let capacity_words = capacity_words.max(1);
let buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("ExtrasPool"),
(capacity_words as usize) * 4,
*BUFFER_USAGE,
)
.into(),
)?;
Ok(Self {
buffer,
capacity_words,
next_offset: 0,
shadow: vec![0u32; capacity_words as usize],
slices: HashMap::new(),
free_list: HashMap::new(),
dirty_range: None,
uploader: MappedUploader::new("ExtrasPool"),
})
}
pub fn upload_stats(&self) -> crate::buffer::mapped_staging_ring::UploadStats {
self.uploader.stats()
}
pub fn slice_for(&self, shader_id: MaterialShaderId, slot_index: usize) -> Option<(u32, u32)> {
self.slices.get(&(shader_id, slot_index)).copied()
}
pub fn assign_or_update(
&mut self,
gpu: &AwsmRendererWebGpu,
shader_id: MaterialShaderId,
slot_index: usize,
data: &[u32],
) -> Result<AssignOutcome, ExtrasPoolError> {
let len = data.len() as u32;
let key = (shader_id, slot_index);
let mut resized = false;
let (offset, length) = match self.slices.get(&key) {
Some(&(off, prev_len)) if prev_len == len => (off, prev_len),
existing => {
if let Some(&(old_off, old_len)) = existing {
self.free_list.entry(old_len).or_default().push(old_off);
self.slices.remove(&key);
}
if let Some(stack) = self.free_list.get_mut(&len) {
if let Some(offset) = stack.pop() {
self.slices.insert(key, (offset, len));
(offset, len)
} else {
self.bump_allocate(gpu, key, len, &mut resized)?
}
} else {
self.bump_allocate(gpu, key, len, &mut resized)?
}
}
};
let start = offset as usize;
let end = start + length as usize;
self.shadow[start..end].copy_from_slice(data);
let dirty_start_bytes = offset * 4;
let dirty_end_bytes = (offset + length) * 4;
match self.dirty_range {
Some((s, e)) => {
self.dirty_range = Some((s.min(dirty_start_bytes), e.max(dirty_end_bytes)));
}
None => {
self.dirty_range = Some((dirty_start_bytes, dirty_end_bytes));
}
}
Ok(AssignOutcome {
offset,
length,
resized,
})
}
fn bump_allocate(
&mut self,
gpu: &AwsmRendererWebGpu,
key: (MaterialShaderId, usize),
len: u32,
resized: &mut bool,
) -> Result<(u32, u32), ExtrasPoolError> {
let offset = self.next_offset;
let new_next = offset.saturating_add(len);
if new_next > self.capacity_words {
let mut new_capacity = self.capacity_words;
while new_capacity < new_next {
new_capacity =
new_capacity
.checked_mul(2)
.ok_or(ExtrasPoolError::OutOfCapacity {
needed: new_next,
capacity: self.capacity_words,
})?;
}
self.grow_to(gpu, new_capacity)?;
*resized = true;
}
self.next_offset = offset.saturating_add(len);
self.slices.insert(key, (offset, len));
Ok((offset, len))
}
pub fn drop_shader(&mut self, shader_id: MaterialShaderId) -> usize {
let drained: Vec<((MaterialShaderId, usize), (u32, u32))> = self
.slices
.iter()
.filter(|((sid, _), _)| *sid == shader_id)
.map(|(k, v)| (*k, *v))
.collect();
let count = drained.len();
for (key, (offset, len)) in drained {
self.slices.remove(&key);
self.free_list.entry(len).or_default().push(offset);
}
count
}
fn grow_to(
&mut self,
gpu: &AwsmRendererWebGpu,
new_capacity_words: u32,
) -> Result<(), ExtrasPoolError> {
debug_assert!(new_capacity_words > self.capacity_words);
let new_buffer = gpu
.create_buffer(
&BufferDescriptor::new(
Some("ExtrasPool"),
(new_capacity_words as usize) * 4,
*BUFFER_USAGE,
)
.into(),
)
.map_err(ExtrasPoolError::Core)?;
self.buffer = new_buffer;
self.capacity_words = new_capacity_words;
self.shadow.resize(new_capacity_words as usize, 0);
if self.next_offset > 0 {
let live_end_bytes = self.next_offset * 4;
self.dirty_range = match self.dirty_range {
Some((_, e)) => Some((0, e.max(live_end_bytes))),
None => Some((0, live_end_bytes)),
};
}
tracing::info!(
target: "awsm_renderer::extras_pool",
"ExtrasPool grew to {} words ({} bytes); live bytes={}",
new_capacity_words,
(new_capacity_words as usize) * 4,
self.next_offset * 4,
);
Ok(())
}
pub fn write_gpu(&mut self, gpu: &AwsmRendererWebGpu) -> Result<(), AwsmCoreError> {
let Some((start_bytes, end_bytes)) = self.dirty_range.take() else {
return Ok(());
};
let shadow_bytes: &[u8] = unsafe {
std::slice::from_raw_parts(self.shadow.as_ptr() as *const u8, self.shadow.len() * 4)
};
self.uploader.write_dirty_ranges(
gpu,
&self.buffer,
shadow_bytes.len(),
shadow_bytes,
&[(start_bytes as usize, end_bytes as usize)],
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct AssignOutcome {
pub offset: u32,
pub length: u32,
pub resized: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum ExtrasPoolError {
#[error("[extras-pool] out of capacity: needed {needed} words, capacity {capacity}")]
OutOfCapacity {
needed: u32,
capacity: u32,
},
#[error("[extras-pool] gpu buffer creation failed: {0:?}")]
Core(AwsmCoreError),
}