use awsm_renderer_core::{
buffers::{BufferDescriptor, BufferUsage},
error::AwsmCoreError,
renderer::AwsmRendererWebGpu,
};
use crate::dynamic_materials::BucketEntry;
pub const BUCKET_LUT_NOT_FOUND: u32 = 0xFFFF_FFFF;
pub struct MaterialBucketLut {
pub buffer: web_sys::GpuBuffer,
capacity: u32,
}
impl MaterialBucketLut {
pub fn new(gpu: &AwsmRendererWebGpu, entries: &[BucketEntry]) -> Result<Self, AwsmCoreError> {
let bytes = build_lut_bytes(entries);
let capacity = (bytes.len() / 4) as u32;
let buffer = create_buffer(gpu, capacity)?;
gpu.write_buffer(&buffer, None, bytes.as_slice(), None, None)?;
Ok(Self { buffer, capacity })
}
pub fn ensure(
&mut self,
gpu: &AwsmRendererWebGpu,
entries: &[BucketEntry],
) -> Result<bool, AwsmCoreError> {
let bytes = build_lut_bytes(entries);
let needed = (bytes.len() / 4) as u32;
let recreated = if needed > self.capacity {
self.buffer = create_buffer(gpu, needed)?;
self.capacity = needed;
true
} else {
false
};
gpu.write_buffer(&self.buffer, None, bytes.as_slice(), None, None)?;
Ok(recreated)
}
}
fn build_lut_bytes(entries: &[BucketEntry]) -> Vec<u8> {
let max_sid = entries
.iter()
.map(|e| e.shader_id.as_u32())
.max()
.unwrap_or(0);
let len = max_sid as usize + 1;
let mut bytes = vec![0xFFu8; len * 4];
for (bucket_index, entry) in entries.iter().enumerate() {
let slot = entry.shader_id.as_u32() as usize;
let base = slot * 4;
bytes[base..base + 4].copy_from_slice(&(bucket_index as u32).to_ne_bytes());
}
bytes
}
fn create_buffer(
gpu: &AwsmRendererWebGpu,
capacity_u32: u32,
) -> Result<web_sys::GpuBuffer, AwsmCoreError> {
gpu.create_buffer(
&BufferDescriptor::new(
Some("MaterialClassifyBucketLut"),
(capacity_u32.max(1) * 4) as usize,
BufferUsage::new().with_storage().with_copy_dst(),
)
.into(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic_materials::{first_party_bucket_entries, BucketEntry, ShadingBase};
use awsm_renderer_materials::MaterialShaderId;
fn u32s(bytes: &[u8]) -> Vec<u32> {
bytes
.chunks_exact(4)
.map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
fn entry(shader_id: MaterialShaderId, name: &str) -> BucketEntry {
BucketEntry {
shader_id,
base: ShadingBase::Custom,
pbr_features: 0,
name: name.to_string(),
}
}
#[test]
fn lut_maps_sparse_holey_id_list_to_bucket_index() {
let entries = vec![
entry(MaterialShaderId::SKYBOX, "skybox"),
entry(MaterialShaderId::TOON, "toon"),
entry(MaterialShaderId::from_dynamic_raw(10_000), "a"),
entry(MaterialShaderId::from_dynamic_raw(10_005), "b"),
];
let lut = u32s(&build_lut_bytes(&entries));
assert_eq!(lut.len(), 10_006); assert_eq!(lut[0], 0);
assert_eq!(lut[3], 1);
assert_eq!(lut[10_000], 2);
assert_eq!(lut[10_005], 3);
assert_eq!(lut[1], BUCKET_LUT_NOT_FOUND);
assert_eq!(lut[2], BUCKET_LUT_NOT_FOUND);
assert_eq!(lut[4], BUCKET_LUT_NOT_FOUND);
assert_eq!(lut[9_999], BUCKET_LUT_NOT_FOUND);
assert_eq!(lut[10_001], BUCKET_LUT_NOT_FOUND);
let mapped = lut.iter().filter(|&&v| v != BUCKET_LUT_NOT_FOUND).count();
assert_eq!(mapped, entries.len());
}
#[test]
fn lut_seeds_from_first_party_entries() {
let entries = first_party_bucket_entries();
let lut = u32s(&build_lut_bytes(&entries));
assert_eq!(lut[0], 0);
let mapped = lut.iter().filter(|&&v| v != BUCKET_LUT_NOT_FOUND).count();
assert_eq!(mapped, entries.len());
}
}