1use std::collections::BTreeSet;
6
7use serde::Serialize;
8use serde_json::Value;
9
10use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
11use crate::{
12 Collision, Component, Entity, Instance, JsonError, MaterialVariants, Mesh, MeshFile, Name, Source,
13 Transform, World,
14};
15
16#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct Lods {
31 pub detail: u8,
32 pub forms: Vec<LodForm>,
33 pub default: Option<usize>,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
38pub struct LodForm {
39 pub detail: u8,
40 pub form: Entity,
41}
42
43impl Lods {
44 pub fn levels(&self) -> impl Iterator<Item = u8> + '_ {
46 std::iter::once(self.detail).chain(self.forms.iter().map(|f| f.detail))
47 }
48
49 pub fn choose(&self, choice: LodChoice) -> usize {
52 match choice {
53 LodChoice::Finest => 0,
54 LodChoice::Default => self.default.unwrap_or(0),
55 LodChoice::AtMost(n) => {
56 let mut best: Option<(usize, u8)> = None;
57 for (i, d) in self.levels().enumerate() {
58 if d <= n && best.is_none_or(|(_, b)| d > b) {
59 best = Some((i, d));
60 }
61 }
62 best.map_or(0, |(i, _)| i)
63 }
64 }
65 }
66}
67
68impl Component for Lods {
69 const NAME: &'static str = "core.lods";
70 const GEOMETRY: crate::Geometry = crate::Geometry::None;
71 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
73 crate::world::Linearity::Invariant
74 }
75 fn to_json(&self) -> Result<Option<Value>, JsonError> {
76 crate::json::to_value(self).map(Some)
77 }
78 fn references(&self, out: &mut Vec<Entity>) {
79 out.extend(self.forms.iter().map(|f| f.form));
80 }
81 fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
85 if let Some(d) = self.default
86 && d > self.forms.len()
87 {
88 return Err(format!("default form {d} of {}", self.forms.len() + 1));
89 }
90 if !self.forms.is_empty() && !(w.has::<Mesh>(e) || w.has::<MeshFile>(e)) {
91 return Err("other forms of an entity without a mesh".into());
92 }
93 let mut seen = BTreeSet::new();
94 for f in &self.forms {
95 if f.form == e || !w.is_alive(f.form) {
96 return Err(format!("form {:?} is not another alive entity", f.form));
97 }
98 if !seen.insert(f.form) {
99 return Err(format!("form {:?} is listed twice", f.form));
100 }
101 if w.has::<Instance>(f.form) || w.has::<Transform>(f.form) || w.has::<Lods>(f.form) {
102 return Err(format!("form {:?} is placed or has forms of its own", f.form));
103 }
104 }
105 Ok(())
106 }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum LodChoice {
112 Finest,
114 Default,
116 AtMost(u8),
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub struct SelectLod {
133 pub choice: LodChoice,
134}
135
136pub const SELECT_LOD: PassSpec = PassSpec {
138 name: "select-lod",
139 options: &[OptionSpec::required(
140 "form",
141 "finest",
142 "finest, default (the form the source names, else the finest) or a level 0-255: the form each prototype \
143 keeps (LodChoice)",
144 )],
145 make: select_lod,
146};
147
148fn select_lod(g: &Given) -> Result<Box<dyn Pass>, String> {
149 let choice = match g.required("form") {
150 "finest" => LodChoice::Finest,
151 "default" => LodChoice::Default,
152 n => LodChoice::AtMost(
153 n.parse()
154 .map_err(|_| format!("form={n} is not finest, default or a level 0-255"))?,
155 ),
156 };
157 Ok(Box::new(SelectLod { choice }))
158}
159
160impl Pass for SelectLod {
161 fn name(&self) -> &'static str {
162 "select-lod"
163 }
164 fn options(&self) -> Options {
165 let form = match self.choice {
166 LodChoice::Finest => "finest".to_owned(),
167 LodChoice::Default => "default".to_owned(),
168 LodChoice::AtMost(n) => n.to_string(),
169 };
170 Options::from([("form".to_owned(), form)])
171 }
172 fn removes(&self) -> &[&'static str] {
174 &[Lods::NAME, Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
175 }
176 fn modifies(&self) -> &[&'static str] {
177 &[Mesh::NAME, MeshFile::NAME, MaterialVariants::NAME]
178 }
179 fn despawns_entities(&self) -> bool {
180 true
181 }
182 fn removes_by_log(&self) -> bool {
185 true
186 }
187 fn approximates(&self) -> &'static str {
188 match self.choice {
189 LodChoice::Default => "the finest form where the source authored no default",
190 _ => "",
191 }
192 }
193 fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
194 let all: Vec<(Entity, Lods)> = w.iter::<Lods>().map(|(e, l)| (e, l.clone())).collect();
195 let mut forms = BTreeSet::new();
196 for (proto, lods) in &all {
197 lods.verify(*proto, w).map_err(|m| format!("{proto:?}: {m}"))?;
198 for f in &lods.forms {
199 if !forms.insert(f.form) {
200 return Err(format!("form {:?} belongs to more than one prototype", f.form));
201 }
202 }
203 if lods.choose(self.choice) != 0 {
204 w.check_remove_mesh(*proto)?;
205 }
206 }
207 if let Some((name, holder, form)) = w
208 .references_to(&forms)
209 .into_iter()
210 .find(|(name, _, _)| *name != Lods::NAME)
211 {
212 return Err(format!("{name} on {holder:?} refers to form {form:?}"));
213 }
214 for (proto, lods) in all {
215 let chosen = lods.choose(self.choice);
216 if self.choice == LodChoice::Default && lods.default.is_none() {
217 log.approximate("prototypes without an authored default: the finest form", 1);
218 }
219 match chosen.checked_sub(1).map(|k| lods.forms[k]) {
220 None => log.count("prototypes drawn with their own form", 1),
221 Some(f) => {
222 if let Some(own) = w.get::<Mesh>(proto) {
223 let n = own.primitives().len() as u64;
224 log.dropped(
225 crate::ledger::Measure::Primitives,
226 "the prototype's own form left out",
227 n,
228 );
229 }
230 for name in w.remove_mesh(proto)? {
231 log.record_removed(name);
232 }
233 for (left, name) in [
236 (w.remove::<Name>(f.form).is_some(), Name::NAME),
237 (w.remove::<Source>(f.form).is_some(), Source::NAME),
238 (w.remove::<Collision>(f.form).is_some(), Collision::NAME),
239 ] {
240 if left {
241 log.record_removed(name);
242 log.count(
243 &format!(
244 "{name} of the chosen form left behind: the prototype keeps its own"
245 ),
246 1,
247 );
248 }
249 }
250 let moved = w.move_components(f.form, proto)?;
251 debug_assert!(moved, "checked alive above");
252 let draws = w.has::<Mesh>(proto) || w.has::<MeshFile>(proto);
253 log.count(
254 if draws {
255 "prototypes drawn with another form"
256 } else {
257 "prototypes whose chosen form draws nothing"
258 },
259 1,
260 );
261 }
262 }
263 for (k, f) in lods.forms.iter().enumerate() {
264 if k + 1 != chosen {
265 crate::ledger::despawn(w, f.form, log, "forms left out");
266 log.count("forms left out", 1);
267 }
268 }
269 w.remove::<Lods>(proto);
270 }
271 Ok(())
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::pass::{PassManager, PassRegistry, RunReport};
279 use crate::{Opaque, OpaqueData};
280
281 #[derive(Clone, Debug, PartialEq)]
283 struct Tag(String);
284 impl Component for Tag {
285 const NAME: &'static str = "game.tag";
286 const GEOMETRY: crate::Geometry = crate::Geometry::None;
287 }
288
289 fn world() -> (World, Entity, [Entity; 2]) {
293 let mut w = World::new();
294 let proto = w.spawn();
295 w.insert(proto, Mesh::default());
296 w.insert(proto, Name("the prototype".into()));
297 w.insert(proto, Tag("finest".into()));
298 let coarse = w.spawn();
299 w.insert(
300 coarse,
301 MeshFile {
302 path: "p.glb".into(),
303 mesh: 1,
304 materials: vec![],
305 },
306 );
307 w.insert(coarse, Tag("coarse".into()));
308 w.insert_opaque(
309 coarse,
310 "game.form",
311 Opaque {
312 version: 1,
313 data: OpaqueData::Json(serde_json::json!({"lod": 1})),
314 },
315 )
316 .unwrap();
317 let nothing = w.spawn();
318 w.insert(nothing, Tag("nothing".into()));
319 w.insert(
320 proto,
321 Lods {
322 detail: 0,
323 forms: vec![
324 LodForm {
325 detail: 1,
326 form: coarse,
327 },
328 LodForm {
329 detail: 3,
330 form: nothing,
331 },
332 ],
333 default: Some(1),
334 },
335 );
336 (w, proto, [coarse, nothing])
337 }
338
339 fn manager() -> PassManager {
340 let mut r = PassRegistry::new();
341 r.add(&SELECT_LOD);
342 PassManager::new(r)
343 }
344
345 #[test]
346 fn select_lod_refuses_a_dead_form_before_it_changes_anything() {
347 let (mut w, proto, forms) = world();
349 w.despawn(forms[0]);
350 let m = manager();
351 let err = m
352 .run(
353 &mut w,
354 &m.registry.parse("select-lod{form=default}").unwrap(),
355 &mut RunReport::default(),
356 )
357 .unwrap_err();
358 assert!(err.to_string().contains("not another alive entity"), "{err}");
359 assert!(w.has::<Mesh>(proto), "the prototype keeps its geometry");
360 assert!(w.has::<Lods>(proto) && w.is_alive(forms[1]));
361 }
362
363 #[test]
364 fn lods_verify_and_keep_their_forms_in_closures_and_subsets() {
365 let (w, proto, forms) = world();
366 assert_eq!(w.verify_components(), vec![]);
367 let keep = w.closure([proto]);
368 assert_eq!(keep, [proto, forms[0], forms[1]].into());
369 assert_eq!(w.subset(&keep).verify_components(), vec![]);
370 }
371
372 #[test]
375 fn the_prototype_keeps_its_name_and_source_whichever_form_is_drawn() {
376 let (mut w, proto, forms) = world();
377 w.insert(proto, Source("proto".into()));
378 w.insert(forms[0], Name("proto#lod1".into()));
379 w.insert(forms[0], Source("proto/lod1".into()));
380 assert_eq!(w.verify_components(), vec![]);
381 let m = manager().with_pass_checks(true);
382 let mut report = RunReport::default();
383 m.run(
384 &mut w,
385 &m.registry.parse("select-lod{form=default}").unwrap(),
386 &mut report,
387 )
388 .unwrap();
389 assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
390 assert_eq!(w.get::<Name>(proto).unwrap().0, "the prototype");
391 assert_eq!(w.get::<Source>(proto).unwrap().0, "proto");
392 let counts = &report.steps[0].log.counts;
393 assert_eq!(
394 counts["core.name of the chosen form left behind: the prototype keeps its own"],
395 1
396 );
397 assert_eq!(
398 counts["core.source of the chosen form left behind: the prototype keeps its own"],
399 1
400 );
401 }
402
403 #[test]
404 fn choice_follows_the_reference_rule() {
405 let lods = |detail: u8, levels: &[u8], default: Option<usize>| Lods {
406 detail,
407 forms: levels
408 .iter()
409 .enumerate()
410 .map(|(i, &d)| LodForm {
411 detail: d,
412 form: Entity::from_index(10 + i as u32),
413 })
414 .collect(),
415 default,
416 };
417 let l = lods(0, &[1, 1, 3], Some(2));
418 assert_eq!(l.choose(LodChoice::Finest), 0);
419 assert_eq!(l.choose(LodChoice::Default), 2);
420 assert_eq!(lods(0, &[1, 3], None).choose(LodChoice::Default), 0);
422 assert_eq!(l.choose(LodChoice::AtMost(0)), 0);
424 assert_eq!(l.choose(LodChoice::AtMost(2)), 1);
425 assert_eq!(l.choose(LodChoice::AtMost(255)), 3);
426 assert_eq!(lods(2, &[3], None).choose(LodChoice::AtMost(1)), 0);
427 assert_eq!(lods(0, &[3, 1], None).choose(LodChoice::AtMost(2)), 2);
429 }
430
431 #[test]
432 fn default_without_an_authored_default_takes_the_finest_named_and_counted() {
433 let (mut w, proto, forms) = world();
434 w.get_mut::<Lods>(proto).unwrap().default = None;
435 assert_eq!(w.verify_components(), vec![]);
436 let m = manager();
437 let mut report = RunReport::default();
438 m.run(
439 &mut w,
440 &m.registry.parse("select-lod{form=default}").unwrap(),
441 &mut report,
442 )
443 .unwrap();
444 assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
445 assert!(forms.iter().all(|f| !w.is_alive(*f)));
446 let step = &report.steps[0];
447 assert_eq!(
448 step.log.approximations,
449 [(
450 "prototypes without an authored default: the finest form".to_owned(),
451 1
452 )]
453 .into()
454 );
455 assert_eq!(
456 step.approximates,
457 "the finest form where the source authored no default"
458 );
459 let (mut w, _, _) = world();
461 let mut report = RunReport::default();
462 m.run(
463 &mut w,
464 &m.registry.parse("select-lod{form=default}").unwrap(),
465 &mut report,
466 )
467 .unwrap();
468 assert!(report.steps[0].log.approximations.is_empty());
469 assert_eq!(
470 SelectLod {
471 choice: LodChoice::Finest
472 }
473 .approximates(),
474 ""
475 );
476 }
477
478 #[test]
479 fn select_lod_takes_one_form_with_everything_it_holds() {
480 for (pipeline, name, mesh, file, opaque) in [
481 ("select-lod{form=finest}", "finest", true, false, false),
482 ("select-lod{form=default}", "coarse", false, true, true),
483 ("select-lod{form=2}", "coarse", false, true, true),
484 ("select-lod{form=3}", "nothing", false, false, false),
485 ] {
486 let (mut w, proto, forms) = world();
487 let m = manager();
488 let mut report = RunReport::default();
489 m.run(&mut w, &m.registry.parse(pipeline).unwrap(), &mut report)
490 .unwrap();
491 assert_eq!(w.get::<Tag>(proto).unwrap().0, name, "{pipeline}");
492 assert_eq!(
493 w.get::<Name>(proto).unwrap().0,
494 "the prototype",
495 "{pipeline}: the name stays"
496 );
497 assert_eq!(
498 (w.has::<Mesh>(proto), w.has::<MeshFile>(proto)),
499 (mesh, file),
500 "{pipeline}"
501 );
502 assert_eq!(w.get_opaque(proto, "game.form").is_some(), opaque, "{pipeline}");
503 assert!(!w.has::<Lods>(proto));
504 assert!(forms.iter().all(|f| !w.is_alive(*f)), "{pipeline}: a form left");
505 assert_eq!(w.entity_count(), 1);
506 let step = &report.steps[0];
507 let left_out = if name == "finest" { 2 } else { 1 };
508 assert_eq!(step.log.counts["forms left out"], left_out, "{pipeline}");
509 let mut removed = vec!["core.lods"];
511 removed.extend((!mesh).then_some("core.mesh"));
512 removed.extend((!file).then_some("core.mesh_file"));
513 removed.extend((!opaque).then_some("game.form"));
514 assert_eq!(step.removed, removed, "{pipeline}");
515 }
516 assert_eq!(
517 SELECT_LOD
518 .instance(&Options::from([("form".into(), "2".into())]))
519 .unwrap()
520 .options()["form"],
521 "2"
522 );
523 assert!(SELECT_LOD.instance(&Options::new()).is_err());
525 for form in ["finest", "default", "0", "255"] {
526 let given = Options::from([("form".into(), form.into())]);
527 assert_eq!(SELECT_LOD.instance(&given).unwrap().options(), given, "{form}");
528 }
529 assert!(
530 SELECT_LOD
531 .instance(&Options::from([("form".into(), "high".into())]))
532 .is_err()
533 );
534 assert!(
535 SELECT_LOD
536 .instance(&Options::from([("lod".into(), "1".into())]))
537 .is_err()
538 );
539 }
540
541 #[test]
542 fn select_lod_preserves_opaque_shader_inputs() {
543 use crate::{Primitive, Semantic, Stream, Topology};
544 use std::collections::BTreeMap;
545 let (mut w, proto, forms) = world();
546 let raw = vec![[-3.0, 0.5, 9.0, -0.0]; 3];
547 w.insert(
548 forms[0],
549 Mesh::new(vec![Primitive {
550 topology: Topology::Triangles,
551 streams: BTreeMap::from([
552 (Semantic::Position, Stream::F32x3(vec![[0.0; 3]; 3])),
553 (Semantic::ShaderInput(4), Stream::F32x4(raw.clone())),
554 ]),
555 indices: None,
556 material: None,
557 }]),
558 );
559 let m = manager();
560 m.run(
561 &mut w,
562 &m.registry.parse("select-lod{form=default}").unwrap(),
563 &mut RunReport::default(),
564 )
565 .unwrap();
566 let mesh = w.get::<Mesh>(proto).unwrap();
567 let Stream::F32x4(values) = &mesh.primitives()[0].streams[&Semantic::ShaderInput(4)] else {
568 panic!("opaque stream missing")
569 };
570 assert_eq!(
571 values.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>(),
572 raw.iter().map(|v| v.map(f32::to_bits)).collect::<Vec<_>>()
573 );
574 }
575
576 #[derive(Clone, Debug, PartialEq)]
578 struct PerPrimitive(Vec<u8>);
579 impl Component for PerPrimitive {
580 const NAME: &'static str = "game.per_primitive";
581 const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
582 fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
583 Some(crate::remap_list(&self.0, from).map(PerPrimitive))
584 }
585 }
586
587 #[test]
591 fn the_own_forms_per_primitive_data_leaves_with_it() {
592 for loaded in [true, false] {
593 let (mut w, proto, _) = world();
594 if !loaded {
595 w.remove::<Mesh>(proto);
596 w.insert(
597 proto,
598 MeshFile {
599 path: "own.glb".into(),
600 mesh: 0,
601 materials: vec![],
602 },
603 );
604 }
605 w.insert(proto, PerPrimitive(vec![]));
606 w.insert(proto, MaterialVariants(vec![]));
607 let m = manager().with_pass_checks(true);
608 let mut report = RunReport::default();
609 m.run(
610 &mut w,
611 &m.registry.parse("select-lod{form=default}").unwrap(),
612 &mut report,
613 )
614 .unwrap();
615 assert_eq!(w.get::<Tag>(proto).unwrap().0, "coarse");
616 assert!(
617 !w.has::<PerPrimitive>(proto) && !w.has::<MaterialVariants>(proto),
618 "loaded {loaded}: the own form's per-primitive data stayed"
619 );
620 assert_eq!(
621 w.get::<MeshFile>(proto).unwrap().path,
622 std::path::Path::new("p.glb")
623 );
624 let removed = &report.steps[0].removed;
625 for name in [PerPrimitive::NAME, MaterialVariants::NAME] {
626 assert!(removed.iter().any(|n| n == name), "loaded {loaded}: {removed:?}");
627 }
628 }
629 let (mut w, proto, _) = world();
631 w.insert(proto, PerPrimitive(vec![]));
632 let m = manager().with_pass_checks(true);
633 m.run(
634 &mut w,
635 &m.registry.parse("select-lod{form=finest}").unwrap(),
636 &mut RunReport::default(),
637 )
638 .unwrap();
639 assert!(w.has::<PerPrimitive>(proto) && w.has::<Mesh>(proto));
640 }
641
642 #[test]
645 fn an_opaque_component_on_a_prototype_drawn_with_another_form_is_refused() {
646 let opaque = || Opaque {
647 version: 1,
648 data: OpaqueData::Bytes(vec![1]),
649 };
650 let (mut w, proto, forms) = world();
651 w.insert_opaque(proto, "game.thing", opaque()).unwrap();
652 let m = manager().with_pass_checks(true);
653 let err = m
654 .run(
655 &mut w,
656 &m.registry.parse("select-lod{form=default}").unwrap(),
657 &mut RunReport::default(),
658 )
659 .unwrap_err();
660 assert!(err.to_string().contains("game.thing"), "{err}");
661 assert!(w.has::<Mesh>(proto) && w.has::<Lods>(proto));
662 assert!(forms.iter().all(|f| w.is_alive(*f)));
663 assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
664 m.run(
665 &mut w,
666 &m.registry.parse("select-lod{form=finest}").unwrap(),
667 &mut RunReport::default(),
668 )
669 .unwrap();
670 assert!(w.get_opaque(proto, "game.thing").is_some() && w.has::<Mesh>(proto));
671 }
672
673 #[test]
674 fn an_invalid_selection_is_refused_without_mutation() {
675 let (mut w, proto, forms) = world();
677 let second = w.spawn();
678 w.insert(second, Mesh::default());
679 w.insert(
680 second,
681 Lods {
682 detail: 0,
683 forms: vec![],
684 default: Some(99),
685 },
686 );
687 let err = SelectLod {
688 choice: LodChoice::Default,
689 }
690 .run(&mut w, &mut PassLog::default())
691 .unwrap_err();
692 assert!(err.contains("default form 99 of 1"), "{err}");
693 assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
694 assert!(forms.iter().all(|e| w.is_alive(*e)) && w.has::<Lods>(second));
695 w.insert(second, w.get::<Lods>(proto).unwrap().clone());
697 let err = SelectLod {
698 choice: LodChoice::Default,
699 }
700 .run(&mut w, &mut PassLog::default())
701 .unwrap_err();
702 assert!(err.contains("more than one prototype"), "{err}");
703 w.remove::<Lods>(second);
705 let placed = w.spawn();
706 w.insert(
707 placed,
708 Instance {
709 prototype: forms[0],
710 variant: 0,
711 },
712 );
713 let err = SelectLod {
714 choice: LodChoice::Default,
715 }
716 .run(&mut w, &mut PassLog::default())
717 .unwrap_err();
718 assert!(
719 err.contains("core.instance") && err.contains("refers to form"),
720 "{err}"
721 );
722 assert!(forms.iter().all(|e| w.is_alive(*e)));
723 assert_eq!(w.get::<Tag>(proto).unwrap().0, "finest");
724 }
725
726 #[test]
727 fn malformed_lods_fail_verification() {
728 let failure = |edit: &dyn Fn(&mut World, Entity, [Entity; 2])| {
729 let (mut w, proto, forms) = world();
730 edit(&mut w, proto, forms);
731 w.verify_components()
732 .into_iter()
733 .map(|(_, _, m)| m)
734 .collect::<Vec<_>>()
735 };
736 let twice = failure(&|w, proto, _| {
737 let lods = w.get_mut::<Lods>(proto).unwrap();
738 lods.forms[1] = lods.forms[0];
739 });
740 assert!(twice[0].contains("listed twice"), "{twice:?}");
741 let placed = failure(&|w, _, forms| {
742 w.insert(forms[0], Transform::IDENTITY);
743 });
744 assert!(placed[0].contains("placed"), "{placed:?}");
745 let meshless = failure(&|w, proto, _| {
746 w.remove::<Mesh>(proto);
747 });
748 assert!(meshless[0].contains("without a mesh"), "{meshless:?}");
749 let gone = failure(&|w, _, forms| {
750 w.despawn(forms[1]);
751 });
752 assert!(gone[0].contains("not another alive entity"), "{gone:?}");
753 }
754}