concinnity_core/render/decal/
set.rs1use alloc::vec::Vec;
7use core::cell::Cell;
8use core::fmt;
9
10use crate::gfx::frustum::Frustum;
11use crate::render::decal::DecalRecord;
12use crate::render::frame_dirty::FrameDirty;
13use crate::render::uniforms::DecalParams;
14
15const EDGE_FADE_POW: f32 = 2.0;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct AtCapacity;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RemoveError {
28 OutOfRange,
30 AlreadyRemoved,
32}
33
34impl fmt::Display for RemoveError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::OutOfRange => f.write_str("out of range"),
38 Self::AlreadyRemoved => f.write_str("already removed"),
39 }
40 }
41}
42
43struct Slot {
47 record: DecalRecord,
48 aabb_min: [f32; 3],
49 aabb_max: [f32; 3],
50 params: DecalParams,
51 dirty: Cell<FrameDirty>,
52}
53
54impl Slot {
55 fn new(record: DecalRecord, frames: usize) -> Self {
56 let (aabb_min, aabb_max) = record.aabb();
57 Self {
58 aabb_min,
59 aabb_max,
60 params: DecalParams {
61 model: record.model,
62 inv_model: record.inv_model,
63 tint: record.tint,
64 fade_pow: EDGE_FADE_POW,
65 _pad0: 0.0,
66 _pad1: 0.0,
67 _pad2: 0.0,
68 },
69 record,
70 dirty: Cell::new(FrameDirty::new(frames)),
71 }
72 }
73}
74
75pub struct DecalSet {
98 slots: Vec<Option<Slot>>,
99 free_slots: Vec<usize>,
100 live: usize,
101 capacity: usize,
102 frames: usize,
103}
104
105impl DecalSet {
106 pub fn new(capacity: usize, frames: usize) -> Self {
109 Self {
110 slots: Vec::new(),
111 free_slots: Vec::new(),
112 live: 0,
113 capacity,
114 frames,
115 }
116 }
117
118 pub fn insert(&mut self, record: DecalRecord) -> Result<usize, AtCapacity> {
122 let slot = Slot::new(record, self.frames);
123 let id = match self.free_slots.pop() {
124 Some(id) => {
125 self.slots[id] = Some(slot);
126 id
127 }
128 None => {
129 if self.slots.len() >= self.capacity {
130 return Err(AtCapacity);
131 }
132 self.slots.push(Some(slot));
133 self.slots.len() - 1
134 }
135 };
136 self.live += 1;
137 Ok(id)
138 }
139
140 pub fn remove(&mut self, id: usize) -> Result<(), RemoveError> {
142 let slot = self.slots.get_mut(id).ok_or(RemoveError::OutOfRange)?;
143 if slot.take().is_none() {
144 return Err(RemoveError::AlreadyRemoved);
145 }
146 self.free_slots.push(id);
147 self.live -= 1;
148 Ok(())
149 }
150
151 pub fn is_empty(&self) -> bool {
153 self.live == 0
154 }
155
156 pub fn visible<'a>(
160 &'a self,
161 frustum: &'a Frustum,
162 ) -> impl Iterator<Item = VisibleDecal<'a>> + 'a {
163 self.slots.iter().enumerate().filter_map(move |(id, slot)| {
164 let slot = slot.as_ref()?;
165 if !frustum.intersects_aabb(slot.aabb_min, slot.aabb_max) {
166 return None;
167 }
168 Some(VisibleDecal {
169 id,
170 record: &slot.record,
171 params: &slot.params,
172 dirty: &slot.dirty,
173 })
174 })
175 }
176}
177
178pub struct VisibleDecal<'a> {
181 pub id: usize,
184 pub record: &'a DecalRecord,
186 pub params: &'a DecalParams,
188 dirty: &'a Cell<FrameDirty>,
189}
190
191impl VisibleDecal<'_> {
192 pub fn take_upload(&self, frame: usize) -> bool {
197 let mut dirty = self.dirty.get();
198 let pending = dirty.take(frame);
199 self.dirty.set(dirty);
200 pending
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::render::decal::decal_model_matrix;
208 use crate::render::decal::invert_decal_model;
209
210 fn record_at(position: [f32; 3]) -> DecalRecord {
211 let model = decal_model_matrix(position, [0.0; 3], [1.0; 3]);
212 DecalRecord {
213 model,
214 inv_model: invert_decal_model(model).expect("unit decal inverts"),
215 texture_slot: 0,
216 tint: [1.0; 4],
217 }
218 }
219
220 fn frustum_containing_everything() -> Frustum {
223 use crate::gfx::frustum::Plane;
224 let plane = |normal: [f32; 3]| Plane { normal, d: 1.0e6 };
225 Frustum {
226 planes: [
227 plane([1.0, 0.0, 0.0]),
228 plane([-1.0, 0.0, 0.0]),
229 plane([0.0, 1.0, 0.0]),
230 plane([0.0, -1.0, 0.0]),
231 plane([0.0, 0.0, 1.0]),
232 plane([0.0, 0.0, -1.0]),
233 ],
234 }
235 }
236
237 fn frustum_left_of_ten() -> Frustum {
239 use crate::gfx::frustum::Plane;
240 let wide = |normal: [f32; 3]| Plane { normal, d: 1.0e6 };
241 let mut planes = frustum_containing_everything().planes;
242 planes[0] = Plane {
243 normal: [-1.0, 0.0, 0.0],
244 d: 10.0,
245 };
246 planes[1] = wide([1.0, 0.0, 0.0]);
247 Frustum { planes }
248 }
249
250 #[test]
251 fn an_empty_set_is_empty_and_draws_nothing() {
252 let decals = DecalSet::new(4, 2);
253 assert!(decals.is_empty());
254 assert_eq!(decals.visible(&frustum_containing_everything()).count(), 0);
255 }
256
257 #[test]
258 fn insert_hands_out_ascending_ids_up_to_capacity() {
259 let mut decals = DecalSet::new(2, 2);
260 assert_eq!(decals.insert(record_at([0.0; 3])), Ok(0));
261 assert_eq!(decals.insert(record_at([0.0; 3])), Ok(1));
262 assert_eq!(decals.insert(record_at([0.0; 3])), Err(AtCapacity));
263 assert!(!decals.is_empty());
264 }
265
266 #[test]
267 fn a_removed_slot_is_reused_before_the_table_grows() {
268 let mut decals = DecalSet::new(2, 2);
269 let first = decals.insert(record_at([0.0; 3])).expect("first slot");
270 decals.insert(record_at([0.0; 3])).expect("second slot");
271 decals.remove(first).expect("live slot");
272 assert_eq!(decals.insert(record_at([0.0; 3])), Ok(first));
273 assert_eq!(decals.insert(record_at([0.0; 3])), Err(AtCapacity));
275 }
276
277 #[test]
278 fn remove_rejects_unknown_and_repeated_ids() {
279 let mut decals = DecalSet::new(2, 2);
280 assert_eq!(decals.remove(0), Err(RemoveError::OutOfRange));
281 let id = decals.insert(record_at([0.0; 3])).expect("free slot");
282 assert_eq!(decals.remove(id), Ok(()));
283 assert_eq!(decals.remove(id), Err(RemoveError::AlreadyRemoved));
284 assert!(decals.is_empty());
285 }
286
287 #[test]
288 fn remove_errors_read_as_the_backend_messages() {
289 use alloc::format;
290 assert_eq!(format!("{}", RemoveError::OutOfRange), "out of range");
291 assert_eq!(
292 format!("{}", RemoveError::AlreadyRemoved),
293 "already removed"
294 );
295 }
296
297 #[test]
298 fn only_slots_meeting_the_frustum_are_visible() {
299 let mut decals = DecalSet::new(4, 2);
300 let near = decals.insert(record_at([0.0; 3])).expect("free slot");
301 let far = decals
302 .insert(record_at([100.0, 0.0, 0.0]))
303 .expect("free slot");
304 let frustum = frustum_left_of_ten();
305 let ids: Vec<usize> = decals.visible(&frustum).map(|d| d.id).collect();
306 assert_eq!(ids, alloc::vec![near]);
307 let all: Vec<usize> = decals
309 .visible(&frustum_containing_everything())
310 .map(|d| d.id)
311 .collect();
312 assert_eq!(all, alloc::vec![near, far]);
313 }
314
315 #[test]
316 fn a_tombstoned_slot_never_becomes_visible() {
317 let mut decals = DecalSet::new(4, 2);
318 let id = decals.insert(record_at([0.0; 3])).expect("free slot");
319 decals.remove(id).expect("live slot");
320 assert_eq!(decals.visible(&frustum_containing_everything()).count(), 0);
321 }
322
323 #[test]
324 fn visible_carries_the_cached_params_of_its_record() {
325 let mut decals = DecalSet::new(4, 2);
326 let record = record_at([1.0, 2.0, 3.0]);
327 decals.insert(record).expect("free slot");
328 let frustum = frustum_containing_everything();
329 let decal = decals.visible(&frustum).next().expect("one visible decal");
330 assert_eq!(decal.record.model, record.model);
331 assert_eq!(decal.params.model, record.model);
332 assert_eq!(decal.params.inv_model, record.inv_model);
333 assert_eq!(decal.params.tint, record.tint);
334 assert_eq!(decal.params.fade_pow, EDGE_FADE_POW);
335 }
336
337 #[test]
338 fn a_new_slot_uploads_once_per_frame_in_flight() {
339 let mut decals = DecalSet::new(4, 3);
340 decals.insert(record_at([0.0; 3])).expect("free slot");
341 let frustum = frustum_containing_everything();
342 for frame in 0..3 {
343 let decal = decals.visible(&frustum).next().expect("visible");
344 assert!(decal.take_upload(frame), "frame {frame} seeds stale");
345 }
346 for frame in (0..3).cycle().take(9) {
348 let decal = decals.visible(&frustum).next().expect("visible");
349 assert!(!decal.take_upload(frame), "frame {frame} stays clean");
350 }
351 }
352
353 #[test]
354 fn a_reused_slot_is_stale_again_for_every_frame() {
355 let mut decals = DecalSet::new(4, 2);
356 let id = decals.insert(record_at([0.0; 3])).expect("free slot");
357 let frustum = frustum_containing_everything();
358 for frame in 0..2 {
359 assert!(
360 decals
361 .visible(&frustum)
362 .next()
363 .expect("visible")
364 .take_upload(frame)
365 );
366 }
367 decals.remove(id).expect("live slot");
368 assert_eq!(decals.insert(record_at([5.0, 0.0, 0.0])), Ok(id));
369 for frame in 0..2 {
370 let decal = decals.visible(&frustum).next().expect("visible");
371 assert!(
372 decal.take_upload(frame),
373 "frame {frame} re-armed by the add"
374 );
375 }
376 }
377
378 #[test]
379 fn a_decal_culled_this_frame_keeps_its_pending_upload() {
380 let mut decals = DecalSet::new(4, 2);
381 decals
382 .insert(record_at([100.0, 0.0, 0.0]))
383 .expect("free slot");
384 assert_eq!(decals.visible(&frustum_left_of_ten()).count(), 0);
386 let frustum = frustum_containing_everything();
387 for frame in 0..2 {
388 let decal = decals.visible(&frustum).next().expect("visible");
389 assert!(decal.take_upload(frame), "frame {frame} still owes a write");
390 }
391 }
392}