use awsm_renderer_core::{
buffers::{BufferDescriptor, BufferUsage},
error::AwsmCoreError,
renderer::AwsmRendererWebGpu,
};
pub const INDIRECT_ARGS_STRIDE: u32 = 16;
pub const MAX_CLASSIFY_TILE_BYTES: u32 = 32 * 1024 * 1024;
pub const MIN_CLASSIFY_BUCKET_CAPACITY: u32 = 1024;
pub fn capped_bucket_capacity(requested: u32, bucket_count: u32) -> u32 {
let bucket_count = bucket_count.max(1);
let budget_cap = (MAX_CLASSIFY_TILE_BYTES / 8 / bucket_count).max(MIN_CLASSIFY_BUCKET_CAPACITY);
requested.max(1).min(budget_cap)
}
pub fn header_bytes(bucket_count: u32) -> u32 {
let args_bytes = bucket_count * INDIRECT_ARGS_STRIDE;
let offsets_bytes = bucket_count * 4;
let capacity_bytes = 4;
let unpadded = args_bytes + offsets_bytes + capacity_bytes;
(unpadded + 15) & !15
}
pub struct ClassifyBuffers {
pub buffer: web_sys::GpuBuffer,
pub bucket_capacity: u32,
pub bucket_count: u32,
pub size_bytes: u32,
header_scratch: Vec<u8>,
}
impl ClassifyBuffers {
pub fn new(
gpu: &AwsmRendererWebGpu,
bucket_capacity: u32,
bucket_count: u32,
) -> Result<Self, AwsmCoreError> {
let bucket_count = bucket_count.max(1);
let bucket_capacity = capped_bucket_capacity(bucket_capacity, bucket_count);
let header = header_bytes(bucket_count);
let tiles_bytes = bucket_capacity
.saturating_mul(bucket_count)
.saturating_mul(8);
let size_bytes = header + tiles_bytes;
let buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("MaterialClassifyBuckets"),
size_bytes as usize,
BufferUsage::new()
.with_storage()
.with_indirect()
.with_copy_dst(),
)
.into(),
)?;
let mut header_scratch = vec![0u8; header as usize];
write_header(&mut header_scratch, bucket_capacity, bucket_count);
Ok(Self {
buffer,
bucket_capacity,
bucket_count,
size_bytes,
header_scratch,
})
}
pub fn ensure_capacity(
&mut self,
gpu: &AwsmRendererWebGpu,
needed_capacity: u32,
) -> Result<bool, AwsmCoreError> {
let target = capped_bucket_capacity(
needed_capacity.saturating_mul(2).max(needed_capacity),
self.bucket_count,
);
if target <= self.bucket_capacity {
return Ok(false);
}
*self = Self::new(gpu, target, self.bucket_count)?;
Ok(true)
}
pub fn ensure_bucket_count(
&mut self,
gpu: &AwsmRendererWebGpu,
needed_bucket_count: u32,
) -> Result<bool, AwsmCoreError> {
if needed_bucket_count <= self.bucket_count {
return Ok(false);
}
*self = Self::new(gpu, self.bucket_capacity, needed_bucket_count)?;
Ok(true)
}
pub fn reset_header(&self, gpu: &AwsmRendererWebGpu) -> Result<(), AwsmCoreError> {
gpu.write_buffer(
&self.buffer,
None,
self.header_scratch.as_slice(),
None,
None,
)
}
}
pub fn write_header(dst: &mut [u8], bucket_capacity: u32, bucket_count: u32) {
let one = 1u32.to_ne_bytes();
for bucket in 0..bucket_count as usize {
let base = bucket * INDIRECT_ARGS_STRIDE as usize;
dst[base..base + 4].copy_from_slice(&[0; 4]); dst[base + 4..base + 8].copy_from_slice(&one); dst[base + 8..base + 12].copy_from_slice(&one); dst[base + 12..base + 16].copy_from_slice(&[0; 4]); }
let offsets_base = (bucket_count * INDIRECT_ARGS_STRIDE) as usize;
for bucket in 0..bucket_count as usize {
let off = (bucket as u32).saturating_mul(bucket_capacity);
let dst_base = offsets_base + bucket * 4;
dst[dst_base..dst_base + 4].copy_from_slice(&off.to_ne_bytes());
}
let cap_base = offsets_base + (bucket_count * 4) as usize;
dst[cap_base..cap_base + 4].copy_from_slice(&bucket_capacity.to_ne_bytes());
}
pub fn indirect_args_offset(bucket_index: u32) -> u32 {
bucket_index * INDIRECT_ARGS_STRIDE
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_tiles_capped_to_budget_at_high_counts() {
const TILE_COUNT_720P: u32 = 14400; let requested = TILE_COUNT_720P * 2;
for &bc in &[1u32, 5, 16, 32, 64] {
assert_eq!(
capped_bucket_capacity(requested, bc),
requested,
"cap must not bind at {bc} buckets (no typical-case regression)"
);
}
for &bc in &[254u32, 512, 1024, 4096, 65534] {
let cap = capped_bucket_capacity(requested, bc);
let total_bytes = (cap as u64) * (bc as u64) * 8;
assert!(
cap == MIN_CLASSIFY_BUCKET_CAPACITY
|| total_bytes <= MAX_CLASSIFY_TILE_BYTES as u64,
"at {bc} buckets: cap={cap}, total={total_bytes} exceeds budget"
);
assert!(cap >= MIN_CLASSIFY_BUCKET_CAPACITY);
}
let cap_1024 = capped_bucket_capacity(requested, 1029);
let bytes_1024 = (cap_1024 as u64) * 1029 * 8;
assert!(
bytes_1024
<= MAX_CLASSIFY_TILE_BYTES as u64
+ (MIN_CLASSIFY_BUCKET_CAPACITY as u64 * 1029 * 8),
"1024-bucket tiles array {bytes_1024} B not bounded"
);
assert!(
bytes_1024 < 64 * 1024 * 1024,
"1024-bucket tiles array should be well under 64 MB, got {bytes_1024}"
);
}
}