use serde::Serialize;
use serde_json::Value;
use crate::render_graph::{RenderGraph, SceneMembers, ScenePlacements};
use crate::shader::ShaderBinding;
use crate::{Component, Entity, Instance, JsonError, Mesh, World};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct StepBinding {
pub draw: String,
pub pass: Option<String>,
pub program: StepProgram,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StepProgram {
Main,
Bound(ShaderBinding),
Unbound { why: String },
}
impl StepBinding {
pub fn binding<'a>(&'a self, material: Entity, w: &'a World) -> Option<&'a ShaderBinding> {
match &self.program {
StepProgram::Main => w.get::<ShaderBinding>(material),
StepProgram::Bound(b) => Some(b),
StepProgram::Unbound { .. } => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct StepBindings(pub Vec<StepBinding>);
impl StepBindings {
pub fn of<'a>(&'a self, draw: &'a str) -> impl Iterator<Item = &'a StepBinding> + 'a {
self.0.iter().filter(move |b| b.draw == draw)
}
}
impl Component for StepBindings {
const NAME: &'static str = "core.step_bindings";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn references(&self, out: &mut Vec<Entity>) {
for b in &self.0 {
if let StepProgram::Bound(binding) = &b.program {
binding.references(out);
}
}
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
if self.0.is_empty() {
return Err("step bindings that bind no draw".into());
}
let draws: Option<Vec<(&str, &SceneMembers)>> = w.resource::<RenderGraph>().map(|g| g.scene_draws());
for (i, b) in self.0.iter().enumerate() {
if b.draw.trim().is_empty() {
return Err("a step binding of no draw".into());
}
if self.0[..i]
.iter()
.any(|o| o.draw == b.draw && (o.pass.is_none() || o.pass == b.pass))
{
return Err(format!(
"the draw {} bound twice{}",
b.draw,
b.pass
.as_deref()
.map(|p| format!(" through the pass {p}"))
.unwrap_or_default()
));
}
if b.pass.is_none() && self.0.iter().filter(|o| o.draw == b.draw).count() > 1 {
return Err(format!(
"the draw {} bound several times, not each through a named pass",
b.draw
));
}
if let Some(draws) = &draws {
match draws.iter().find(|(d, _)| *d == b.draw).map(|(_, m)| m) {
None => {
return Err(format!(
"the draw {} is no scene step's of the world's core.render_graph",
b.draw
));
}
Some(SceneMembers::NotRead { .. }) => {
return Err(format!(
"the draw {} is a scene step's of the world's core.render_graph that does not read what \
it draws",
b.draw
));
}
Some(SceneMembers::Stated { .. }) => {}
}
}
if b.pass.as_deref().is_some_and(|p| p.trim().is_empty()) {
return Err(format!("the draw {}'s pass has an empty name", b.draw));
}
match &b.program {
StepProgram::Main if !w.has::<ShaderBinding>(e) => {
return Err(format!(
"the draw {} draws the material's main binding, and it has no core.shader_binding",
b.draw
));
}
StepProgram::Main => {}
StepProgram::Bound(binding) => binding
.verify(e, w)
.map_err(|why| format!("the draw {}: {why}", b.draw))?,
StepProgram::Unbound { why } if why.trim().is_empty() => {
return Err(format!("the draw {} is unbound and says not why", b.draw));
}
StepProgram::Unbound { .. } => {}
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StepDraws(pub Vec<String>);
impl StepDraws {
pub fn draws_in(&self, draw: &str) -> bool {
self.0.iter().any(|d| d == draw)
}
}
impl Component for StepDraws {
const NAME: &'static str = "core.step_draws";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
if !w.has::<Instance>(e) && !w.has::<Mesh>(e) {
return Err("step draws on an entity that draws nothing (no core.instance or core.mesh)".into());
}
if self.0.is_empty() {
return Err("step draws that name no draw".into());
}
let listed: Option<Vec<&str>> = w.resource::<RenderGraph>().map(|g| {
g.scene_draws()
.into_iter()
.filter(|(_, m)| {
**m == SceneMembers::Stated {
placements: ScenePlacements::Listed,
}
})
.map(|(d, _)| d)
.collect()
});
for (i, d) in self.0.iter().enumerate() {
if self.0[..i].contains(d) {
return Err(format!("the draw {d} named twice"));
}
if let Some(listed) = &listed
&& !listed.contains(&d.as_str())
{
return Err(format!(
"the draw {d} is no scene step's of the world's core.render_graph that lists its placements"
));
}
}
Ok(())
}
}
pub(crate) fn retain_made(w: &mut World) -> crate::Dropped {
let Some(graph) = w.resource::<RenderGraph>() else {
return crate::Dropped::default();
};
let made: std::collections::BTreeSet<String> = graph
.scene_draws()
.into_iter()
.map(|(d, _)| d.to_owned())
.collect();
let stale_bindings: Vec<Entity> = w
.iter::<StepBindings>()
.filter(|(e, _)| w.is_alive(*e))
.filter(|(_, b)| b.0.iter().any(|b| !made.contains(&b.draw)))
.map(|(e, _)| e)
.collect();
let stale_draws: Vec<Entity> = w
.iter::<StepDraws>()
.filter(|(e, _)| w.is_alive(*e))
.filter(|(_, d)| d.0.iter().any(|d| !made.contains(d)))
.map(|(e, _)| e)
.collect();
let mut bindings = Vec::new();
for e in stale_bindings {
let Some(mut b) = w.remove::<StepBindings>(e) else {
continue;
};
b.0.retain(|b| {
let keep = made.contains(&b.draw);
if !keep {
bindings.push((e, b.draw.clone()));
}
keep
});
if !b.0.is_empty() {
w.insert(e, b);
}
}
let mut draws = Vec::new();
for e in stale_draws {
let Some(mut d) = w.remove::<StepDraws>(e) else {
continue;
};
d.0.retain(|d| {
let keep = made.contains(d);
if !keep {
draws.push((e, d.clone()));
}
keep
});
if !d.0.is_empty() {
w.insert(e, d);
}
}
crate::Dropped {
step_bindings: bindings,
step_draws: draws,
..crate::Dropped::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_graph::{
DepthView, DrawBucket, DrawSorting, Outputs, PassWork, Presence, RenderPass, RenderTarget, Rounding,
Runs, SceneView, TargetFormat, TargetShape, TargetSize, TargetView,
};
fn graph(draws: &[(&str, SceneMembers)]) -> RenderGraph {
RenderGraph {
targets: vec![RenderTarget {
name: "depth".into(),
format: TargetFormat::Depth32Stencil8,
size: TargetSize::View {
numerator: 1,
denominator: 1,
depth: 1,
rounding: Rounding::Down,
},
shape: TargetShape::Plane,
mips: 1,
history: false,
shared: false,
}],
buffers: Vec::new(),
passes: draws
.iter()
.map(|(draw, members)| RenderPass {
name: (*draw).into(),
shared: false,
presence: Presence::Always,
runs: Runs::Once,
work: PassWork::Scene {
bucket: DrawBucket::Forward,
view: SceneView::Camera,
outputs: Outputs {
colour: Vec::new(),
depth: Some(DepthView {
view: TargetView {
target: 0,
mip: 0,
first_layer: 0,
layers: 0,
},
read_only_depth: false,
read_only_stencil: false,
}),
},
draw: (*draw).into(),
members: members.clone(),
frame: Vec::new(),
sorting: DrawSorting::NotStated {
why: "a fixture".into(),
},
},
})
.collect(),
picture: None,
lighting: None,
local_shadow_atlas: None,
left_out: Vec::new(),
}
}
#[test]
fn step_bindings_and_draws_verify_their_own_rules() {
let mut w = World::new();
let material = w.spawn();
let bound = |draw: &str, program| StepBinding {
draw: draw.into(),
pass: Some("DepthNormals".into()),
program,
};
let unbound = |draw: &str, why: &str| bound(draw, StepProgram::Unbound { why: why.into() });
assert!(
StepBindings(vec![unbound("depth_normals", "not read")])
.verify(material, &w)
.is_ok()
);
let err = |b: StepBindings, w: &World| b.verify(material, w).unwrap_err();
assert!(err(StepBindings(Vec::new()), &w).contains("no draw"));
assert!(err(StepBindings(vec![unbound("a", "x"), unbound("a", "y")]), &w).contains("twice"));
let through = |pass: Option<&str>| StepBinding {
draw: "a".into(),
pass: pass.map(str::to_owned),
program: StepProgram::Unbound { why: "x".into() },
};
let two = StepBindings(vec![
through(Some("DepthNormals")),
through(Some("DepthNormalsOnly")),
]);
assert!(two.verify(material, &w).is_ok());
assert_eq!(two.of("a").count(), 2);
assert!(
err(
StepBindings(vec![through(Some("DepthNormals")), through(None)]),
&w
)
.contains("named pass")
);
assert!(
err(
StepBindings(vec![through(None), through(Some("DepthNormals"))]),
&w
)
.contains("named pass")
);
assert!(err(StepBindings(vec![unbound("a", " ")]), &w).contains("not why"));
assert!(err(StepBindings(vec![bound("a", StepProgram::Main)]), &w).contains("core.shader_binding"));
w.insert(material, ShaderBinding::default());
assert!(
StepBindings(vec![bound("a", StepProgram::Main)])
.verify(material, &w)
.is_ok()
);
assert_eq!(
StepBindings(vec![bound("a", StepProgram::Main)]).0[0].binding(material, &w),
Some(&ShaderBinding::default())
);
let stated = |placements| SceneMembers::Stated { placements };
w.insert_resource(graph(&[
("forward", stated(ScenePlacements::All)),
("depth_normals", stated(ScenePlacements::Listed)),
(
"velocity",
SceneMembers::NotRead {
why: "a fixture".into(),
},
),
]));
assert!(
StepBindings(vec![bound("forward", StepProgram::Main)])
.verify(material, &w)
.is_ok()
);
assert!(err(StepBindings(vec![bound("a", StepProgram::Main)]), &w).contains("no scene step's"));
assert!(err(StepBindings(vec![unbound("velocity", "x")]), &w).contains("does not read"));
let placement = w.spawn();
let draws = |d: &[&str]| StepDraws(d.iter().map(|s| (*s).to_owned()).collect());
assert!(
draws(&["depth_normals"])
.verify(placement, &w)
.unwrap_err()
.contains("draws nothing")
);
let prototype = w.spawn();
w.insert(
placement,
Instance {
prototype,
variant: 0,
},
);
assert!(draws(&["depth_normals"]).verify(placement, &w).is_ok());
assert!(draws(&["depth_normals"]).draws_in("depth_normals"));
assert!(draws(&[]).verify(placement, &w).unwrap_err().contains("no draw"));
assert!(
draws(&["depth_normals", "depth_normals"])
.verify(placement, &w)
.unwrap_err()
.contains("twice")
);
assert!(
draws(&["forward"])
.verify(placement, &w)
.unwrap_err()
.contains("lists its placements")
);
}
fn unbound(draw: &str) -> StepBinding {
StepBinding {
draw: draw.into(),
pass: None,
program: StepProgram::Unbound {
why: "a fixture".into(),
},
}
}
fn grouped() -> (World, [Entity; 4], RenderPass) {
use iridia_interface::Stage;
let mut w = World::new();
let program = w.spawn();
let (lit, depth_only, placement) = (w.spawn(), w.spawn(), w.spawn());
let stated = |placements| SceneMembers::Stated { placements };
let mut g = graph(&[
("forward", stated(ScenePlacements::All)),
("depth_normals", stated(ScenePlacements::Listed)),
]);
let depth_step = g.passes.remove(1);
let screen = RenderPass {
name: "resolve".into(),
shared: false,
presence: Presence::Always,
runs: Runs::Once,
work: PassWork::Screen {
vertices: 3,
stages: vec![crate::shader::StageBinding::new(Stage::Pixel, program)],
state: Default::default(),
outputs: Outputs {
colour: Vec::new(),
depth: Some(DepthView {
view: TargetView {
target: 0,
mip: 0,
first_layer: 0,
layers: 0,
},
read_only_depth: false,
read_only_stencil: false,
}),
},
region: None,
},
};
g.passes.push(RenderPass {
name: "depth group".into(),
shared: false,
presence: Presence::Always,
runs: Runs::Once,
work: PassWork::Group {
steps: vec![screen, depth_step],
},
});
g.picture = Some(0);
let group = g.passes[1].clone();
w.insert_resource(g);
w.insert(
lit,
StepBindings(vec![unbound("forward"), unbound("depth_normals")]),
);
w.insert(depth_only, StepBindings(vec![unbound("depth_normals")]));
w.insert(placement, Mesh::new(Vec::new()));
w.insert(placement, StepDraws(vec!["depth_normals".into()]));
(w, [program, lit, depth_only, placement], group)
}
#[test]
fn a_part_drops_the_steps_reaching_outside_it_and_the_bindings_naming_their_draws() {
use crate::render_graph::LightingPipeline;
let (w, [program, lit, depth_only, placement], group) = grouped();
let stated = |placements| SceneMembers::Stated { placements };
let keep = std::collections::BTreeSet::from([lit, depth_only, placement]);
let part = w.part(&keep);
assert_eq!(
part.dropped.graph.steps,
vec![("depth group".to_owned(), program)]
);
assert!(part.dropped.graph.picture && !part.dropped.graph.lighting);
assert_eq!(
part.dropped.step_bindings,
vec![
(lit, "depth_normals".to_owned()),
(depth_only, "depth_normals".to_owned())
]
);
assert_eq!(
part.dropped.step_draws,
vec![(placement, "depth_normals".to_owned())]
);
let p = &part.world;
assert_eq!(p.verify_components(), Vec::new());
assert_eq!(p.verify_resources(), Vec::new());
assert_eq!(
p.get::<StepBindings>(lit),
Some(&StepBindings(vec![unbound("forward")]))
);
assert!(!p.has::<StepBindings>(depth_only) && !p.has::<StepDraws>(placement));
let kept = p.resource::<RenderGraph>().unwrap();
assert_eq!((kept.passes.len(), kept.picture), (1, None));
assert!(w.part(&w.entities().collect()).dropped.is_empty());
let panicked =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| w.subset(&keep))).unwrap_err();
let message = panicked.downcast_ref::<String>().unwrap();
assert!(
message.contains("depth group")
&& message.contains("step bindings")
&& message.contains("depth_normals"),
"{message}"
);
let mut despawned = w.clone();
despawned.despawn(program);
assert_eq!(despawned.take_dropped(), part.dropped);
assert_eq!(despawned.verify_components(), Vec::new());
assert!(despawned.take_dropped().is_empty());
let mut split = w.clone();
let plain = split.spawn();
split.insert(plain, Mesh::new(Vec::new()));
let mut moved = split.clone();
let counted = moved.split_part(&std::collections::BTreeSet::from([plain]));
assert_eq!(counted.dropped.graph.steps, part.dropped.graph.steps);
assert!(counted.dropped.step_bindings.is_empty() && counted.dropped.step_draws.is_empty());
let off = split.split_off(&std::collections::BTreeSet::from([plain]));
assert_eq!(split.resource::<RenderGraph>().map(|g| g.passes.len()), Some(2));
split.absorb(off).unwrap();
let panicked =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| split.split_off(&keep))).unwrap_err();
let message = panicked.downcast_ref::<String>().unwrap();
assert!(
message.contains("split_off stripped") && message.contains("depth_normals"),
"{message}"
);
let mut g = graph(&[
("a", stated(ScenePlacements::All)),
("b", stated(ScenePlacements::All)),
]);
g.passes.insert(0, group);
g.lighting = Some(LightingPipeline {
passes: vec![2],
..LightingPipeline::default()
});
let mut renumbered = g.clone();
assert_eq!(renumbered.retain_steps(&|e| e != program).steps.len(), 1);
assert_eq!(renumbered.lighting.map(|l| l.passes), Some(vec![1]));
g.lighting = Some(LightingPipeline {
passes: vec![0, 2],
..LightingPipeline::default()
});
assert!(g.retain_steps(&|e| e != program).lighting);
assert_eq!(g.lighting, None);
}
#[test]
fn a_run_records_what_the_world_dropped_outside_its_passes() {
use crate::pass::{Pass, PassLog, PassManager, PassRegistry, Pipeline, RunReport};
struct Despawns(Entity);
impl Pass for Despawns {
fn name(&self) -> &'static str {
"despawn-program"
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
w.despawn(self.0);
Ok(())
}
}
struct Nothing;
impl Pass for Nothing {
fn name(&self) -> &'static str {
"nothing"
}
fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
Ok(())
}
}
let manager = PassManager::new(PassRegistry::new()).with_pass_checks(true);
let steps = "render graph steps dropped: they drew with an entity that left the world";
let bindings = "step bindings dropped: only dropped render graph steps made their draws";
let (mut w, [program, ..], _) = grouped();
let mut report = RunReport::default();
let pipeline = Pipeline {
passes: vec![Box::new(Despawns(program))],
};
manager.run(&mut w, &pipeline, &mut report).unwrap();
let counts = &report.steps[0].log.counts;
assert_eq!((counts.get(steps), counts.get(bindings)), (Some(&1), Some(&2)));
assert!(report.dropped_outside.is_empty());
let (mut w, [program, ..], _) = grouped();
w.despawn(program);
let mut report = RunReport::default();
let pipeline = Pipeline {
passes: vec![Box::new(Nothing)],
};
manager.run(&mut w, &pipeline, &mut report).unwrap();
assert_eq!(report.dropped_outside.len(), 1);
let (next, dropped) = &report.dropped_outside[0];
assert_eq!(
(
next.as_str(),
dropped.graph.steps.len(),
dropped.step_bindings.len()
),
("nothing", 1, 2)
);
assert!(!report.steps[0].log.counts.contains_key(steps));
assert!(
report
.to_string()
.contains("outside any pass, before nothing: dropped render graph steps depth group")
);
}
}