use std::sync::LazyLock;
use awsm_renderer_core::{
buffers::{BufferDescriptor, BufferUsage},
error::AwsmCoreError,
renderer::AwsmRendererWebGpu,
};
pub const TILE_PIXEL_SIZE: u32 = 16;
pub const DEFAULT_SLICE_COUNT: u32 = 32;
pub const DEFAULT_MAX_PER_FROXEL_CAPACITY: u32 = 32;
pub const DEFAULT_MESH_INDICES_CAPACITY: u32 = 4;
pub const DEFAULT_TILE_LIGHT_CAPACITY: u32 = 16;
pub const MAX_TILE_LIGHT_CAPACITY: u32 = crate::lights::MAX_PUNCTUAL_LIGHTS as u32;
pub const CULL_PARAMS_BYTE_SIZE: usize = 64;
pub const STORAGE_ENTRY_BYTE_SIZE: usize = 4;
pub const OVERFLOW_BYTE_SIZE: usize = 4;
pub const OVERFLOW_READBACK_BYTES: usize = OVERFLOW_BYTE_SIZE;
static PARAMS_USAGE: LazyLock<BufferUsage> =
LazyLock::new(|| BufferUsage::new().with_uniform().with_copy_dst());
static STORAGE_USAGE: LazyLock<BufferUsage> = LazyLock::new(|| {
BufferUsage::new()
.with_storage()
.with_copy_dst()
.with_copy_src()
});
static OVERFLOW_USAGE: LazyLock<BufferUsage> = LazyLock::new(|| {
BufferUsage::new()
.with_storage()
.with_copy_src()
.with_copy_dst()
});
static OVERFLOW_READBACK_USAGE: LazyLock<BufferUsage> =
LazyLock::new(|| BufferUsage::new().with_map_read().with_copy_dst());
static TILE_LIGHTS_USAGE: LazyLock<BufferUsage> =
LazyLock::new(|| BufferUsage::new().with_storage());
pub struct LightCullingBuffers {
pub params_buffer: web_sys::GpuBuffer,
pub storage_buffer: web_sys::GpuBuffer,
pub tile_lights_buffer: web_sys::GpuBuffer,
pub overflow_buffer: web_sys::GpuBuffer,
pub overflow_readback_buffer: web_sys::GpuBuffer,
pub slice_count: u32,
pub max_per_froxel_capacity: u32,
pub tile_light_capacity: u32,
pub mesh_indices_capacity_u32: u32,
pub viewport_w: u32,
pub viewport_h: u32,
pub froxel_count: u32,
last_params: Option<[u8; CULL_PARAMS_BYTE_SIZE]>,
zero_overflow: Vec<u8>,
}
impl LightCullingBuffers {
pub fn new(
gpu: &AwsmRendererWebGpu,
viewport_w: u32,
viewport_h: u32,
slice_count: u32,
max_per_froxel_capacity: u32,
mesh_indices_capacity_u32: u32,
tile_light_capacity: u32,
) -> Result<Self, AwsmCoreError> {
let viewport_w = viewport_w.max(1);
let viewport_h = viewport_h.max(1);
let mesh_indices_capacity_u32 =
mesh_indices_capacity_u32.max(DEFAULT_MESH_INDICES_CAPACITY);
let tile_light_capacity =
tile_light_capacity.clamp(DEFAULT_TILE_LIGHT_CAPACITY, MAX_TILE_LIGHT_CAPACITY);
let tiles_x = viewport_w.div_ceil(TILE_PIXEL_SIZE);
let tiles_y = viewport_h.div_ceil(TILE_PIXEL_SIZE);
let froxel_count = tiles_x
.saturating_mul(tiles_y)
.saturating_mul(slice_count)
.max(1);
let params_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingParams"),
CULL_PARAMS_BYTE_SIZE,
*PARAMS_USAGE,
)
.into(),
)?;
let stride = max_per_froxel_capacity.saturating_add(1).max(2);
let froxel_region_entries = froxel_count.saturating_mul(stride);
let storage_entries = mesh_indices_capacity_u32
.saturating_add(froxel_region_entries)
.max(1);
let storage_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingStorage"),
storage_entries as usize * STORAGE_ENTRY_BYTE_SIZE,
*STORAGE_USAGE,
)
.into(),
)?;
let tile_count = tiles_x.saturating_mul(tiles_y).max(1);
let tile_lights_entries = tile_count
.saturating_mul(tile_light_capacity.saturating_add(1))
.max(1);
let tile_lights_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingTileLights"),
tile_lights_entries as usize * STORAGE_ENTRY_BYTE_SIZE,
*TILE_LIGHTS_USAGE,
)
.into(),
)?;
let overflow_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingOverflow"),
OVERFLOW_BYTE_SIZE,
*OVERFLOW_USAGE,
)
.into(),
)?;
let overflow_readback_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingOverflowReadback"),
OVERFLOW_READBACK_BYTES,
*OVERFLOW_READBACK_USAGE,
)
.into(),
)?;
Ok(Self {
params_buffer,
storage_buffer,
tile_lights_buffer,
overflow_buffer,
overflow_readback_buffer,
slice_count,
max_per_froxel_capacity,
mesh_indices_capacity_u32,
tile_light_capacity,
viewport_w,
viewport_h,
froxel_count,
last_params: None,
zero_overflow: vec![0u8; OVERFLOW_BYTE_SIZE],
})
}
pub fn ensure_viewport(
&mut self,
gpu: &AwsmRendererWebGpu,
viewport_w: u32,
viewport_h: u32,
) -> Result<bool, AwsmCoreError> {
let viewport_w = viewport_w.max(1);
let viewport_h = viewport_h.max(1);
let tiles_x_new = viewport_w.div_ceil(TILE_PIXEL_SIZE);
let tiles_y_new = viewport_h.div_ceil(TILE_PIXEL_SIZE);
let froxel_count_new = tiles_x_new
.saturating_mul(tiles_y_new)
.saturating_mul(self.slice_count)
.max(1);
if viewport_w == self.viewport_w
&& viewport_h == self.viewport_h
&& froxel_count_new == self.froxel_count
{
return Ok(false);
}
if froxel_count_new <= self.froxel_count
&& viewport_w == self.viewport_w
&& viewport_h == self.viewport_h
{
return Ok(false);
}
if froxel_count_new <= self.froxel_count {
self.viewport_w = viewport_w;
self.viewport_h = viewport_h;
return Ok(false);
}
*self = Self::new(
gpu,
viewport_w,
viewport_h,
self.slice_count,
self.max_per_froxel_capacity,
self.mesh_indices_capacity_u32,
self.tile_light_capacity,
)?;
Ok(true)
}
pub fn set_max_per_froxel_capacity(
&mut self,
gpu: &AwsmRendererWebGpu,
new_capacity: u32,
) -> Result<bool, AwsmCoreError> {
if new_capacity == self.max_per_froxel_capacity {
return Ok(false);
}
*self = Self::new(
gpu,
self.viewport_w,
self.viewport_h,
self.slice_count,
new_capacity,
self.mesh_indices_capacity_u32,
self.tile_light_capacity,
)?;
Ok(true)
}
pub fn ensure_tile_light_capacity(
&mut self,
gpu: &AwsmRendererWebGpu,
needed: u32,
) -> Result<bool, AwsmCoreError> {
if needed <= self.tile_light_capacity {
return Ok(false);
}
let new_capacity = needed
.checked_next_power_of_two()
.unwrap_or(MAX_TILE_LIGHT_CAPACITY)
.clamp(DEFAULT_TILE_LIGHT_CAPACITY, MAX_TILE_LIGHT_CAPACITY);
let tile_count = self.tiles_x().saturating_mul(self.tiles_y()).max(1);
let entries = tile_count
.saturating_mul(new_capacity.saturating_add(1))
.max(1);
self.tile_lights_buffer = gpu.create_buffer(
&BufferDescriptor::new(
Some("LightCullingTileLights"),
entries as usize * STORAGE_ENTRY_BYTE_SIZE,
*TILE_LIGHTS_USAGE,
)
.into(),
)?;
self.tile_light_capacity = new_capacity;
Ok(true)
}
pub fn tiles_x(&self) -> u32 {
self.viewport_w.div_ceil(TILE_PIXEL_SIZE)
}
pub fn tiles_y(&self) -> u32 {
self.viewport_h.div_ceil(TILE_PIXEL_SIZE)
}
pub fn write_params(
&mut self,
gpu: &AwsmRendererWebGpu,
z_near: f32,
z_far: f32,
debug_light_heatmap: u32,
debug_view_mode: u32,
debug_wireframe: u32,
) -> Result<(), AwsmCoreError> {
let tiles_x = self.tiles_x();
let tiles_y = self.tiles_y();
let log_far_over_near = (z_far / z_near.max(f32::EPSILON)).ln();
let mut bytes = [0u8; CULL_PARAMS_BYTE_SIZE];
bytes[0..4].copy_from_slice(&tiles_x.to_ne_bytes());
bytes[4..8].copy_from_slice(&tiles_y.to_ne_bytes());
bytes[8..12].copy_from_slice(&self.viewport_w.to_ne_bytes());
bytes[12..16].copy_from_slice(&self.viewport_h.to_ne_bytes());
bytes[16..20].copy_from_slice(&self.mesh_indices_capacity_u32.to_ne_bytes());
bytes[20..24].copy_from_slice(&self.max_per_froxel_capacity.to_ne_bytes());
bytes[24..28].copy_from_slice(&self.tile_light_capacity.to_ne_bytes());
bytes[28..32].copy_from_slice(&z_near.to_ne_bytes());
bytes[32..36].copy_from_slice(&z_far.to_ne_bytes());
bytes[36..40].copy_from_slice(&log_far_over_near.to_ne_bytes());
bytes[40..44].copy_from_slice(&debug_light_heatmap.to_ne_bytes());
bytes[44..48].copy_from_slice(&debug_view_mode.to_ne_bytes());
bytes[48..52].copy_from_slice(&debug_wireframe.to_ne_bytes());
if self.last_params == Some(bytes) {
return Ok(());
}
gpu.write_buffer(&self.params_buffer, None, bytes.as_slice(), None, None)?;
self.last_params = Some(bytes);
Ok(())
}
pub fn reset_overflow(&self, gpu: &AwsmRendererWebGpu) -> Result<(), AwsmCoreError> {
gpu.write_buffer(
&self.overflow_buffer,
None,
self.zero_overflow.as_slice(),
None,
None,
)
}
}