use alloc::vec::Vec;
pub struct CullStatus;
impl CullStatus {
pub const DRAWN: u32 = 0;
pub const HIZ_CANDIDATE: u32 = 1;
pub const CULLED: u32 = 2;
pub const REDRAW: u32 = 3;
pub const HIZ_CULLED: u32 = 4;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CullStatusCounts {
pub drawn: u32,
pub frustum_culled: u32,
pub hiz_candidate: u32,
pub redrawn: u32,
pub hiz_culled: u32,
pub unknown: u32,
}
impl CullStatusCounts {
pub fn visible(self) -> u32 {
self.drawn + self.redrawn
}
pub fn hiz_rejected(self) -> u32 {
self.hiz_candidate + self.hiz_culled
}
pub fn total(self) -> u32 {
self.drawn
+ self.frustum_culled
+ self.hiz_candidate
+ self.redrawn
+ self.hiz_culled
+ self.unknown
}
}
pub fn tally(raw: &[u32]) -> CullStatusCounts {
let mut c = CullStatusCounts::default();
for &status in raw {
let slot = match status {
CullStatus::DRAWN => &mut c.drawn,
CullStatus::CULLED => &mut c.frustum_culled,
CullStatus::HIZ_CANDIDATE => &mut c.hiz_candidate,
CullStatus::REDRAW => &mut c.redrawn,
CullStatus::HIZ_CULLED => &mut c.hiz_culled,
_ => &mut c.unknown,
};
*slot += 1;
}
c
}
pub fn decode(bytes: &[u8], count: usize) -> Result<Vec<u32>, &'static str> {
if bytes.len() < count * core::mem::size_of::<u32>() {
return Err("cull-status readback is shorter than the live object count");
}
Ok(bytes[..count * core::mem::size_of::<u32>()]
.chunks_exact(4)
.map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tally_counts_every_outcome() {
let raw = [
CullStatus::DRAWN,
CullStatus::DRAWN,
CullStatus::CULLED,
CullStatus::HIZ_CANDIDATE,
CullStatus::REDRAW,
CullStatus::HIZ_CULLED,
CullStatus::HIZ_CULLED,
];
let c = tally(&raw);
assert_eq!(c.drawn, 2);
assert_eq!(c.frustum_culled, 1);
assert_eq!(c.hiz_candidate, 1);
assert_eq!(c.redrawn, 1);
assert_eq!(c.hiz_culled, 2);
assert_eq!(c.unknown, 0);
assert_eq!(c.total(), raw.len() as u32);
assert_eq!(c.visible(), 3);
assert_eq!(c.hiz_rejected(), 3);
}
#[test]
fn tally_of_nothing_is_all_zero() {
assert_eq!(tally(&[]), CullStatusCounts::default());
assert_eq!(tally(&[]).total(), 0);
}
#[test]
fn unnamed_status_values_land_in_unknown() {
let c = tally(&[5, 7, u32::MAX]);
assert_eq!(c.unknown, 3);
assert_eq!(c.total(), 3);
assert_eq!(c.visible(), 0);
assert_eq!(c.hiz_rejected(), 0);
}
#[test]
fn decode_truncates_to_the_live_object_count() {
let mut bytes = Vec::new();
for v in [CullStatus::DRAWN, CullStatus::CULLED, 9u32] {
bytes.extend_from_slice(&v.to_ne_bytes());
}
let decoded = decode(&bytes, 2).expect("two objects fit");
assert_eq!(decoded, [CullStatus::DRAWN, CullStatus::CULLED]);
assert_eq!(tally(&decoded).unknown, 0);
}
#[test]
fn decode_rejects_a_short_mapping() {
let bytes = [0u8; 4];
assert!(decode(&bytes, 2).is_err());
assert!(decode(&bytes, 1).is_ok());
assert!(decode(&[], 0).is_ok());
}
#[test]
fn constants_match_cull_slang() {
let declared = parse_shader_statuses(crate::render::shaders::CULL);
let expected = [
("STATUS_DRAWN", CullStatus::DRAWN),
("STATUS_HIZ_CANDIDATE", CullStatus::HIZ_CANDIDATE),
("STATUS_CULLED", CullStatus::CULLED),
("STATUS_REDRAW", CullStatus::REDRAW),
("STATUS_HIZ_CULLED", CullStatus::HIZ_CULLED),
];
assert_eq!(
declared.len(),
expected.len(),
"cull.slang declares {} STATUS_* constants, the host names {}: {declared:?}",
declared.len(),
expected.len(),
);
for (name, value) in expected {
let found = declared
.iter()
.find(|(n, _)| n == name)
.unwrap_or_else(|| panic!("cull.slang declares no {name}"));
assert_eq!(found.1, value, "{name} disagrees with cull.slang");
}
}
fn parse_shader_statuses(src: &str) -> Vec<(alloc::string::String, u32)> {
src.lines()
.filter_map(|line| {
let rest = line.trim().strip_prefix("static const uint STATUS_")?;
let (name, rest) = rest.split_once('=')?;
let value = rest.split_once(';')?.0.trim().trim_end_matches('u');
Some((
alloc::format!("STATUS_{}", name.trim()),
value.parse().ok()?,
))
})
.collect()
}
}