use alloc::vec::Vec;
use core::cell::Cell;
use core::fmt;
use crate::gfx::frustum::Frustum;
use crate::render::decal::DecalRecord;
use crate::render::frame_dirty::FrameDirty;
use crate::render::uniforms::DecalParams;
const EDGE_FADE_POW: f32 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtCapacity;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoveError {
OutOfRange,
AlreadyRemoved,
}
impl fmt::Display for RemoveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OutOfRange => f.write_str("out of range"),
Self::AlreadyRemoved => f.write_str("already removed"),
}
}
}
struct Slot {
record: DecalRecord,
aabb_min: [f32; 3],
aabb_max: [f32; 3],
params: DecalParams,
dirty: Cell<FrameDirty>,
}
impl Slot {
fn new(record: DecalRecord, frames: usize) -> Self {
let (aabb_min, aabb_max) = record.aabb();
Self {
aabb_min,
aabb_max,
params: DecalParams {
model: record.model,
inv_model: record.inv_model,
tint: record.tint,
fade_pow: EDGE_FADE_POW,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
},
record,
dirty: Cell::new(FrameDirty::new(frames)),
}
}
}
pub struct DecalSet {
slots: Vec<Option<Slot>>,
free_slots: Vec<usize>,
live: usize,
capacity: usize,
frames: usize,
}
impl DecalSet {
pub fn new(capacity: usize, frames: usize) -> Self {
Self {
slots: Vec::new(),
free_slots: Vec::new(),
live: 0,
capacity,
frames,
}
}
pub fn insert(&mut self, record: DecalRecord) -> Result<usize, AtCapacity> {
let slot = Slot::new(record, self.frames);
let id = match self.free_slots.pop() {
Some(id) => {
self.slots[id] = Some(slot);
id
}
None => {
if self.slots.len() >= self.capacity {
return Err(AtCapacity);
}
self.slots.push(Some(slot));
self.slots.len() - 1
}
};
self.live += 1;
Ok(id)
}
pub fn remove(&mut self, id: usize) -> Result<(), RemoveError> {
let slot = self.slots.get_mut(id).ok_or(RemoveError::OutOfRange)?;
if slot.take().is_none() {
return Err(RemoveError::AlreadyRemoved);
}
self.free_slots.push(id);
self.live -= 1;
Ok(())
}
pub fn is_empty(&self) -> bool {
self.live == 0
}
pub fn visible<'a>(
&'a self,
frustum: &'a Frustum,
) -> impl Iterator<Item = VisibleDecal<'a>> + 'a {
self.slots.iter().enumerate().filter_map(move |(id, slot)| {
let slot = slot.as_ref()?;
if !frustum.intersects_aabb(slot.aabb_min, slot.aabb_max) {
return None;
}
Some(VisibleDecal {
id,
record: &slot.record,
params: &slot.params,
dirty: &slot.dirty,
})
})
}
}
pub struct VisibleDecal<'a> {
pub id: usize,
pub record: &'a DecalRecord,
pub params: &'a DecalParams,
dirty: &'a Cell<FrameDirty>,
}
impl VisibleDecal<'_> {
pub fn take_upload(&self, frame: usize) -> bool {
let mut dirty = self.dirty.get();
let pending = dirty.take(frame);
self.dirty.set(dirty);
pending
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::decal::decal_model_matrix;
use crate::render::decal::invert_decal_model;
fn record_at(position: [f32; 3]) -> DecalRecord {
let model = decal_model_matrix(position, [0.0; 3], [1.0; 3]);
DecalRecord {
model,
inv_model: invert_decal_model(model).expect("unit decal inverts"),
texture_slot: 0,
tint: [1.0; 4],
}
}
fn frustum_containing_everything() -> Frustum {
use crate::gfx::frustum::Plane;
let plane = |normal: [f32; 3]| Plane { normal, d: 1.0e6 };
Frustum {
planes: [
plane([1.0, 0.0, 0.0]),
plane([-1.0, 0.0, 0.0]),
plane([0.0, 1.0, 0.0]),
plane([0.0, -1.0, 0.0]),
plane([0.0, 0.0, 1.0]),
plane([0.0, 0.0, -1.0]),
],
}
}
fn frustum_left_of_ten() -> Frustum {
use crate::gfx::frustum::Plane;
let wide = |normal: [f32; 3]| Plane { normal, d: 1.0e6 };
let mut planes = frustum_containing_everything().planes;
planes[0] = Plane {
normal: [-1.0, 0.0, 0.0],
d: 10.0,
};
planes[1] = wide([1.0, 0.0, 0.0]);
Frustum { planes }
}
#[test]
fn an_empty_set_is_empty_and_draws_nothing() {
let decals = DecalSet::new(4, 2);
assert!(decals.is_empty());
assert_eq!(decals.visible(&frustum_containing_everything()).count(), 0);
}
#[test]
fn insert_hands_out_ascending_ids_up_to_capacity() {
let mut decals = DecalSet::new(2, 2);
assert_eq!(decals.insert(record_at([0.0; 3])), Ok(0));
assert_eq!(decals.insert(record_at([0.0; 3])), Ok(1));
assert_eq!(decals.insert(record_at([0.0; 3])), Err(AtCapacity));
assert!(!decals.is_empty());
}
#[test]
fn a_removed_slot_is_reused_before_the_table_grows() {
let mut decals = DecalSet::new(2, 2);
let first = decals.insert(record_at([0.0; 3])).expect("first slot");
decals.insert(record_at([0.0; 3])).expect("second slot");
decals.remove(first).expect("live slot");
assert_eq!(decals.insert(record_at([0.0; 3])), Ok(first));
assert_eq!(decals.insert(record_at([0.0; 3])), Err(AtCapacity));
}
#[test]
fn remove_rejects_unknown_and_repeated_ids() {
let mut decals = DecalSet::new(2, 2);
assert_eq!(decals.remove(0), Err(RemoveError::OutOfRange));
let id = decals.insert(record_at([0.0; 3])).expect("free slot");
assert_eq!(decals.remove(id), Ok(()));
assert_eq!(decals.remove(id), Err(RemoveError::AlreadyRemoved));
assert!(decals.is_empty());
}
#[test]
fn remove_errors_read_as_the_backend_messages() {
use alloc::format;
assert_eq!(format!("{}", RemoveError::OutOfRange), "out of range");
assert_eq!(
format!("{}", RemoveError::AlreadyRemoved),
"already removed"
);
}
#[test]
fn only_slots_meeting_the_frustum_are_visible() {
let mut decals = DecalSet::new(4, 2);
let near = decals.insert(record_at([0.0; 3])).expect("free slot");
let far = decals
.insert(record_at([100.0, 0.0, 0.0]))
.expect("free slot");
let frustum = frustum_left_of_ten();
let ids: Vec<usize> = decals.visible(&frustum).map(|d| d.id).collect();
assert_eq!(ids, alloc::vec![near]);
let all: Vec<usize> = decals
.visible(&frustum_containing_everything())
.map(|d| d.id)
.collect();
assert_eq!(all, alloc::vec![near, far]);
}
#[test]
fn a_tombstoned_slot_never_becomes_visible() {
let mut decals = DecalSet::new(4, 2);
let id = decals.insert(record_at([0.0; 3])).expect("free slot");
decals.remove(id).expect("live slot");
assert_eq!(decals.visible(&frustum_containing_everything()).count(), 0);
}
#[test]
fn visible_carries_the_cached_params_of_its_record() {
let mut decals = DecalSet::new(4, 2);
let record = record_at([1.0, 2.0, 3.0]);
decals.insert(record).expect("free slot");
let frustum = frustum_containing_everything();
let decal = decals.visible(&frustum).next().expect("one visible decal");
assert_eq!(decal.record.model, record.model);
assert_eq!(decal.params.model, record.model);
assert_eq!(decal.params.inv_model, record.inv_model);
assert_eq!(decal.params.tint, record.tint);
assert_eq!(decal.params.fade_pow, EDGE_FADE_POW);
}
#[test]
fn a_new_slot_uploads_once_per_frame_in_flight() {
let mut decals = DecalSet::new(4, 3);
decals.insert(record_at([0.0; 3])).expect("free slot");
let frustum = frustum_containing_everything();
for frame in 0..3 {
let decal = decals.visible(&frustum).next().expect("visible");
assert!(decal.take_upload(frame), "frame {frame} seeds stale");
}
for frame in (0..3).cycle().take(9) {
let decal = decals.visible(&frustum).next().expect("visible");
assert!(!decal.take_upload(frame), "frame {frame} stays clean");
}
}
#[test]
fn a_reused_slot_is_stale_again_for_every_frame() {
let mut decals = DecalSet::new(4, 2);
let id = decals.insert(record_at([0.0; 3])).expect("free slot");
let frustum = frustum_containing_everything();
for frame in 0..2 {
assert!(
decals
.visible(&frustum)
.next()
.expect("visible")
.take_upload(frame)
);
}
decals.remove(id).expect("live slot");
assert_eq!(decals.insert(record_at([5.0, 0.0, 0.0])), Ok(id));
for frame in 0..2 {
let decal = decals.visible(&frustum).next().expect("visible");
assert!(
decal.take_upload(frame),
"frame {frame} re-armed by the add"
);
}
}
#[test]
fn a_decal_culled_this_frame_keeps_its_pending_upload() {
let mut decals = DecalSet::new(4, 2);
decals
.insert(record_at([100.0, 0.0, 0.0]))
.expect("free slot");
assert_eq!(decals.visible(&frustum_left_of_ten()).count(), 0);
let frustum = frustum_containing_everything();
for frame in 0..2 {
let decal = decals.visible(&frustum).next().expect("visible");
assert!(decal.take_upload(frame), "frame {frame} still owes a write");
}
}
}