use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub mod memo;
pub mod stages;
pub use crate::ledger::{Conservation, Gap, Measure};
pub use crate::pass_options::{Choice, Given, OptionSpec, PassSpec, TextForm};
use crate::world::{Component, Entity, Resource, World};
pub use stages::Stages;
pub type Options = BTreeMap<String, String>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PassLog {
pub counts: BTreeMap<String, u64>,
pub approximations: BTreeMap<String, u64>,
pub approximated_on: BTreeMap<String, BTreeSet<Entity>>,
pub omitted: BTreeMap<String, u64>,
pub removed: BTreeSet<String>,
pub placed: BTreeSet<Entity>,
pub placed_from: BTreeMap<Entity, Entity>,
pub drops: BTreeMap<(Measure, String), u64>,
pub details: BTreeMap<String, Vec<serde_json::Value>>,
}
impl PassLog {
pub fn count(&mut self, what: &str, n: u64) {
*self.counts.entry(what.to_owned()).or_default() += n;
}
pub fn detail(&mut self, label: &str, value: serde_json::Value) {
self.details.entry(label.to_owned()).or_default().push(value);
}
pub fn record_removed(&mut self, name: &str) {
self.removed.insert(name.to_owned());
}
pub fn approximate(&mut self, what: &str, n: u64) {
*self.approximations.entry(what.to_owned()).or_default() += n;
}
pub fn approximate_on(&mut self, e: Entity, what: &str, n: u64) {
self.approximate(what, n);
self.approximated_on.entry(what.to_owned()).or_default().insert(e);
}
pub fn omit(&mut self, what: &str, n: u64) {
*self.omitted.entry(what.to_owned()).or_default() += n;
}
pub fn dropped(&mut self, measure: Measure, why: &str, n: u64) {
if n > 0 {
*self.drops.entry((measure, why.to_owned())).or_default() += n;
}
}
pub fn declared(&self, measure: Measure) -> u64 {
self.drops
.iter()
.filter(|((m, _), _)| *m == measure)
.map(|(_, n)| n)
.sum()
}
pub fn place(&mut self, e: Entity) {
self.placed.insert(e);
}
pub fn place_from(&mut self, e: Entity, source: Entity) {
self.placed.insert(e);
self.placed_from.insert(e, source);
}
pub fn absorb(&mut self, other: PassLog) {
let PassLog {
counts,
approximations,
approximated_on,
omitted,
removed,
placed,
placed_from,
drops,
details,
} = other;
for ((m, why), n) in drops {
self.dropped(m, &why, n);
}
for (k, n) in counts {
self.count(&k, n);
}
for (k, n) in approximations {
self.approximate(&k, n);
}
for (k, entities) in approximated_on {
self.approximated_on.entry(k).or_default().extend(entities);
}
for (k, n) in omitted {
self.omit(&k, n);
}
self.removed.extend(removed);
self.placed.extend(placed);
self.placed_from.extend(placed_from);
for (label, entries) in details {
self.details.entry(label).or_default().extend(entries);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Settings {
pub threads: primase_foundations::parallel::Threads,
}
#[derive(Clone, Copy, Debug)]
pub struct Setting {
pub name: &'static str,
text: fn(&World) -> Option<String>,
}
impl Setting {
pub const fn of<T: RunSetting>() -> Self {
Setting {
name: T::NAME,
text: setting_text::<T>,
}
}
pub fn text(&self, w: &World) -> Option<String> {
(self.text)(w)
}
}
pub trait RunSetting: Resource {
fn text(&self) -> String;
}
fn setting_text<T: RunSetting>(w: &World) -> Option<String> {
w.resource::<T>().map(RunSetting::text)
}
fn count_dropped(log: &mut PassLog, d: &crate::Dropped) {
let n = |x: usize| u64::try_from(x).unwrap_or(u64::MAX);
for (what, count) in [
(
"render graph steps dropped: they drew with an entity that left the world",
n(d.graph.steps.len()),
),
(
"render graph lighting pipelines dropped: they selected a dropped step",
u64::from(d.graph.lighting),
),
(
"render graph pictures dropped: a dropped step wrote them",
u64::from(d.graph.picture),
),
(
"step bindings dropped: only dropped render graph steps made their draws",
n(d.step_bindings.len()),
),
(
"step draws dropped: only dropped render graph steps made their draws",
n(d.step_draws.len()),
),
] {
if count > 0 {
log.count(what, count);
}
}
}
fn dropped_from(d: &crate::Dropped, name: &str) -> BTreeSet<Entity> {
let list = if name == <crate::StepBindings as crate::Component>::NAME {
&d.step_bindings
} else if name == <crate::StepDraws as crate::Component>::NAME {
&d.step_draws
} else {
return BTreeSet::new();
};
list.iter().map(|(e, _)| *e).collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("`{name}`, which the world does not have: the program states it for the run")]
pub struct Unstated {
pub name: &'static str,
}
pub trait Pass: Send + Sync {
fn name(&self) -> &'static str;
fn options(&self) -> Options {
Options::new()
}
fn may_lack_inputs(&self) -> bool {
false
}
fn requires(&self) -> &[&'static str] {
&[]
}
fn settings(&self) -> &[Setting] {
&[]
}
fn provides(&self) -> &[&'static str] {
&[]
}
fn removes(&self) -> &[&'static str] {
&[]
}
fn modifies(&self) -> &[&'static str] {
&[]
}
fn approximates(&self) -> &'static str {
""
}
fn despawns_entities(&self) -> bool {
false
}
fn removes_by_log(&self) -> bool {
false
}
fn settles(&self) -> &[&'static str] {
&[]
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String>;
}
#[derive(Default)]
pub struct PassRegistry {
specs: BTreeMap<&'static str, &'static PassSpec>,
}
impl fmt::Debug for PassRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self.specs.keys()).finish()
}
}
impl PassRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, spec: &'static PassSpec) {
let name = spec.name;
spec.validate();
let example = spec.example();
let made = spec
.instance(&example)
.unwrap_or_else(|e| panic!("pass {name} with its example options {example:?}: {e}"));
assert_eq!(
made.name(),
name,
"factory registered as {name} makes {}",
made.name()
);
let printed = made.options();
if let Err(e) = spec.check(&printed) {
panic!("pass {name} prints {printed:?}, which its declaration refuses: {e}");
}
let again = spec
.instance(&printed)
.unwrap_or_else(|e| panic!("pass {name}: what it prints, {printed:?}, does not make it: {e}"));
assert_eq!(
again.options(),
printed,
"pass {name}: made from what it prints, it prints otherwise"
);
assert!(
self.specs.insert(name, spec).is_none(),
"pass {name} registered twice"
);
}
pub fn describe(&self, name: &str) -> Option<String> {
self.specs.get(name).map(|spec| spec.describe())
}
pub fn requires_options(&self, name: &str) -> bool {
self.specs.get(name).is_some_and(|spec| spec.requires_options())
}
pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.specs.keys().copied()
}
pub fn make_registered(&self, name: &str) -> Result<Box<dyn Pass>, Error> {
let spec = self
.specs
.get(name)
.ok_or_else(|| Error::UnknownPass(name.to_owned()))?;
self.make(name, &spec.example())
}
pub fn make(&self, name: &str, options: &Options) -> Result<Box<dyn Pass>, Error> {
let spec = self
.specs
.get(name)
.ok_or_else(|| Error::UnknownPass(name.to_owned()))?;
spec.instance(options).map_err(|message| Error::Options {
pass: name.to_owned(),
message,
})
}
pub fn parse(&self, text: &str) -> Result<Pipeline, Error> {
let mut passes = Vec::new();
for (name, options) in parse_pipeline(text)? {
passes.push(self.make(&name, &options)?);
}
Ok(Pipeline { passes })
}
pub fn prepare_texts(&self, present: &BTreeSet<String>, texts: Vec<String>) -> TextPreparation {
let mut held = present.clone();
let mut runnable = Vec::new();
let mut unfed = Vec::new();
for text in &texts {
let Ok(pipeline) = self.parse(text) else {
runnable.push(text.clone());
continue;
};
let inputs = pipeline.unfed(&held);
if inputs.passes.is_empty() {
held = inputs.held;
runnable.push(text.clone());
} else {
let mut missing = Vec::new();
for name in inputs.passes.iter().flat_map(|(_, names)| names) {
if !missing.iter().any(|held| held == name) {
missing.push((*name).to_owned());
}
}
unfed.push((text.clone(), missing));
}
}
TextPreparation {
requested: texts,
runnable,
unfed,
}
}
}
fn is_word(s: &str) -> bool {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
}
pub(crate) fn parse_pipeline(text: &str) -> Result<Vec<(String, Options)>, Error> {
let bad = |m: String| Error::Parse(format!("{m} in pipeline {text:?}"));
let mut out = Vec::new();
let mut rest = text.trim();
if rest.is_empty() {
return Ok(out);
}
loop {
let end = rest.find([',', '{']).unwrap_or(rest.len());
let name = rest[..end].trim();
if !is_word(name) {
return Err(bad(format!("{name:?} is not a pass name")));
}
rest = &rest[end..];
let mut options = Options::new();
if let Some(r) = rest.strip_prefix('{') {
let close = r
.find('}')
.ok_or_else(|| bad(format!("{name}: no closing brace")))?;
let mut last: Option<String> = None;
for item in r[..close].split(',').filter(|s| !s.trim().is_empty()) {
let Some((k, v)) = item.split_once('=') else {
let value = last
.as_ref()
.and_then(|k| options.get_mut(k))
.ok_or_else(|| bad(format!("{name}: option {item:?} is not key=value")))?;
value.push(',');
value.push_str(item.trim());
continue;
};
let (k, v) = (k.trim(), v.trim());
if !is_word(k) || v.contains('{') {
return Err(bad(format!("{name}: bad option {item:?}")));
}
if options.insert(k.to_owned(), v.to_owned()).is_some() {
return Err(bad(format!("{name}: option {k} twice")));
}
last = Some(k.to_owned());
}
rest = r[close + 1..].trim_start();
}
out.push((name.to_owned(), options));
if rest.is_empty() {
return Ok(out);
}
rest = rest
.strip_prefix(',')
.ok_or_else(|| bad(format!("unexpected {rest:?}")))?
.trim_start();
}
}
#[derive(Default)]
pub struct Pipeline {
pub passes: Vec<Box<dyn Pass>>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TextPreparation {
pub requested: Vec<String>,
pub runnable: Vec<String>,
pub unfed: Vec<(String, Vec<String>)>,
}
impl fmt::Debug for Pipeline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Pipeline({self})")
}
}
impl Pipeline {
pub fn check(&self) -> Result<()> {
check_settled(&self.passes)
}
pub fn unfed(&self, present: &BTreeSet<String>) -> Unfed {
let mut held = present.clone();
let mut passes = Vec::new();
for p in &self.passes {
let missing: Vec<&'static str> = p
.requires()
.iter()
.copied()
.filter(|&r| !held.contains(r))
.collect();
if p.may_lack_inputs() && !p.requires().is_empty() && missing.len() == p.requires().len() {
continue;
}
if missing.is_empty() {
held.extend(p.provides().iter().map(|&n| n.to_owned()));
} else {
passes.push((pass_text(p.as_ref()), missing));
}
}
Unfed { passes, held }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unfed {
pub passes: Vec<(String, Vec<&'static str>)>,
pub held: BTreeSet<String>,
}
impl fmt::Display for Pipeline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, p) in self.passes.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(&pass_text(p.as_ref()))?;
}
Ok(())
}
}
pub fn carries(value: &str) -> bool {
parse_pipeline(&format!("p{{k={value}}}"))
.ok()
.and_then(|mut passes| passes.pop())
.and_then(|(_, options)| options.get("k").cloned())
.is_some_and(|v| v == value)
}
pub fn pass_text(p: &dyn Pass) -> String {
stated(p.name(), &p.options())
}
pub fn text_form<T: TextForm>(value: &T) -> String {
stated(T::NAME, &value.options())
}
fn stated(name: &str, options: &Options) -> String {
if options.is_empty() {
return name.to_owned();
}
let items: Vec<String> = options.iter().map(|(k, v)| format!("{k}={v}")).collect();
format!("{name}{{{}}}", items.join(","))
}
pub fn parse_text_form<T: TextForm>(text: &str) -> Result<T, Error> {
let mut items = parse_pipeline(text)?;
let (name, options) = match (items.pop(), items.is_empty()) {
(Some(item), true) => item,
_ => return Err(Error::Parse(format!("{text:?} is not one {}", T::NAME))),
};
if name != T::NAME {
return Err(Error::Parse(format!("{name} is not {}", T::NAME)));
}
let refused = |message| Error::Stated {
name: T::NAME,
message,
};
crate::pass_options::check_options(T::NAME, T::OPTIONS, &options).map_err(refused)?;
T::make(&Given::new(T::NAME, T::OPTIONS, &options)).map_err(refused)
}
pub fn validate_text_form<T: TextForm>() {
let name = T::NAME;
assert!(is_word(name), "{name:?} is not a name a text can start with");
crate::pass_options::validate_options(name, T::OPTIONS);
let example = PassSpec {
name,
options: T::OPTIONS,
make: |_| unreachable!("the example is only read"),
}
.example();
let made = T::make(&Given::new(name, T::OPTIONS, &example))
.unwrap_or_else(|e| panic!("{name} with its example options {example:?}: {e}"));
let printed = made.options();
if let Err(e) = crate::pass_options::check_options(name, T::OPTIONS, &printed) {
panic!("{name} prints {printed:?}, which its declaration refuses: {e}");
}
let again = T::make(&Given::new(name, T::OPTIONS, &printed))
.unwrap_or_else(|e| panic!("{name}: what it prints, {printed:?}, does not make it: {e}"));
assert_eq!(
again.options(),
printed,
"{name}: made from what it prints, it prints otherwise"
);
}
pub type LegalityCheck = Arc<dyn Fn(&World, Entity) -> bool + Send + Sync>;
#[derive(Clone)]
pub enum Legality {
Legal,
Dynamic(LegalityCheck),
Covered(fn(&World, Entity) -> bool),
Illegal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Rule {
Legal,
Dynamic,
Covered,
Illegal,
}
impl fmt::Debug for Legality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Legality::Legal => "Legal",
Legality::Dynamic(_) => "Dynamic",
Legality::Covered(_) => "Covered",
Legality::Illegal => "Illegal",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
Partial,
Full,
}
#[derive(Clone)]
struct ResourceRule {
what: String,
check: Arc<dyn Fn(&World) -> bool + Send + Sync>,
}
impl fmt::Debug for ResourceRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ResourceRule({:?})", self.what)
}
}
#[derive(Clone, Debug)]
pub struct Target {
pub name: String,
rules: BTreeMap<String, Legality>,
rule_words: BTreeMap<String, String>,
resource_rules: BTreeMap<String, ResourceRule>,
}
impl Target {
pub fn new(name: impl Into<String>) -> Self {
Target {
name: name.into(),
rules: BTreeMap::new(),
rule_words: BTreeMap::new(),
resource_rules: BTreeMap::new(),
}
}
pub fn rule(mut self, name: &str, legality: Legality) -> Self {
self.rules.insert(name.to_owned(), legality);
self
}
pub fn legal(self, name: &str) -> Self {
self.rule(name, Legality::Legal)
}
pub fn illegal(self, name: &str) -> Self {
self.rule(name, Legality::Illegal)
}
pub fn dynamic(self, name: &str, check: impl Fn(&World, Entity) -> bool + Send + Sync + 'static) -> Self {
self.rule(name, Legality::Dynamic(Arc::new(check)))
}
pub fn covered(mut self, name: &str, what: &str, check: fn(&World, Entity) -> bool) -> Self {
self.rule_words.insert(name.to_owned(), what.to_owned());
self.rule(name, Legality::Covered(check))
}
pub fn dynamic_typed<T: Component>(self, check: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
self.dynamic(T::NAME, move |w, e| w.get::<T>(e).is_some_and(&check))
}
pub fn check<T: Component>(
mut self,
what: &str,
check: impl Fn(&T) -> bool + Send + Sync + 'static,
) -> Self {
self.rule_words.insert(T::NAME.to_owned(), what.to_owned());
self.dynamic_typed::<T>(check)
}
pub fn check_resource<R: Resource>(
mut self,
what: &str,
check: impl Fn(&R) -> bool + Send + Sync + 'static,
) -> Self {
self.resource_rules.insert(
R::NAME.to_owned(),
ResourceRule {
what: what.to_owned(),
check: Arc::new(move |w: &World| w.resource::<R>().is_none_or(&check)),
},
);
self
}
pub fn rule_words(&self, name: &str) -> Option<&str> {
self.rule_words
.get(name)
.or_else(|| self.resource_rules.get(name).map(|r| &r.what))
.map(String::as_str)
}
pub fn refused_resources(&self, w: &World) -> Vec<&str> {
self.resource_rules
.iter()
.filter(|(_, r)| !(r.check)(w))
.map(|(n, _)| n.as_str())
.collect()
}
pub fn legality(&self, name: &str) -> Option<&Legality> {
self.rules.get(name)
}
pub fn rules(&self) -> Vec<(&str, Rule)> {
self.rules
.iter()
.map(|(name, legality)| {
let rule = match legality {
Legality::Legal => Rule::Legal,
Legality::Dynamic(_) => Rule::Dynamic,
Legality::Covered(_) => Rule::Covered,
Legality::Illegal => Rule::Illegal,
};
(name.as_str(), rule)
})
.collect()
}
pub fn checked_resources(&self) -> Vec<&str> {
self.resource_rules.keys().map(String::as_str).collect()
}
pub fn check_prepared(&self, w: &World) -> Prepared {
let mut out = Prepared::default();
for name in w.component_names() {
let all = w.entities_named(name);
match self.rules.get(name) {
Some(Legality::Legal) => {}
Some(Legality::Dynamic(f)) => {
let bad: Vec<Entity> = all.into_iter().filter(|&e| !f(w, e)).collect();
if !bad.is_empty() {
let words = self.rule_words(name).unwrap_or_default().to_owned();
out.invalid.push((name.to_owned(), words, bad));
}
}
Some(Legality::Covered(f)) => {
let uncovered = all.iter().filter(|&&e| !f(w, e)).count();
if uncovered > 0 {
let words = self.rule_words(name).unwrap_or_default();
out.unsupported
.push((format!("{name} not {words}"), uncovered as u64));
}
}
Some(Legality::Illegal) | None => out.unsupported.push((name.to_owned(), all.len() as u64)),
}
}
out.invalid_resources = self.refused_resources(w).into_iter().map(str::to_owned).collect();
for name in w.resource_names() {
let written = crate::convert::OPERATIONAL_RESOURCES.contains(&name)
|| self.resource_rules.contains_key(name)
|| self
.rules
.get(name)
.is_some_and(|rule| !matches!(rule, Legality::Illegal));
if !written {
out.unsupported_resources.push(name.to_owned());
}
}
out
}
pub fn illegal_entities(&self, w: &World, name: &str, mode: Mode) -> Vec<Entity> {
let all = w.entities_named(name);
match self.rules.get(name) {
Some(Legality::Legal) => Vec::new(),
Some(Legality::Illegal) => all,
Some(Legality::Dynamic(f)) => all.into_iter().filter(|&e| !f(w, e)).collect(),
Some(Legality::Covered(_)) if mode == Mode::Partial => Vec::new(),
Some(Legality::Covered(f)) => all.into_iter().filter(|&e| !f(w, e)).collect(),
None if mode == Mode::Partial => Vec::new(),
None => all,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Prepared {
pub unsupported: Vec<(String, u64)>,
pub invalid: Vec<(String, String, Vec<Entity>)>,
pub invalid_resources: Vec<String>,
pub unsupported_resources: Vec<String>,
}
pub trait Validator: Send + Sync {
fn name(&self) -> &'static str;
fn check(&self, w: &World) -> Vec<(Option<Entity>, String)>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifyFailure {
pub check: String,
pub entity: Option<Entity>,
pub message: String,
}
impl fmt::Display for VerifyFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.entity {
Some(e) => write!(f, "{} on {e:?}: {}", self.check, self.message),
None => write!(f, "{}: {}", self.check, self.message),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Parse(String),
#[error("pass {pass} left out {dropped} {measure} but declared {declared} (PassLog::dropped)")]
Conservation {
pass: String,
measure: Measure,
dropped: u64,
declared: u64,
},
#[error("no pass named {0:?}")]
UnknownPass(String),
#[error("pass {pass}: {message}")]
Options { pass: String, message: String },
#[error("{name}: {message}")]
Stated { name: &'static str, message: String },
#[error("pass {pass} requires `{name}`, which the world does not have")]
Missing { pass: String, name: String },
#[error("the world states `{name}` as {world}, the program as {program}")]
SettingDisagrees {
name: &'static str,
world: String,
program: String,
},
#[error("pass {pass} failed: {message}")]
Pass { pass: String, message: String },
#[error("pass {pass} {what} `{names:?}` without declaring it")]
Undeclared {
pass: String,
what: &'static str,
names: Vec<String>,
},
#[error("verify after {after}: {}", failures.iter().take(10).map(|f| f.to_string()).collect::<Vec<_>>().join("; "))]
Verify {
after: String,
failures: Vec<VerifyFailure>,
},
#[error("instrument at {step}: {message}")]
Instrument { step: String, message: String },
#[error("{}", illegal_text(.target, .name, .entities, .rule, .lowered_by))]
Illegal {
target: String,
name: String,
entities: Vec<Entity>,
rule: String,
lowered_by: Vec<String>,
},
#[error("{changed_by} provides `{name}` after {settled_by} settled it: its values would not be settled")]
SettledThenChanged {
settled_by: String,
changed_by: String,
name: String,
},
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
fn illegal_text(target: &str, name: &str, entities: &[Entity], rule: &str, lowered_by: &[String]) -> String {
let rule = if rule.is_empty() {
String::new()
} else {
format!(" ({rule})")
};
let on = if entities.is_empty() {
String::new()
} else {
format!(" on {entities:?}")
};
let why = if lowered_by.is_empty() {
"nothing in the stated pipeline removes or rewrites it".to_owned()
} else {
format!("{} left it", lowered_by.join(", "))
};
format!("`{target}` does not accept `{name}`{rule}{on} after the pipeline: {why}")
}
fn check_settled(passes: &[Box<dyn Pass>]) -> Result<()> {
for (i, a) in passes.iter().enumerate() {
for &n in a.settles() {
for (j, b) in passes.iter().enumerate().skip(i + 1) {
let adds = b.provides().contains(&n) && !b.settles().contains(&n);
if adds && !passes[j + 1..].iter().any(|c| c.settles().contains(&n)) {
return Err(Error::SettledThenChanged {
settled_by: pass_text(a.as_ref()),
changed_by: pass_text(b.as_ref()),
name: n.to_owned(),
});
}
}
}
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct StepReport {
pub pass: String,
pub approximates: &'static str,
pub log: PassLog,
pub added: Vec<String>,
pub removed: Vec<String>,
pub conservation: Vec<Gap>,
pub failed: Option<String>,
pub skipped: Vec<String>,
pub time: StepTime,
pub settings: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StepTime {
pub run: Duration,
pub checks: Duration,
}
impl PartialEq for StepReport {
fn eq(&self, other: &StepReport) -> bool {
let StepReport {
pass,
approximates,
log,
added,
removed,
conservation,
failed,
skipped,
time: _,
settings: _,
} = self;
(
pass,
approximates,
log,
added,
removed,
conservation,
failed,
skipped,
) == (
&other.pass,
&other.approximates,
&other.log,
&other.added,
&other.removed,
&other.conservation,
&other.failed,
&other.skipped,
)
}
}
impl Eq for StepReport {}
impl RunReport {
pub fn time(&self) -> StepTime {
self.steps.iter().fold(StepTime::default(), |t, s| StepTime {
run: t.run + s.time.run,
checks: t.checks + s.time.checks,
})
}
}
pub const SKIPPED: &str = "runs holding none of its required names, skipped";
pub const SKIPPED_UNFED: &str = "runs missing required names, skipped";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MissingInputs {
#[default]
Refuse,
Skip,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RunReport {
pub target: String,
pub pipeline: String,
pub steps: Vec<StepReport>,
pub dropped_outside: Vec<(String, crate::Dropped)>,
}
impl fmt::Display for RunReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "target {}: pipeline [{}]", self.target, self.pipeline)?;
for s in &self.steps {
write!(f, " {}: +{:?} -{:?}", s.pass, s.added, s.removed)?;
for (k, n) in &s.log.counts {
write!(f, " {k}={n}")?;
}
for (k, n) in &s.log.approximations {
write!(f, " ~{k}={n}")?;
}
for (k, n) in &s.log.omitted {
write!(f, " -{k}={n}")?;
}
for ((m, why), n) in &s.log.drops {
write!(f, " -{m}:{why}={n}")?;
}
for g in &s.conservation {
write!(f, " !{g}")?;
}
if s.skipped.is_empty() {
write!(
f,
" ({:.3} s, checks {:.3} s",
s.time.run.as_secs_f64(),
s.time.checks.as_secs_f64()
)?;
for setting in &s.settings {
write!(f, "; {setting}")?;
}
write!(f, ")")?;
}
writeln!(f)?;
}
for (next, dropped) in &self.dropped_outside {
writeln!(f, " outside any pass, before {next}: dropped {dropped}")?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Step<'a> {
pub index: usize,
pub name: &'static str,
pub text: &'a str,
}
pub trait Instrument {
fn before_pass(&mut self, _step: &Step<'_>, _w: &World) -> std::result::Result<(), String> {
Ok(())
}
fn after_pass(
&mut self,
_step: &Step<'_>,
_w: &World,
_report: &StepReport,
) -> std::result::Result<(), String> {
Ok(())
}
fn pass_failed(
&mut self,
_step: &Step<'_>,
_w: &World,
_error: &Error,
) -> std::result::Result<(), String> {
Ok(())
}
}
struct NoInstrument;
impl Instrument for NoInstrument {}
#[derive(Default)]
pub struct PassManager {
pub registry: PassRegistry,
validators: Vec<Box<dyn Validator>>,
conservation: Conservation,
pass_checks: bool,
missing_inputs: MissingInputs,
settings: Option<Settings>,
}
impl fmt::Debug for PassManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PassManager")
.field("registry", &self.registry)
.field(
"validators",
&self.validators.iter().map(|v| v.name()).collect::<Vec<_>>(),
)
.finish()
}
}
impl PassManager {
pub fn new(registry: PassRegistry) -> Self {
PassManager {
registry,
validators: Vec::new(),
conservation: Conservation::Off,
pass_checks: cfg!(debug_assertions),
missing_inputs: MissingInputs::Refuse,
settings: None,
}
}
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = Some(settings);
self
}
fn state_settings(&self, w: &mut World) -> Result<()> {
let Some(Settings { threads }) = self.settings else {
return Ok(());
};
match w.resource::<primase_foundations::parallel::Threads>() {
Some(&held) if held != threads => Err(Error::SettingDisagrees {
name: <primase_foundations::parallel::Threads as Resource>::NAME,
world: RunSetting::text(&held),
program: RunSetting::text(&threads),
}),
Some(_) => Ok(()),
None => {
w.insert_resource(threads);
Ok(())
}
}
}
pub fn with_pass_checks(mut self, on: bool) -> Self {
self.pass_checks = on;
self
}
pub fn pass_checks(&self) -> bool {
self.pass_checks
}
pub fn with_missing_inputs(mut self, policy: MissingInputs) -> Self {
self.missing_inputs = policy;
self
}
pub fn with_conservation(mut self, c: Conservation) -> Self {
self.conservation = c;
self
}
pub fn conservation(&self) -> Conservation {
self.conservation
}
pub fn add_validator(&mut self, v: Box<dyn Validator>) {
self.validators.push(v);
}
pub fn verify(&self, w: &World, after: &str) -> Result<()> {
let mut failures: Vec<VerifyFailure> = w
.verify_components()
.into_iter()
.map(|(c, e, message)| VerifyFailure {
check: c.to_owned(),
entity: Some(e),
message,
})
.collect();
failures.extend(
w.verify_resources()
.into_iter()
.map(|(r, message)| VerifyFailure {
check: r.to_owned(),
entity: None,
message,
}),
);
for v in &self.validators {
failures.extend(v.check(w).into_iter().map(|(entity, message)| VerifyFailure {
check: v.name().to_owned(),
entity,
message,
}));
}
if failures.is_empty() {
Ok(())
} else {
Err(Error::Verify {
after: after.to_owned(),
failures,
})
}
}
pub fn run(&self, w: &mut World, pipeline: &Pipeline, report: &mut RunReport) -> Result<()> {
self.run_instrumented(w, pipeline, report, &mut NoInstrument)
}
pub fn run_instrumented(
&self,
w: &mut World,
pipeline: &Pipeline,
report: &mut RunReport,
instrument: &mut dyn Instrument,
) -> Result<()> {
self.run_with(w, pipeline, report, instrument)
}
pub fn run_part(&self, w: &mut World, pipeline: &Pipeline, report: &mut RunReport) -> Result<()> {
self.run_with(w, pipeline, report, &mut NoInstrument)
}
fn run_with(
&self,
w: &mut World,
pipeline: &Pipeline,
report: &mut RunReport,
instrument: &mut dyn Instrument,
) -> Result<()> {
pipeline.check()?;
self.state_settings(w)?;
for (index, p) in pipeline.passes.iter().enumerate() {
let text = pass_text(p.as_ref());
let missing: Vec<String> = p
.requires()
.iter()
.filter(|r| !w.has_component_named(r) && !w.resource_names().contains(r))
.map(|r| (*r).to_owned())
.collect();
let declared_skip =
p.may_lack_inputs() && !p.requires().is_empty() && missing.len() == p.requires().len();
if declared_skip || (self.missing_inputs == MissingInputs::Skip && !missing.is_empty()) {
let mut log = PassLog::default();
log.count(if declared_skip { SKIPPED } else { SKIPPED_UNFED }, 1);
report.steps.push(StepReport {
pass: text,
approximates: p.approximates(),
log,
added: Vec::new(),
removed: Vec::new(),
conservation: Vec::new(),
failed: None,
skipped: missing,
time: StepTime::default(),
settings: Vec::new(),
});
continue;
}
let step = Step {
index,
name: p.name(),
text: &text,
};
let hook = |r: std::result::Result<(), String>| {
r.map_err(|message| Error::Instrument {
step: text.clone(),
message,
})
};
hook(instrument.before_pass(&step, w))?;
let owned_before = w
.resource::<crate::convert::PartScope>()
.map(|s| s.owned(w))
.unwrap_or_default();
let outside = w.take_dropped();
if !outside.is_empty() {
report.dropped_outside.push((text.clone(), outside));
}
let mut log = PassLog::default();
let started = Instant::now();
let mut run = Duration::ZERO;
let ran = self.run_pass(w, p.as_ref(), &text, &mut log, &mut run);
let time = StepTime {
run,
checks: started.elapsed().saturating_sub(run),
};
match ran {
Ok(mut done) => {
done.time = time;
crate::convert::PartScope::declare(
w,
&done.log.placed,
&done.log.placed_from,
&owned_before,
);
hook(instrument.after_pass(&step, w, &done))?;
report.steps.push(done);
}
Err(e) => {
report.steps.push(StepReport {
pass: text.clone(),
approximates: p.approximates(),
log,
added: Vec::new(),
removed: Vec::new(),
conservation: Vec::new(),
failed: Some(e.to_string()),
skipped: Vec::new(),
time,
settings: Vec::new(),
});
let _ = instrument.pass_failed(&step, w, &e);
return Err(e);
}
}
}
Ok(())
}
fn run_pass(
&self,
w: &mut World,
p: &dyn Pass,
text: &str,
log: &mut PassLog,
run: &mut Duration,
) -> Result<StepReport> {
for &r in p.requires() {
if !w.has_component_named(r) && !w.resource_names().contains(&r) {
return Err(Error::Missing {
pass: text.to_owned(),
name: r.to_owned(),
});
}
}
let settings = p
.settings()
.iter()
.map(|s| {
s.text(w).ok_or_else(|| Error::Missing {
pass: text.to_owned(),
name: s.name.to_owned(),
})
})
.collect::<Result<Vec<_>>>()?;
let before: BTreeSet<String> = w.component_names().into_iter().map(str::to_owned).collect();
let resources_before: BTreeSet<String> = w.resource_names().into_iter().map(str::to_owned).collect();
let holders: BTreeMap<&str, Vec<Entity>> = if p.despawns_entities() {
before
.iter()
.filter(|n| !p.removes().contains(&n.as_str()))
.map(|n| (n.as_str(), w.entities_named(n)))
.collect()
} else {
BTreeMap::new()
};
let held_before: BTreeMap<&str, BTreeSet<Entity>> = if self.pass_checks {
before
.iter()
.map(|n| (n.as_str(), w.entities_named(n).into_iter().collect()))
.collect()
} else {
BTreeMap::new()
};
if self.pass_checks {
w.start_effects();
}
let first_made = w.next_index();
let ledger = (self.conservation != Conservation::Off).then(|| {
w.start_rebuild_log();
crate::ledger::Before::take(w)
});
let started = Instant::now();
let ran = p.run(w, log);
*run = started.elapsed();
let rebuilds = w.take_rebuild_log();
let mut effects = w.take_effects();
effects.dropped = w.take_dropped();
ran.map_err(|message| Error::Pass {
pass: text.to_owned(),
message,
})?;
count_dropped(log, &effects.dropped);
let conservation = ledger.map_or_else(Vec::new, |b| b.gaps(w, &rebuilds, log));
if self.conservation == Conservation::Enforce
&& let Some(g) = conservation.first()
{
return Err(Error::Conservation {
pass: text.to_owned(),
measure: g.measure,
dropped: g.dropped,
declared: g.declared,
});
}
let made = first_made..w.next_index();
let consumed = |e: &Entity| log.placed_from.values().any(|s| s == e);
if let Some(e) = log
.placed
.iter()
.find(|e| !made.contains(&e.index()) || (!w.is_alive(**e) && !consumed(e)))
{
return Err(Error::Pass {
pass: text.to_owned(),
message: if e.index() < first_made {
format!("placed {e:?}, which it did not make")
} else if e.index() >= made.end {
format!("placed {e:?}, which was never made")
} else {
format!("placed {e:?}, which is not alive after it, and nothing it placed came from it")
},
});
}
if let Some((what, e)) = log
.approximated_on
.iter()
.flat_map(|(what, entities)| entities.iter().map(move |e| (what, e)))
.find(|(_, e)| !w.is_alive(**e))
{
return Err(Error::Pass {
pass: text.to_owned(),
message: format!("approximation {what:?} attributed to {e:?}, which is not alive after it"),
});
}
if let Some((e, s)) = log.placed_from.iter().find(|(_, s)| s.index() >= made.end) {
return Err(Error::Pass {
pass: text.to_owned(),
message: format!("placed {e:?} from {s:?}, which was never made"),
});
}
let after: BTreeSet<String> = w.component_names().into_iter().map(str::to_owned).collect();
let resources_after: BTreeSet<String> = w.resource_names().into_iter().map(str::to_owned).collect();
let mut added: Vec<String> = after.difference(&before).cloned().collect();
let mut removed: Vec<String> = before.difference(&after).cloned().collect();
let despawned_with = |n: &str| {
holders
.get(n)
.is_some_and(|held| held.iter().all(|&e| !w.is_alive(e)))
};
for (what, names, declared, removal) in [
("added", &added, p.provides(), false),
("removed", &removed, p.removes(), true),
] {
let undeclared: Vec<String> = names
.iter()
.filter(|n| {
!declared.contains(&n.as_str())
&& !(removal && despawned_with(n))
&& !(removal && !dropped_from(&effects.dropped, n).is_empty())
&& !(removal && p.removes_by_log() && log.removed.contains(n.as_str()))
})
.cloned()
.collect();
if !undeclared.is_empty() {
return Err(Error::Undeclared {
pass: text.to_owned(),
what,
names: undeclared,
});
}
}
for (what, names, declared) in [
(
"added",
resources_after.difference(&resources_before),
p.provides(),
),
(
"removed",
resources_before.difference(&resources_after),
p.removes(),
),
] {
let names: Vec<String> = names.cloned().collect();
let undeclared: Vec<String> = names
.iter()
.filter(|n| !declared.contains(&n.as_str()))
.cloned()
.collect();
if !undeclared.is_empty() {
return Err(Error::Undeclared {
pass: text.to_owned(),
what,
names: undeclared,
});
}
if what == "added" {
added.extend(names);
} else {
removed.extend(names);
}
}
added.sort();
removed.sort();
if self.pass_checks {
self.check_entity_effects(w, p, text, log, &held_before, &effects)?;
self.verify(w, text)?;
}
Ok(StepReport {
pass: text.to_owned(),
approximates: p.approximates(),
log: std::mem::take(log),
added,
removed,
conservation,
failed: None,
skipped: Vec::new(),
time: StepTime::default(),
settings,
})
}
fn check_entity_effects(
&self,
w: &World,
p: &dyn Pass,
text: &str,
log: &PassLog,
held_before: &BTreeMap<&str, BTreeSet<Entity>>,
effects: &crate::world::Effects,
) -> Result<()> {
let undeclared = |what: &'static str, names: Vec<String>| -> Result<()> {
if names.is_empty() {
Ok(())
} else {
Err(Error::Undeclared {
pass: text.to_owned(),
what,
names,
})
}
};
let some = |n: &str, es: &[Entity]| format!("{n} ({} entities, the first {:?})", es.len(), es[0]);
let copied = |e: Entity, name: &str| {
effects.copies.iter().any(|&(from, to)| {
to == e
&& (held_before.get(name).is_some_and(|h| h.contains(&from))
|| w.entity_has_named(from, name))
})
};
let mut added = Vec::new();
for name in w.component_names() {
if p.provides().contains(&name) {
continue;
}
let before = held_before.get(name);
let new: Vec<Entity> = w
.entities_named(name)
.into_iter()
.filter(|e| !before.is_some_and(|b| b.contains(e)) && !copied(*e, name))
.collect();
if !new.is_empty() {
added.push(some(name, &new));
}
}
undeclared("added to entities", added)?;
let (mut taken, mut despawned) = (Vec::new(), BTreeSet::new());
for (&name, before) in held_before {
let declared = p.removes().contains(&name) || (p.removes_by_log() && log.removed.contains(name));
let gone: Vec<Entity> = before
.iter()
.copied()
.filter(|&e| !w.entity_has_named(e, name))
.collect();
let (mut alive, dead): (Vec<Entity>, Vec<Entity>) =
gone.into_iter().partition(|&e| w.is_alive(e));
let world_dropped = dropped_from(&effects.dropped, name);
alive.retain(|e| !world_dropped.contains(e));
if !declared && !alive.is_empty() {
taken.push(some(name, &alive));
}
if !p.despawns_entities() {
despawned.extend(dead);
}
}
undeclared("taken from entities", taken)?;
if let Some(first) = despawned.first() {
return Err(Error::Pass {
pass: text.to_owned(),
message: format!(
"despawned {} entities (the first {first:?}) without declaring it (Pass::despawns_entities)",
despawned.len()
),
});
}
let names = w.resource_names();
let replaced = effects
.resources_removed
.iter()
.filter(|r| names.iter().any(|n| n == *r));
let written: Vec<String> = effects
.resources_written
.iter()
.chain(replaced)
.collect::<BTreeSet<_>>()
.into_iter()
.filter(|r| !p.modifies().contains(&r.as_str()) && !p.provides().contains(&r.as_str()))
.cloned()
.collect();
undeclared("written (resources)", written)
}
pub fn convert(
&self,
w: &mut World,
pipeline: &Pipeline,
target: &Target,
mode: Mode,
) -> Result<RunReport> {
self.convert_instrumented(w, pipeline, target, mode, &mut NoInstrument)
}
pub fn convert_instrumented(
&self,
w: &mut World,
pipeline: &Pipeline,
target: &Target,
mode: Mode,
instrument: &mut dyn Instrument,
) -> Result<RunReport> {
self.verify(w, "the reader")?;
let mut report = RunReport {
target: target.name.clone(),
pipeline: pipeline.to_string(),
..RunReport::default()
};
self.run_instrumented(w, pipeline, &mut report, instrument)?;
let refused = w
.component_names()
.into_iter()
.map(|name| (name.to_owned(), target.illegal_entities(w, name, mode)))
.find(|(_, bad)| !bad.is_empty())
.or_else(|| {
target
.refused_resources(w)
.first()
.map(|n| ((*n).to_owned(), Vec::new()))
});
if let Some((name, bad)) = refused {
let lowered_by = pipeline
.passes
.iter()
.filter(|p| p.removes().contains(&name.as_str()) || p.modifies().contains(&name.as_str()))
.map(|p| pass_text(p.as_ref()))
.collect();
return Err(Error::Illegal {
target: target.name.clone(),
rule: target.rule_words(&name).unwrap_or_default().to_owned(),
name,
entities: bad.into_iter().take(10).collect(),
lowered_by,
});
}
self.verify(w, &format!("the pipeline, before {}", target.name))?;
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::{Opaque, OpaqueData};
#[test]
fn what_the_text_form_carries() {
for carried in ["", "512", "-64,-64,64,64", "3:2,4:1", "a:x;b:y", "k=1:v"] {
assert!(carries(carried), "{carried:?}");
}
for lost in ["a:x,", "a:1,;b=c:2", "a:1,b=2", "x{y}", " a", "a ", "a, b"] {
assert!(!carries(lost), "{lost:?}");
}
}
#[test]
fn option_values_with_commas_parse_as_display_writes_them() {
let parsed = parse_pipeline("a{rect=-64,-64,64,64,depth=3:2,4:1},b{by=1,2,3},c").unwrap();
let opts = |pairs: &[(&str, &str)]| -> Options {
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
};
assert_eq!(
parsed,
vec![
(
"a".to_owned(),
opts(&[("rect", "-64,-64,64,64"), ("depth", "3:2,4:1")])
),
("b".to_owned(), opts(&[("by", "1,2,3")])),
("c".to_owned(), Options::new()),
]
);
assert!(parse_pipeline("a{-64,x=1}").is_err());
}
#[derive(Clone, Debug, PartialEq)]
struct File(u32);
impl Component for File {
const NAME: &'static str = "toy.file";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
}
#[derive(Clone, Debug, PartialEq)]
struct Mesh(u32);
impl Component for Mesh {
const NAME: &'static str = "toy.mesh";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn verify(&self, _e: Entity, _w: &World) -> std::result::Result<(), String> {
if self.0 == 13 {
Err("unlucky mesh".into())
} else {
Ok(())
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct Layered(u32);
impl Component for Layered {
const NAME: &'static str = "toy.layered";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
}
#[derive(Clone, Debug, PartialEq)]
struct Pbr(u32);
impl Component for Pbr {
const NAME: &'static str = "toy.pbr";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
}
struct Load;
impl Pass for Load {
fn name(&self) -> &'static str {
"load"
}
fn provides(&self) -> &[&'static str] {
&["toy.mesh"]
}
fn removes(&self) -> &[&'static str] {
&["toy.file"]
}
fn requires(&self) -> &[&'static str] {
&["toy.file"]
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
let files: Vec<(Entity, u32)> = w.iter::<File>().map(|(e, f)| (e, f.0)).collect();
for (e, n) in files {
w.remove::<File>(e);
w.insert(e, Mesh(n));
log.count("loaded", 1);
}
Ok(())
}
}
const LOAD: PassSpec = PassSpec {
name: "load",
options: &[],
make: |_| Ok(Box::new(Load)),
};
struct Bake {
size: u32,
}
impl Pass for Bake {
fn name(&self) -> &'static str {
"bake"
}
fn options(&self) -> Options {
if self.size == 1024 {
Options::new()
} else {
Options::from([("size".to_owned(), self.size.to_string())])
}
}
fn requires(&self) -> &[&'static str] {
&["toy.mesh", "toy.layered"]
}
fn provides(&self) -> &[&'static str] {
&["toy.pbr"]
}
fn removes(&self) -> &[&'static str] {
&["toy.layered"]
}
fn approximates(&self) -> &'static str {
"layer blend baked to one texture"
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
let ls: Vec<(Entity, u32)> = w.iter::<Layered>().map(|(e, l)| (e, l.0)).collect();
for (e, n) in ls {
w.remove::<Layered>(e);
w.insert(e, Pbr(n));
log.approximate("baked", 1);
}
Ok(())
}
}
const BAKE: PassSpec = PassSpec {
name: "bake",
options: &[OptionSpec::optional(
"size",
"512",
"texels: the baked texture's size",
"1024",
)],
make: |g| {
let size = match g.optional("size") {
Some(v) => v.parse().map_err(|_| format!("size {v:?} is not a number"))?,
None => 1024,
};
Ok(Box::new(Bake { size }))
},
};
struct Sneaky;
impl Pass for Sneaky {
fn name(&self) -> &'static str {
"sneaky"
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
let e = w.spawn();
w.insert(e, Pbr(1));
Ok(())
}
}
const SNEAKY: PassSpec = PassSpec {
name: "sneaky",
options: &[],
make: |_| Ok(Box::new(Sneaky)),
};
fn registry() -> PassRegistry {
let mut r = PassRegistry::new();
r.add(&LOAD);
r.add(&BAKE);
r.add(&SNEAKY);
r
}
fn world() -> World {
let mut w = World::new();
let a = w.spawn();
w.insert(a, File(1));
w.insert(a, Layered(5));
w.insert_opaque(
a,
"game.thing",
Opaque {
version: 1,
data: OpaqueData::Bytes(vec![]),
},
)
.unwrap();
w
}
fn gltf() -> Target {
Target::new("gltf")
.legal("toy.mesh")
.legal("toy.pbr")
.illegal("toy.file")
.illegal("toy.layered")
}
struct Slow(u64);
impl Pass for Slow {
fn name(&self) -> &'static str {
"slow"
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
std::thread::sleep(Duration::from_millis(self.0));
Ok(())
}
}
#[test]
fn unfed_passes_are_named_with_what_they_lack_and_the_names_held_after() {
let names = |n: &[&str]| n.iter().map(|s| (*s).to_owned()).collect::<BTreeSet<String>>();
let load_then_bake = || Pipeline {
passes: vec![Box::new(Load), Box::new(Bake { size: 1024 })],
};
assert_eq!(
load_then_bake().unfed(&names(&["toy.file", "toy.layered"])),
Unfed {
passes: Vec::new(),
held: names(&["toy.file", "toy.layered", "toy.mesh", "toy.pbr"]),
}
);
assert_eq!(
load_then_bake().unfed(&names(&["toy.layered"])),
Unfed {
passes: vec![
("load".to_owned(), vec!["toy.file"]),
("bake".to_owned(), vec!["toy.mesh"])
],
held: names(&["toy.layered"]),
}
);
struct Either;
impl Pass for Either {
fn name(&self) -> &'static str {
"either"
}
fn requires(&self) -> &[&'static str] {
&["toy.mesh", "toy.pbr"]
}
fn provides(&self) -> &[&'static str] {
&["toy.lit"]
}
fn may_lack_inputs(&self) -> bool {
true
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Ok(())
}
}
let either = || Pipeline {
passes: vec![Box::new(Either)],
};
assert_eq!(
either().unfed(&names(&[])),
Unfed {
passes: Vec::new(),
held: names(&[]),
}
);
assert_eq!(
either().unfed(&names(&["toy.mesh"])).passes,
[("either".to_owned(), vec!["toy.pbr"])]
);
assert_eq!(
either().unfed(&names(&["toy.mesh", "toy.pbr"])),
Unfed {
passes: Vec::new(),
held: names(&["toy.lit", "toy.mesh", "toy.pbr"]),
}
);
}
#[test]
fn optional_preparation_reports_missing_inputs_and_uses_actual_preceding_outputs() {
struct NeedsBoth;
impl Pass for NeedsBoth {
fn name(&self) -> &'static str {
"needs-both"
}
fn requires(&self) -> &[&'static str] {
&["toy.mesh", "toy.pbr"]
}
fn run(&self, _: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
log.count("ran with both inputs", 1);
Ok(())
}
}
let pipeline = || Pipeline {
passes: vec![
Box::new(NeedsBoth),
Box::new(Load),
Box::new(NeedsBoth),
Box::new(Bake { size: 1024 }),
Box::new(NeedsBoth),
],
};
let mut input = world();
let mut report = RunReport::default();
PassManager::new(PassRegistry::new())
.with_missing_inputs(MissingInputs::Skip)
.run(&mut input, &pipeline(), &mut report)
.unwrap();
assert_eq!(report.steps.len(), 5);
assert_eq!(report.steps[0].skipped, ["toy.mesh", "toy.pbr"]);
assert_eq!(report.steps[2].skipped, ["toy.pbr"]);
assert_eq!(report.steps[2].log.counts[SKIPPED_UNFED], 1);
assert!(report.steps[0].added.is_empty() && report.steps[2].added.is_empty());
assert!(report.steps[4].skipped.is_empty());
assert_eq!(report.steps[4].log.counts["ran with both inputs"], 1);
assert!(input.has_component_named("toy.pbr"));
let mut strict_report = RunReport::default();
assert!(matches!(
PassManager::new(PassRegistry::new()).run(&mut world(), &pipeline(), &mut strict_report),
Err(Error::Missing { .. })
));
assert!(strict_report.steps[0].skipped.is_empty());
}
#[test]
fn optional_preparation_does_not_hide_a_fed_pass_failure() {
struct Fails;
impl Pass for Fails {
fn name(&self) -> &'static str {
"fails"
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Err("source data is malformed".into())
}
}
let mut report = RunReport::default();
let result = PassManager::new(PassRegistry::new())
.with_missing_inputs(MissingInputs::Skip)
.run(
&mut world(),
&Pipeline {
passes: vec![Box::new(Fails)],
},
&mut report,
);
assert!(result.is_err());
assert!(
report.steps[0]
.failed
.as_ref()
.unwrap()
.contains("source data is malformed")
);
assert!(report.steps[0].skipped.is_empty());
}
#[test]
fn steps_are_timed_around_their_runs() {
let pipeline = Pipeline {
passes: vec![Box::new(Slow(30)), Box::new(Load)],
};
let mut report = RunReport::default();
PassManager::new(PassRegistry::new())
.with_pass_checks(true)
.run(&mut world(), &pipeline, &mut report)
.unwrap();
let [slow, load] = [&report.steps[0].time, &report.steps[1].time];
assert!(slow.run >= Duration::from_millis(30), "{slow:?}");
assert!(load.run < slow.run, "{load:?} {slow:?}");
assert_eq!(report.time().run, slow.run + load.run);
assert_eq!(report.time().checks, slow.checks + load.checks);
let text = report.to_string();
let line = text
.lines()
.find(|l| l.trim_start().starts_with("slow:"))
.unwrap();
assert!(
line.ends_with(&format!(
"({:.3} s, checks {:.3} s)",
slow.run.as_secs_f64(),
slow.checks.as_secs_f64()
)),
"{line}"
);
struct Skippable;
impl Pass for Skippable {
fn name(&self) -> &'static str {
"skippable"
}
fn requires(&self) -> &[&'static str] {
&["toy.pbr"]
}
fn may_lack_inputs(&self) -> bool {
true
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Ok(())
}
}
let mut report = RunReport::default();
PassManager::new(PassRegistry::new())
.run_part(
&mut world(),
&Pipeline {
passes: vec![Box::new(Skippable)],
},
&mut report,
)
.unwrap();
assert_eq!(report.steps[0].time, StepTime::default());
assert!(!report.to_string().contains(" s, checks "));
let mut later = report.clone();
later.steps[0].time.run = Duration::from_secs(1);
assert_eq!(later, report);
assert_ne!(later.steps[0].time, report.steps[0].time);
later.steps[0].pass = "other".into();
assert_ne!(later, report);
}
#[test]
fn a_targets_rules_read_back() {
let t = Target::new("t").legal("toy.pbr").illegal("toy.layered");
assert_eq!(
t.rules(),
[("toy.layered", Rule::Illegal), ("toy.pbr", Rule::Legal)]
);
assert!(t.checked_resources().is_empty());
}
#[test]
fn absorb_keeps_every_part_of_the_log() {
let mut w = World::new();
let (a, b, source) = (w.spawn(), w.spawn(), w.spawn());
let mut log = PassLog::default();
log.count("kept", 1);
log.approximate("rounded", 2);
log.place(a);
log.detail("explanation", serde_json::json!({"input": 1, "why": "first"}));
let mut other = PassLog::default();
other.count("kept", 3);
other.count("made", 1);
other.approximate("rounded", 1);
other.omit("no form", 4);
other.record_removed("toy.hat");
other.place_from(b, source);
other.dropped(Measure::Placements, "left out", 2);
other.detail("explanation", serde_json::json!({"input": 2, "why": "second"}));
other.detail("additional", serde_json::json!(["nested", {"result": true}]));
log.dropped(Measure::Placements, "left out", 1);
log.absorb(other);
assert_eq!(
log.counts,
BTreeMap::from([("kept".to_owned(), 4), ("made".to_owned(), 1)])
);
assert_eq!(log.approximations, BTreeMap::from([("rounded".to_owned(), 3)]));
assert_eq!(log.omitted, BTreeMap::from([("no form".to_owned(), 4)]));
assert_eq!(log.removed, BTreeSet::from(["toy.hat".to_owned()]));
assert_eq!(log.placed, BTreeSet::from([a, b]));
assert_eq!(log.placed_from, BTreeMap::from([(b, source)]));
assert_eq!(log.declared(Measure::Placements), 3);
assert_eq!(
log.details["explanation"],
[
serde_json::json!({"input": 1, "why": "first"}),
serde_json::json!({"input": 2, "why": "second"}),
]
);
assert_eq!(
log.details["additional"],
[serde_json::json!(["nested", {"result": true}])]
);
}
#[test]
fn pipeline_text_round_trips() {
let r = registry();
let p = r.parse(" load , bake{ size = 512 } ,bake").unwrap();
assert_eq!(p.to_string(), "load, bake{size=512}, bake");
assert_eq!(r.parse(&p.to_string()).unwrap().to_string(), p.to_string());
assert_eq!(r.parse("").unwrap().passes.len(), 0);
assert!(matches!(r.parse("load,,bake"), Err(Error::Parse(_))));
assert!(matches!(r.parse("bake{size=512"), Err(Error::Parse(_))));
assert!(matches!(r.parse("nope"), Err(Error::UnknownPass(n)) if n == "nope"));
assert!(matches!(r.parse("bake{size=big}"), Err(Error::Options { .. })));
assert!(matches!(r.parse("bake{colour=red}"), Err(Error::Options { .. })));
}
#[test]
fn a_conversion_runs_the_stated_pipeline() {
let m = PassManager::new(registry());
let mut w = world();
let pipeline = m.registry.parse("load, bake").unwrap();
let report = m.convert(&mut w, &pipeline, &gltf(), Mode::Partial).unwrap();
assert_eq!(report.target, "gltf");
assert_eq!(report.pipeline, "load, bake");
assert_eq!(
report.steps[1].log.approximations,
BTreeMap::from([("baked".to_owned(), 1)])
);
assert_eq!(report.steps[1].approximates, "layer blend baked to one texture");
assert_eq!(
w.component_names(),
["game.thing", "toy.mesh", "toy.pbr"],
"partial keeps game.thing"
);
assert!(
report
.to_string()
.starts_with("target gltf: pipeline [load, bake]\n")
);
}
#[test]
fn a_refused_name_says_the_stated_pipeline_does_not_lower_it() {
let m = PassManager::new(registry());
let pipeline = m.registry.parse("load, bake").unwrap();
let err = m
.convert(&mut world(), &pipeline, &gltf(), Mode::Full)
.unwrap_err();
assert_eq!(
err.to_string(),
"`gltf` does not accept `game.thing` on [e0] after the pipeline: nothing in the stated pipeline removes or \
rewrites it"
);
let accepting = gltf().legal("game.thing");
m.convert(&mut world(), &pipeline, &accepting, Mode::Full)
.unwrap();
let mut w = world();
let err = m
.convert(&mut w, &m.registry.parse("load").unwrap(), &gltf(), Mode::Partial)
.unwrap_err();
assert!(
matches!(&err, Error::Illegal { name, lowered_by, .. } if name == "toy.layered" && lowered_by.is_empty()),
"{err}"
);
assert!(
w.has_component_named("toy.layered"),
"nothing was added to lower it"
);
}
#[test]
fn dynamic_legality_refuses_only_failing_values() {
let m = PassManager::new(registry());
let t = Target::new("skate")
.legal("toy.mesh")
.legal("toy.layered")
.legal("game.thing")
.dynamic_typed::<File>(|f| f.0 < 10);
let none = Pipeline::default();
let mut w = world(); let report = m.convert(&mut w, &none, &t, Mode::Full).unwrap();
assert_eq!(report.pipeline, "");
assert!(w.has_component_named("toy.file"));
let failing = || {
let mut w = world();
let e = w.entities().next().unwrap();
w.insert(e, File(99)); w
};
let err = m.convert(&mut failing(), &none, &t, Mode::Full).unwrap_err();
assert!(
matches!(&err, Error::Illegal { name, .. } if name == "toy.file"),
"{err}"
);
let load = m.registry.parse("load").unwrap();
let report = m.convert(&mut failing(), &load, &t, Mode::Full).unwrap();
assert_eq!(report.pipeline, "load");
}
#[test]
fn undeclared_changes_and_verifier_name_the_pass() {
let m = PassManager::new(registry()).with_pass_checks(true);
let mut w = world();
let p = m.registry.parse("sneaky").unwrap();
let err = m.run(&mut w, &p, &mut RunReport::default()).unwrap_err();
assert_eq!(
err.to_string(),
"pass sneaky added `[\"toy.pbr\"]` without declaring it"
);
let mut w = world();
let e = w.entities().next().unwrap();
w.insert(e, File(13)); let p = m.registry.parse("load").unwrap();
let err = m.run(&mut w, &p, &mut RunReport::default()).unwrap_err();
assert_eq!(err.to_string(), "verify after load: toy.mesh on e0: unlucky mesh");
let p = m.registry.parse("bake").unwrap();
let err = m.run(&mut world(), &p, &mut RunReport::default()).unwrap_err();
assert_eq!(
err.to_string(),
"pass bake requires `toy.mesh`, which the world does not have"
);
}
struct Cull {
opted: bool,
strip: bool,
}
impl Pass for Cull {
fn name(&self) -> &'static str {
"cull"
}
fn despawns_entities(&self) -> bool {
self.opted
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
for e in w.iter::<Layered>().map(|(e, _)| e).collect::<Vec<_>>() {
w.despawn(e);
log.count("culled", 1);
}
if self.strip {
for e in w.iter::<File>().map(|(e, _)| e).collect::<Vec<_>>() {
w.remove::<File>(e);
}
}
Ok(())
}
}
struct LogStrip {
opted: bool,
record: bool,
}
impl Pass for LogStrip {
fn name(&self) -> &'static str {
"log-strip"
}
fn removes_by_log(&self) -> bool {
self.opted
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
for e in w.iter::<Layered>().map(|(e, _)| e).collect::<Vec<_>>() {
w.remove::<Layered>(e);
}
if self.record {
log.record_removed("toy.layered");
log.record_removed("toy.file");
}
Ok(())
}
}
#[test]
fn a_pass_removing_by_log_may_take_the_names_it_recorded() {
let run = |opted, record| {
let pipeline = Pipeline {
passes: vec![Box::new(LogStrip { opted, record })],
};
PassManager::new(PassRegistry::new()).run(&mut world(), &pipeline, &mut RunReport::default())
};
let undeclared = "pass log-strip removed `[\"toy.layered\"]` without declaring it";
assert!(run(true, true).is_ok());
assert_eq!(run(true, false).unwrap_err().to_string(), undeclared);
assert_eq!(run(false, true).unwrap_err().to_string(), undeclared);
}
struct Placer {
which: &'static str,
}
impl Pass for Placer {
fn name(&self) -> &'static str {
"placer"
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
let e = match self.which {
"old" => w.entities().next().expect("an entity"),
_ => w.spawn(),
};
if self.which == "gone" {
w.despawn(e);
}
log.place(e);
if self.which == "consumed" {
let derived = w.spawn();
log.place_from(derived, e);
w.despawn(e);
}
Ok(())
}
}
#[test]
fn a_pass_places_only_what_it_made_and_what_is_alive() {
let run = |which| {
let pipeline = Pipeline {
passes: vec![Box::new(Placer { which })],
};
let mut report = RunReport::default();
PassManager::new(PassRegistry::new())
.run(&mut world(), &pipeline, &mut report)
.map(|()| report.steps[0].log.placed.len())
.map_err(|e| e.to_string())
};
assert_eq!(run("new"), Ok(1));
assert!(run("old").unwrap_err().contains("which it did not make"));
assert!(run("gone").unwrap_err().contains("which is not alive after it"));
assert_eq!(run("consumed"), Ok(2));
}
struct Attributer {
which: &'static str,
}
impl Pass for Attributer {
fn name(&self) -> &'static str {
"attributer"
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
let e = w.spawn();
if self.which == "gone" {
w.despawn(e);
}
log.approximate_on(e, "base albedo could not be read", 1);
Ok(())
}
}
#[test]
fn an_approximation_is_attributed_to_a_live_entity() {
let run = |which| {
let pipeline = Pipeline {
passes: vec![Box::new(Attributer { which })],
};
let mut report = RunReport::default();
PassManager::new(PassRegistry::new())
.run(&mut world(), &pipeline, &mut report)
.map(|()| report.steps[0].log.clone())
.map_err(|e| e.to_string())
};
let log = run("live").expect("a live entity is accepted");
assert_eq!(log.approximations["base albedo could not be read"], 1);
assert_eq!(log.approximated_on["base albedo could not be read"].len(), 1);
assert!(run("gone").unwrap_err().contains("attributed to"));
}
#[test]
fn absorb_keeps_the_entities_an_approximation_was_attributed_to() {
let mut w = World::new();
let (a, b) = (w.spawn(), w.spawn());
let mut log = PassLog::default();
log.approximate_on(a, "unread", 1);
log.approximate("rounded", 2);
let mut other = PassLog::default();
other.approximate_on(b, "unread", 3);
log.absorb(other);
assert_eq!(
log.approximations,
BTreeMap::from([("rounded".to_owned(), 2), ("unread".to_owned(), 4)])
);
assert_eq!(
log.approximated_on,
BTreeMap::from([("unread".to_owned(), BTreeSet::from([a, b]))])
);
}
fn cull_world() -> World {
let mut w = world();
let kept = w.spawn();
w.insert(kept, Mesh(2));
w.insert(kept, File(2));
w
}
fn cull(opted: bool, strip: bool, w: &mut World) -> Result<RunReport> {
let mut report = RunReport::default();
let pipeline = Pipeline {
passes: vec![Box::new(Cull { opted, strip })],
};
PassManager::new(PassRegistry::new()).run(w, &pipeline, &mut report)?;
Ok(report)
}
#[test]
fn a_despawning_pass_may_take_names_only_despawned_entities_held() {
let err = cull(false, false, &mut cull_world()).unwrap_err();
assert_eq!(
err.to_string(),
"pass cull removed `[\"game.thing\", \"toy.layered\"]` without declaring it"
);
let mut w = cull_world();
let report = cull(true, false, &mut w).unwrap();
assert_eq!(report.steps[0].removed, ["game.thing", "toy.layered"]);
assert_eq!(w.component_names(), ["toy.file", "toy.mesh"]);
let err = cull(true, true, &mut cull_world()).unwrap_err();
assert_eq!(
err.to_string(),
"pass cull removed `[\"toy.file\"]` without declaring it"
);
struct Adds;
impl Pass for Adds {
fn name(&self) -> &'static str {
"adds"
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
let e = w.spawn();
w.insert(e, Pbr(1));
Ok(())
}
}
let pipeline = Pipeline {
passes: vec![Box::new(Adds)],
};
let err = PassManager::new(PassRegistry::new())
.run(&mut world(), &pipeline, &mut RunReport::default())
.unwrap_err();
assert!(matches!(err, Error::Undeclared { what: "added", .. }), "{err}");
}
#[derive(Clone, Debug)]
struct Env;
impl crate::world::Resource for Env {
const NAME: &'static str = "toy.env";
}
struct EnvPass {
add: bool,
declared: bool,
despawns: bool,
}
impl Pass for EnvPass {
fn name(&self) -> &'static str {
"env"
}
fn provides(&self) -> &[&'static str] {
if self.add && self.declared {
&["toy.env"]
} else {
&[]
}
}
fn removes(&self) -> &[&'static str] {
if !self.add && self.declared {
&["toy.env"]
} else {
&[]
}
}
fn despawns_entities(&self) -> bool {
self.despawns
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
if self.add {
w.insert_resource(Env);
} else {
w.remove_resource::<Env>();
}
Ok(())
}
}
fn env_run(pass: EnvPass, w: &mut World) -> Result<RunReport> {
let mut report = RunReport::default();
PassManager::new(PassRegistry::new()).with_pass_checks(true).run(
w,
&Pipeline {
passes: vec![Box::new(pass)],
},
&mut report,
)?;
Ok(report)
}
#[test]
fn resources_are_checked_by_the_same_declarations() {
let err = env_run(
EnvPass {
add: true,
declared: false,
despawns: false,
},
&mut world(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"pass env added `[\"toy.env\"]` without declaring it"
);
let mut w = world();
let report = env_run(
EnvPass {
add: true,
declared: true,
despawns: false,
},
&mut w,
)
.unwrap();
assert_eq!(report.steps[0].added, ["toy.env"]);
let err = env_run(
EnvPass {
add: true,
declared: false,
despawns: false,
},
&mut w,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"pass env written (resources) `[\"toy.env\"]` without declaring it"
);
let report = env_run(
EnvPass {
add: true,
declared: true,
despawns: false,
},
&mut w,
)
.unwrap();
assert!(report.steps[0].added.is_empty());
let err = env_run(
EnvPass {
add: false,
declared: false,
despawns: true,
},
&mut w,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"pass env removed `[\"toy.env\"]` without declaring it"
);
w.insert_resource(Env);
let report = env_run(
EnvPass {
add: false,
declared: true,
despawns: false,
},
&mut w,
)
.unwrap();
assert_eq!(report.steps[0].removed, ["toy.env"]);
struct NeedsEnv;
impl Pass for NeedsEnv {
fn name(&self) -> &'static str {
"needs-env"
}
fn requires(&self) -> &[&'static str] {
&["toy.env"]
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Ok(())
}
}
let needs = || Pipeline {
passes: vec![Box::new(NeedsEnv)],
};
let m = PassManager::new(PassRegistry::new());
assert!(matches!(
m.run(&mut world(), &needs(), &mut RunReport::default()),
Err(Error::Missing { .. })
));
let mut w = world();
w.insert_resource(Env);
m.run(&mut w, &needs(), &mut RunReport::default()).unwrap();
}
struct Evens;
impl Pass for Evens {
fn name(&self) -> &'static str {
"evens"
}
fn requires(&self) -> &[&'static str] {
&["toy.mesh"]
}
fn modifies(&self) -> &[&'static str] {
&["toy.mesh"]
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
if let Some(mut c) = w.column_mut::<Mesh>() {
for (_, m) in c.iter_mut() {
m.0 += m.0 % 2;
}
}
Ok(())
}
}
const EVENS: PassSpec = PassSpec {
name: "evens",
options: &[],
make: |_| Ok(Box::new(Evens)),
};
fn mesh_world(n: u32) -> World {
let mut w = World::new();
let e = w.spawn();
w.insert(e, Mesh(n));
w
}
#[test]
fn a_stated_pass_that_rewrites_in_place_fixes_failing_values() {
let even = Target::new("even").dynamic_typed::<Mesh>(|m| m.0 % 2 == 0);
let mut r = PassRegistry::new();
r.add(&EVENS);
let m = PassManager::new(r);
let evens = m.registry.parse("evens").unwrap();
let mut w = mesh_world(3);
let report = m.convert(&mut w, &evens, &even, Mode::Partial).unwrap();
assert_eq!(report.pipeline, "evens");
assert_eq!(w.iter::<Mesh>().map(|(_, m)| m.0).collect::<Vec<_>>(), [4]);
m.convert(&mut mesh_world(2), &Pipeline::default(), &even, Mode::Partial)
.unwrap();
let err = m
.convert(&mut mesh_world(3), &Pipeline::default(), &even, Mode::Partial)
.unwrap_err();
assert_eq!(
err.to_string(),
"`even` does not accept `toy.mesh` on [e0] after the pipeline: nothing in the stated pipeline removes or \
rewrites it"
);
}
struct Optional {
lowers: bool,
}
impl Pass for Optional {
fn name(&self) -> &'static str {
"optional"
}
fn options(&self) -> Options {
if self.lowers {
Options::from([("lowers".to_owned(), "true".to_owned())])
} else {
Options::new()
}
}
fn removes(&self) -> &[&'static str] {
if self.lowers { &["toy.layered"] } else { &[] }
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
if !self.lowers {
return Ok(());
}
let es: Vec<Entity> = w.iter::<Layered>().map(|(e, _)| e).collect();
for e in es {
w.remove::<Layered>(e);
}
Ok(())
}
}
#[test]
fn a_stated_pass_declares_by_its_options() {
let mut r = PassRegistry::new();
const OPTIONAL: PassSpec = PassSpec {
name: "optional",
options: &[OptionSpec::optional(
"lowers",
"true",
"true: it removes toy.layered",
"it removes nothing",
)],
make: |g| {
Ok(Box::new(Optional {
lowers: g.optional("lowers").is_some_and(|v| v == "true"),
}))
},
};
r.add(&OPTIONAL);
let m = PassManager::new(r);
let t = Target::new("t").illegal("toy.layered");
let mut w = World::new();
let e = w.spawn();
w.insert(e, Layered(1));
let err = m
.convert(
&mut w.clone(),
&m.registry.parse("optional").unwrap(),
&t,
Mode::Full,
)
.unwrap_err();
assert!(
matches!(&err, Error::Illegal { lowered_by, .. } if lowered_by.is_empty()),
"{err}"
);
let report = m
.convert(
&mut w,
&m.registry.parse("optional{lowers=true}").unwrap(),
&t,
Mode::Full,
)
.unwrap();
assert_eq!(report.pipeline, "optional{lowers=true}");
}
struct Decl {
name: &'static str,
requires: &'static [&'static str],
modifies: &'static [&'static str],
}
impl Pass for Decl {
fn name(&self) -> &'static str {
self.name
}
fn requires(&self) -> &[&'static str] {
self.requires
}
fn modifies(&self) -> &[&'static str] {
self.modifies
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Ok(())
}
}
#[test]
fn values_a_stated_pass_produces_are_checked_after_the_pipeline() {
let mut r = PassRegistry::new();
r.add(&LOAD);
r.add(&EVENS);
let m = PassManager::new(r);
let t = Target::new("even")
.illegal("toy.file")
.dynamic_typed::<Mesh>(|m| m.0 % 2 == 0);
let mut w = w_with_file(3);
let e = w.entities().next().unwrap();
let report = m
.convert(
&mut w,
&m.registry.parse("load, evens").unwrap(),
&t,
Mode::Partial,
)
.unwrap();
assert_eq!(report.pipeline, "load, evens");
assert_eq!(w.get::<Mesh>(e), Some(&Mesh(4)));
let load = m.registry.parse("load").unwrap();
m.convert(&mut w_with_file(2), &load, &t, Mode::Partial).unwrap();
let err = m
.convert(&mut w_with_file(5), &load, &t, Mode::Partial)
.unwrap_err();
assert!(
matches!(&err, Error::Illegal { name, lowered_by, .. } if name == "toy.mesh" && lowered_by.is_empty()),
"{err}"
);
}
fn w_with_file(n: u32) -> World {
let mut w = World::new();
let e = w.spawn();
w.insert(e, File(n));
w
}
#[derive(Clone, Debug, PartialEq)]
struct Table(Vec<Option<u8>>);
impl crate::world::Resource for Table {
const NAME: &'static str = "toy.table";
}
struct Classify;
impl Pass for Classify {
fn name(&self) -> &'static str {
"classify"
}
fn requires(&self) -> &[&'static str] {
&["toy.table"]
}
fn modifies(&self) -> &[&'static str] {
&["toy.table"]
}
fn settles(&self) -> &[&'static str] {
&["toy.table"]
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
if let Some(t) = w.resource_mut::<Table>() {
for row in t.0.iter_mut().filter(|r| r.is_none()) {
*row = Some(0);
log.count("rows classified", 1);
}
}
Ok(())
}
}
struct AddRow;
impl Pass for AddRow {
fn name(&self) -> &'static str {
"add-row"
}
fn provides(&self) -> &[&'static str] {
&["toy.table"]
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
match w.resource_mut::<Table>() {
Some(t) => t.0.push(None),
None => {
w.insert_resource(Table(vec![None]));
}
}
Ok(())
}
}
fn classified() -> Target {
Target::new("t").check_resource::<Table>("every row classified", |t| t.0.iter().all(Option::is_some))
}
fn table_world(rows: Vec<Option<u8>>) -> World {
let mut w = World::new();
w.insert_resource(Table(rows));
w
}
#[test]
fn a_resource_value_rule_is_checked_after_the_pipeline() {
let mut r = PassRegistry::new();
const CLASSIFY: PassSpec = PassSpec {
name: "classify",
options: &[],
make: |_| Ok(Box::new(Classify)),
};
r.add(&CLASSIFY);
let m = PassManager::new(r);
let mut w = table_world(vec![Some(3), None]);
let classify = m.registry.parse("classify").unwrap();
let report = m
.convert(&mut w, &classify, &classified(), Mode::Partial)
.unwrap();
assert_eq!(report.pipeline, "classify");
assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(3), Some(0)])));
m.convert(
&mut table_world(vec![Some(1)]),
&Pipeline::default(),
&classified(),
Mode::Partial,
)
.unwrap();
let err = m
.convert(
&mut table_world(vec![None]),
&Pipeline::default(),
&classified(),
Mode::Partial,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"`t` does not accept `toy.table` (every row classified) after the pipeline: nothing in the stated pipeline \
removes or rewrites it"
);
assert_eq!(classified().rule_words("toy.table"), Some("every row classified"));
}
#[test]
fn a_covered_name_reports_only_its_uncovered_entities() {
let mut w = World::new();
let covered = w.spawn();
w.insert(covered, File(1));
w.insert(covered, Pbr(1));
let bare = w.spawn();
w.insert(bare, File(2));
let t = Target::new("cover")
.legal(Pbr::NAME)
.covered(File::NAME, "covered by toy.pbr", |w, e| w.has::<Pbr>(e));
assert_eq!(
t.check_prepared(&w).unsupported,
vec![("toy.file not covered by toy.pbr".to_owned(), 1)]
);
assert_eq!(t.rule_words(File::NAME), Some("covered by toy.pbr"));
assert!(t.illegal_entities(&w, File::NAME, Mode::Partial).is_empty());
assert_eq!(t.illegal_entities(&w, File::NAME, Mode::Full), vec![bare]);
w.insert(bare, Pbr(2));
assert!(t.check_prepared(&w).unsupported.is_empty());
}
#[test]
fn a_refused_value_names_its_rule_and_the_passes_that_left_it() {
let mut r = PassRegistry::new();
const LAZY: PassSpec = PassSpec {
name: "lazy",
options: &[],
make: |_| {
Ok(Box::new(Decl {
name: "lazy",
requires: &["toy.mesh"],
modifies: &["toy.mesh"],
}))
},
};
r.add(&LAZY);
let m = PassManager::new(r);
let t = Target::new("even").check::<Mesh>("even meshes", |m| m.0 % 2 == 0);
let err = m
.convert(
&mut mesh_world(3),
&m.registry.parse("lazy").unwrap(),
&t,
Mode::Partial,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"`even` does not accept `toy.mesh` (even meshes) on [e0] after the pipeline: lazy left it"
);
assert!(matches!(err, Error::Illegal { ref rule, .. } if rule == "even meshes"));
}
#[test]
fn a_pass_that_provides_a_settled_name_after_it_refuses_the_pipeline() {
let mut r = PassRegistry::new();
const CLASSIFY: PassSpec = PassSpec {
name: "classify",
options: &[],
make: |_| Ok(Box::new(Classify)),
};
r.add(&CLASSIFY);
const ADD_ROW: PassSpec = PassSpec {
name: "add-row",
options: &[],
make: |_| Ok(Box::new(AddRow)),
};
r.add(&ADD_ROW);
let m = PassManager::new(r);
let mut w = table_world(vec![None]);
let err = m
.run(
&mut w,
&m.registry.parse("classify, add-row").unwrap(),
&mut RunReport::default(),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"add-row provides `toy.table` after classify settled it: its values would not be settled"
);
assert_eq!(w.resource::<Table>(), Some(&Table(vec![None])), "nothing ran");
let mut w = table_world(vec![None]);
m.run(
&mut w,
&m.registry.parse("add-row, classify").unwrap(),
&mut RunReport::default(),
)
.unwrap();
assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(0), Some(0)])));
let mut w = table_world(vec![None]);
m.run(
&mut w,
&m.registry.parse("classify, add-row, classify").unwrap(),
&mut RunReport::default(),
)
.unwrap();
assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(0), Some(0)])));
}
#[test]
fn validators_run_with_the_verifier() {
struct NoPbr;
impl Validator for NoPbr {
fn name(&self) -> &'static str {
"no-pbr"
}
fn check(&self, w: &World) -> Vec<(Option<Entity>, String)> {
w.iter::<Pbr>()
.map(|(e, _)| (Some(e), "pbr is banned here".to_owned()))
.collect()
}
}
let mut m = PassManager::new(registry()).with_pass_checks(true);
m.add_validator(Box::new(NoPbr));
let pipeline = m.registry.parse("load, bake").unwrap();
let err = m
.convert(&mut world(), &pipeline, &gltf(), Mode::Partial)
.unwrap_err();
assert_eq!(
err.to_string(),
"verify after bake: no-pbr on e0: pbr is banned here"
);
}
#[test]
fn the_verifier_runs_after_passes_with_the_pass_checks() {
struct NoPbr;
impl Validator for NoPbr {
fn name(&self) -> &'static str {
"no-pbr"
}
fn check(&self, w: &World) -> Vec<(Option<Entity>, String)> {
w.iter::<Pbr>()
.map(|(e, _)| (Some(e), "pbr is banned here".to_owned()))
.collect()
}
}
assert_eq!(PassManager::new(registry()).pass_checks(), cfg!(debug_assertions));
for checks in [true, false] {
let mut m = PassManager::new(registry()).with_pass_checks(checks);
m.add_validator(Box::new(NoPbr));
let pipeline = m.registry.parse("load, bake").unwrap();
let ran = m.run(&mut world(), &pipeline, &mut RunReport::default());
if checks {
assert_eq!(
ran.unwrap_err().to_string(),
"verify after bake: no-pbr on e0: pbr is banned here"
);
} else {
assert!(ran.is_ok());
}
}
}
struct Threaded;
impl Pass for Threaded {
fn name(&self) -> &'static str {
"threaded"
}
fn may_lack_inputs(&self) -> bool {
true
}
fn requires(&self) -> &[&'static str] {
&["toy.layered"]
}
fn settings(&self) -> &[Setting] {
&[crate::parallel::THREADS]
}
fn approximates(&self) -> &'static str {
""
}
fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
let threads = crate::parallel::threads(w).map_err(|e| e.to_string())?;
log.count("threads", threads.get() as u64);
Ok(())
}
}
fn threads(n: usize) -> Settings {
Settings {
threads: primase_foundations::parallel::Threads::new(n).unwrap(),
}
}
#[test]
fn the_manager_states_the_programs_settings_and_the_report_says_them() {
let manager = PassManager::new(PassRegistry::new()).with_settings(threads(3));
let mut w = world();
let mut report = RunReport::default();
let pipeline = Pipeline {
passes: vec![Box::new(Threaded)],
};
manager.run(&mut w, &pipeline, &mut report).unwrap();
assert_eq!(report.steps[0].log.counts["threads"], 3);
assert_eq!(report.steps[0].settings, ["threads 3"]);
assert!(report.to_string().contains("; threads 3)"), "{report}");
let mut other = report.clone();
other.steps[0].settings.clear();
assert_eq!(report, other, "how a step ran is not what it made");
manager.run(&mut w, &pipeline, &mut RunReport::default()).unwrap();
}
#[test]
fn a_setting_the_program_does_not_state_refuses_the_pass_but_never_decides_a_skip() {
let manager = PassManager::new(PassRegistry::new());
let pipeline = Pipeline {
passes: vec![Box::new(Threaded)],
};
let refused = manager
.run(&mut world(), &pipeline, &mut RunReport::default())
.unwrap_err();
assert!(
matches!(&refused, Error::Missing { pass, name } if pass == "threaded" && name == "core.threads"),
"{refused}"
);
let mut report = RunReport::default();
manager.run(&mut World::new(), &pipeline, &mut report).unwrap();
assert_eq!(report.steps[0].skipped, ["toy.layered"]);
assert!(report.steps[0].settings.is_empty());
}
#[test]
fn a_world_stating_another_count_than_the_program_is_refused() {
let manager = PassManager::new(PassRegistry::new()).with_settings(threads(3));
let mut w = world();
w.insert_resource(primase_foundations::parallel::Threads::new(2).unwrap());
let pipeline = Pipeline {
passes: vec![Box::new(Threaded)],
};
let refused = manager
.run(&mut w, &pipeline, &mut RunReport::default())
.unwrap_err();
assert_eq!(
refused.to_string(),
"the world states `core.threads` as threads 2, the program as threads 3"
);
}
}