use std::collections::BTreeSet;
use serde::Serialize;
use serde_json::Value;
use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
use crate::{
Collision, Component, Entity, Instance, JsonError, MaterialVariants, Mesh, MeshFile, Name, Source,
Transform, World,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Lods {
pub detail: u8,
pub forms: Vec<LodForm>,
pub default: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct LodForm {
pub detail: u8,
pub form: Entity,
}
impl Lods {
pub fn levels(&self) -> impl Iterator<Item = u8> + '_ {
std::iter::once(self.detail).chain(self.forms.iter().map(|f| f.detail))
}
pub fn choose(&self, choice: LodChoice) -> usize {
match choice {
LodChoice::Finest => 0,
LodChoice::Default => self.default.unwrap_or(0),
LodChoice::AtMost(n) => {
let mut best: Option<(usize, u8)> = None;
for (i, d) in self.levels().enumerate() {
if d <= n && best.is_none_or(|(_, b)| d > b) {
best = Some((i, d));
}
}
best.map_or(0, |(i, _)| i)
}
}
}
}
impl Component for Lods {
const NAME: &'static str = "core.lods";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.forms.iter().map(|f| f.form));
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
if let Some(d) = self.default
&& d > self.forms.len()
{
return Err(format!("default form {d} of {}", self.forms.len() + 1));
}
if !self.forms.is_empty() && !(w.has::<Mesh>(e) || w.has::<MeshFile>(e)) {
return Err("other forms of an entity without a mesh".into());
}
let mut seen = BTreeSet::new();
for f in &self.forms {
if f.form == e || !w.is_alive(f.form) {
return Err(format!("form {:?} is not another alive entity", f.form));
}
if !seen.insert(f.form) {
return Err(format!("form {:?} is listed twice", f.form));
}
if w.has::<Instance>(f.form) || w.has::<Transform>(f.form) || w.has::<Lods>(f.form) {
return Err(format!("form {:?} is placed or has forms of its own", f.form));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LodChoice {
Finest,
Default,
AtMost(u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SelectLod {
pub choice: LodChoice,
}
pub const SELECT_LOD: PassSpec = PassSpec {
name: "select-lod",
options: &[OptionSpec::required(
"form",
"finest",
"finest, default (the form the source names, else the finest) or a level 0-255: the form each prototype \
keeps (LodChoice)",
)],
make: select_lod,
};
fn select_lod(g: &Given) -> Result<Box<dyn Pass>, String> {
let choice = match g.required("form") {
"finest" => LodChoice::Finest,
"default" => LodChoice::Default,
n => LodChoice::AtMost(
n.parse()
.map_err(|_| format!("form={n} is not finest, default or a level 0-255"))?,
),
};
Ok(Box::new(SelectLod { choice }))
}
impl Pass for SelectLod {
fn name(&self) -> &'static str {
"select-lod"
}
fn options(&self) -> Options {
let form = match self.choice {
LodChoice::Finest => "finest".to_owned(),
LodChoice::Default => "default".to_owned(),
LodChoice::AtMost(n) => n.to_string(),
};
Options::from([("form".to_owned(), form)])
}
fn removes(&self) -> &[&'static str] {
&[Lods::NAME, Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
}
fn modifies(&self) -> &[&'static str] {
&[Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
}
fn despawns_entities(&self) -> bool {
true
}
fn removes_by_log(&self) -> bool {
true
}
fn approximates(&self) -> &'static str {
match self.choice {
LodChoice::Default => "the finest form where the source authored no default",
_ => "",
}
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let all: Vec<(Entity, Lods)> = w.iter::<Lods>().map(|(e, l)| (e, l.clone())).collect();
let mut forms = BTreeSet::new();
for (proto, lods) in &all {
lods.verify(*proto, w).map_err(|m| format!("{proto:?}: {m}"))?;
for f in &lods.forms {
if !forms.insert(f.form) {
return Err(format!("form {:?} belongs to more than one prototype", f.form));
}
}
if lods.choose(self.choice) != 0 {
w.check_remove_mesh(*proto)?;
}
}
if let Some((name, holder, form)) = w
.references_to(&forms)
.into_iter()
.find(|(name, _, _)| *name != Lods::NAME)
{
return Err(format!("{name} on {holder:?} refers to form {form:?}"));
}
for (proto, lods) in all {
let chosen = lods.choose(self.choice);
if self.choice == LodChoice::Default && lods.default.is_none() {
log.approximate("prototypes without an authored default: the finest form", 1);
}
match chosen.checked_sub(1).map(|k| lods.forms[k]) {
None => log.count("prototypes drawn with their own form", 1),
Some(f) => {
if let Some(own) = w.get::<Mesh>(proto) {
let n = own.primitives().len() as u64;
log.dropped(
crate::ledger::Measure::Primitives,
"the prototype's own form left out",
n,
);
}
for name in w.remove_mesh(proto)? {
log.record_removed(name);
}
for (left, name) in [
(w.remove::<Name>(f.form).is_some(), Name::NAME),
(w.remove::<Source>(f.form).is_some(), Source::NAME),
(w.remove::<Collision>(f.form).is_some(), Collision::NAME),
] {
if left {
log.record_removed(name);
log.count(
&format!(
"{name} of the chosen form left behind: the prototype keeps its own"
),
1,
);
}
}
let moved = w.move_components(f.form, proto)?;
debug_assert!(moved, "checked alive above");
let draws = w.has::<Mesh>(proto) || w.has::<MeshFile>(proto);
log.count(
if draws {
"prototypes drawn with another form"
} else {
"prototypes whose chosen form draws nothing"
},
1,
);
}
}
for (k, f) in lods.forms.iter().enumerate() {
if k + 1 != chosen {
crate::ledger::despawn(w, f.form, log, "forms left out");
log.count("forms left out", 1);
}
}
w.remove::<Lods>(proto);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pass::{PassManager, PassRegistry, RunReport};
use crate::{Opaque, OpaqueData};
#[derive(Clone, Debug, PartialEq)]
struct Tag(String);
impl Component for Tag {
const NAME: &'static str = "game.tag";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
}
fn world() -> (World, Entity, [Entity; 2]) {
let mut w = World::new();
let proto = w.spawn();
w.insert(proto, Mesh::default());
w.insert(proto, Name("the prototype".into()));
w.insert(proto, Tag("finest".into()));
let coarse = w.spawn();
w.insert(
coarse,
MeshFile {
path: "p.glb".into(),
mesh: 1,
materials: vec![],
},
);
w.insert(coarse, Tag("coarse".into()));
w.insert_opaque(
coarse,
"game.form",
Opaque {
version: 1,
data: OpaqueData::Json(serde_json::json!({"lod": 1})),
},
)
.unwrap();
let nothing = w.spawn();
w.insert(nothing, Tag("nothing".into()));
w.insert(
proto,
Lods {
detail: 0,
forms: vec![
LodForm {
detail: 1,
form: coarse,
},
LodForm {
detail: 3,
form: nothing,
},
],
default: Some(1),
},
);
(w, proto, [coarse, nothing])
}
fn manager() -> PassManager {
let mut r = PassRegistry::new();
r.add(&SELECT_LOD);
PassManager::new(r)
}
#[test]
fn select_lod_refuses_a_dead_form_before_it_changes_anything() {
let (mut w, proto, forms) = world();
w.despawn(forms[0]);
let m = manager();
let err = m
.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut RunReport::default(),
)
.unwrap_err();
assert!(err.to_string().contains("not another alive entity"), "{err}");
assert!(w.has::<Mesh>(proto), "the prototype keeps its geometry");
assert!(w.has::<Lods>(proto) && w.is_alive(forms[1]));
}
#[test]
fn lods_verify_and_keep_their_forms_in_closures_and_subsets() {
let (w, proto, forms) = world();
assert_eq!(w.verify_components(), vec![]);
let keep = w.closure([proto]);
assert_eq!(keep, [proto, forms[0], forms[1]].into());
assert_eq!(w.subset(&keep).verify_components(), vec![]);
}
#[test]
fn the_prototype_keeps_its_name_and_source_whichever_form_is_drawn() {
let (mut w, proto, forms) = world();
w.insert(proto, Source("proto".into()));
w.insert(forms[0], Name("proto#lod1".into()));
w.insert(forms[0], Source("proto/lod1".into()));
assert_eq!(w.verify_components(), vec![]);
let m = manager().with_pass_checks(true);
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut report,
)
.unwrap();
assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
assert_eq!(w.get::<Name>(proto).unwrap().0, "the prototype");
assert_eq!(w.get::<Source>(proto).unwrap().0, "proto");
let counts = &report.steps[0].log.counts;
assert_eq!(
counts["core.name of the chosen form left behind: the prototype keeps its own"],
1
);
assert_eq!(
counts["core.source of the chosen form left behind: the prototype keeps its own"],
1
);
}
#[test]
fn choice_follows_the_reference_rule() {
let lods = |detail: u8, levels: &[u8], default: Option<usize>| Lods {
detail,
forms: levels
.iter()
.enumerate()
.map(|(i, &d)| LodForm {
detail: d,
form: Entity::from_index(10 + i as u32),
})
.collect(),
default,
};
let l = lods(0, &[1, 1, 3], Some(2));
assert_eq!(l.choose(LodChoice::Finest), 0);
assert_eq!(l.choose(LodChoice::Default), 2);
assert_eq!(lods(0, &[1, 3], None).choose(LodChoice::Default), 0);
assert_eq!(l.choose(LodChoice::AtMost(0)), 0);
assert_eq!(l.choose(LodChoice::AtMost(2)), 1);
assert_eq!(l.choose(LodChoice::AtMost(255)), 3);
assert_eq!(lods(2, &[3], None).choose(LodChoice::AtMost(1)), 0);
assert_eq!(lods(0, &[3, 1], None).choose(LodChoice::AtMost(2)), 2);
}
#[test]
fn default_without_an_authored_default_takes_the_finest_named_and_counted() {
let (mut w, proto, forms) = world();
w.get_mut::<Lods>(proto).unwrap().default = None;
assert_eq!(w.verify_components(), vec![]);
let m = manager();
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut report,
)
.unwrap();
assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
assert!(forms.iter().all(|f| !w.is_alive(*f)));
let step = &report.steps[0];
assert_eq!(
step.log.approximations,
[(
"prototypes without an authored default: the finest form".to_owned(),
1
)]
.into()
);
assert_eq!(
step.approximates,
"the finest form where the source authored no default"
);
let (mut w, _, _) = world();
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut report,
)
.unwrap();
assert!(report.steps[0].log.approximations.is_empty());
assert_eq!(
SelectLod {
choice: LodChoice::Finest
}
.approximates(),
""
);
}
#[test]
fn select_lod_takes_one_form_with_everything_it_holds() {
for (pipeline, name, mesh, file, opaque) in [
("select-lod{form=finest}", "finest", true, false, false),
("select-lod{form=default}", "coarse", false, true, true),
("select-lod{form=2}", "coarse", false, true, true),
("select-lod{form=3}", "nothing", false, false, false),
] {
let (mut w, proto, forms) = world();
let m = manager();
let mut report = RunReport::default();
m.run(&mut w, &m.registry.parse(pipeline).unwrap(), &mut report)
.unwrap();
assert_eq!(w.get::<Tag>(proto).unwrap().0, name, "{pipeline}");
assert_eq!(
w.get::<Name>(proto).unwrap().0,
"the prototype",
"{pipeline}: the name stays"
);
assert_eq!(
(w.has::<Mesh>(proto), w.has::<MeshFile>(proto)),
(mesh, file),
"{pipeline}"
);
assert_eq!(w.get_opaque(proto, "game.form").is_some(), opaque, "{pipeline}");
assert!(!w.has::<Lods>(proto));
assert!(forms.iter().all(|f| !w.is_alive(*f)), "{pipeline}: a form left");
assert_eq!(w.entity_count(), 1);
let step = &report.steps[0];
let left_out = if name == "finest" { 2 } else { 1 };
assert_eq!(step.log.counts["forms left out"], left_out, "{pipeline}");
let mut removed = vec!["core.lods"];
removed.extend((!mesh).then_some("core.mesh"));
removed.extend((!file).then_some("core.mesh_file"));
removed.extend((!opaque).then_some("game.form"));
assert_eq!(step.removed, removed, "{pipeline}");
}
assert_eq!(
SELECT_LOD
.instance(&Options::from([("form".into(), "2".into())]))
.unwrap()
.options()["form"],
"2"
);
assert!(SELECT_LOD.instance(&Options::new()).is_err());
for form in ["finest", "default", "0", "255"] {
let given = Options::from([("form".into(), form.into())]);
assert_eq!(SELECT_LOD.instance(&given).unwrap().options(), given, "{form}");
}
assert!(
SELECT_LOD
.instance(&Options::from([("form".into(), "high".into())]))
.is_err()
);
assert!(
SELECT_LOD
.instance(&Options::from([("lod".into(), "1".into())]))
.is_err()
);
}
#[test]
fn select_lod_preserves_opaque_shader_inputs() {
use crate::{Primitive, Semantic, Stream, Topology};
use std::collections::BTreeMap;
let (mut w, proto, forms) = world();
let raw = vec![[-3.0, 0.5, 9.0, -0.0]; 3];
w.insert(
forms[0],
Mesh::new(vec![Primitive {
topology: Topology::Triangles,
streams: BTreeMap::from([
(Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
(Semantic::ShaderInput(4), Stream::F32x4(raw.clone())),
]),
indices: None,
material: None,
}]),
);
let m = manager();
m.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut RunReport::default(),
)
.unwrap();
let mesh = w.get::<Mesh>(proto).unwrap();
let Stream::F32x4(values) = &mesh.primitives()[0].streams[&Semantic::ShaderInput(4)] else {
panic!("opaque stream missing")
};
assert_eq!(
values.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>(),
raw.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>()
);
}
#[derive(Clone, Debug, PartialEq)]
struct PerPrimitive(Vec<u8>);
impl Component for PerPrimitive {
const NAME: &'static str = "game.per_primitive";
const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
Some(crate::remap_list(&self.0, from).map(PerPrimitive))
}
}
#[test]
fn the_own_forms_per_primitive_data_leaves_with_it() {
for loaded in [true, false] {
let (mut w, proto, _) = world();
if !loaded {
w.remove::<Mesh>(proto);
w.insert(
proto,
MeshFile {
path: "own.glb".into(),
mesh: 0,
materials: vec![],
},
);
}
w.insert(proto, PerPrimitive(vec![]));
w.insert(proto, MaterialVariants(vec![]));
let m = manager().with_pass_checks(true);
let mut report = RunReport::default();
m.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut report,
)
.unwrap();
assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
assert!(
!w.has::<PerPrimitive>(proto) && !w.has::<MaterialVariants>(proto),
"loaded {loaded}: the own form's per-primitive data stayed"
);
assert_eq!(
w.get::<MeshFile>(proto).unwrap().path,
std::path::Path::new("p.glb")
);
let removed = &report.steps[0].removed;
for name in [PerPrimitive::NAME, MaterialVariants::NAME] {
assert!(removed.iter().any(|n| n == name), "loaded {loaded}: {removed:?}");
}
}
let (mut w, proto, _) = world();
w.insert(proto, PerPrimitive(vec![]));
let m = manager().with_pass_checks(true);
m.run(
&mut w,
&m.registry.parse("select-lod{form=finest}").unwrap(),
&mut RunReport::default(),
)
.unwrap();
assert!(w.has::<PerPrimitive>(proto) && w.has::<Mesh>(proto));
}
#[test]
fn an_opaque_component_on_a_prototype_drawn_with_another_form_is_refused() {
let opaque = || Opaque {
version: 1,
data: OpaqueData::Bytes(vec![1]),
};
let (mut w, proto, forms) = world();
w.insert_opaque(proto, "game.thing", opaque()).unwrap();
let m = manager().with_pass_checks(true);
let err = m
.run(
&mut w,
&m.registry.parse("select-lod{form=default}").unwrap(),
&mut RunReport::default(),
)
.unwrap_err();
assert!(err.to_string().contains("game.thing"), "{err}");
assert!(w.has::<Mesh>(proto) && w.has::<Lods>(proto));
assert!(forms.iter().all(|f| w.is_alive(*f)));
assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
m.run(
&mut w,
&m.registry.parse("select-lod{form=finest}").unwrap(),
&mut RunReport::default(),
)
.unwrap();
assert!(w.get_opaque(proto, "game.thing").is_some() && w.has::<Mesh>(proto));
}
#[test]
fn an_invalid_selection_is_refused_without_mutation() {
let (mut w, proto, forms) = world();
let second = w.spawn();
w.insert(second, Mesh::default());
w.insert(
second,
Lods {
detail: 0,
forms: vec![],
default: Some(99),
},
);
let err = SelectLod {
choice: LodChoice::Default,
}
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("default form 99 of 1"), "{err}");
assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
assert!(forms.iter().all(|e| w.is_alive(*e)) && w.has::<Lods>(second));
w.insert(second, w.get::<Lods>(proto).unwrap().clone());
let err = SelectLod {
choice: LodChoice::Default,
}
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("more than one prototype"), "{err}");
w.remove::<Lods>(second);
let placed = w.spawn();
w.insert(
placed,
Instance {
prototype: forms[0],
variant: 0,
},
);
let err = SelectLod {
choice: LodChoice::Default,
}
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(
err.contains("core.instance") && err.contains("refers to form"),
"{err}"
);
assert!(forms.iter().all(|e| w.is_alive(*e)));
assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
}
#[test]
fn malformed_lods_fail_verification() {
let failure = |edit: &dyn Fn(&mut World, Entity, [Entity; 2])| {
let (mut w, proto, forms) = world();
edit(&mut w, proto, forms);
w.verify_components()
.into_iter()
.map(|(_, _, m)| m)
.collect::<Vec<_>>()
};
let twice = failure(&|w, proto, _| {
let lods = w.get_mut::<Lods>(proto).unwrap();
lods.forms[1] = lods.forms[0];
});
assert!(twice[0].contains("listed twice"), "{twice:?}");
let placed = failure(&|w, _, forms| {
w.insert(forms[0], Transform::IDENTITY);
});
assert!(placed[0].contains("placed"), "{placed:?}");
let meshless = failure(&|w, proto, _| {
w.remove::<Mesh>(proto);
});
assert!(meshless[0].contains("without a mesh"), "{meshless:?}");
let gone = failure(&|w, _, forms| {
w.despawn(forms[1]);
});
assert!(gone[0].contains("not another alive entity"), "{gone:?}");
}
}