Skip to main content

primase_core/
step_bindings.rs

1//! Which of the frame's scene steps draw a surface, and through which program: `core.step_bindings` (on materials)
2//! and `core.step_draws` (on placements).
3//!
4//! A frame's scene steps ([`crate::render_graph::PassWork::Scene`]) each make a draw the source names (Unity's
5//! `LightMode` passes of the step, Sekiro's buckets), and a renderer draws a surface in some of them, each time
6//! through a program of its material: its main draw's (`core.shader_binding`), or another pass of it (a depth
7//! prepass draws a material through its depth pass). A source states each part where it lies:
8//!
9//! - on a material, each draw that draws it and the program it is drawn through there ([`StepBindings`]): a material
10//!   without a step binding for a draw is not drawn in it, and a draw that draws it through several of its passes (a
11//!   Unity draw draws every pass whose `LightMode` is one of the draw's) binds it once for each pass;
12//! - on a placement, the draws of the steps that pick their placements one by one ([`StepDraws`],
13//!   [`crate::render_graph::ScenePlacements::Listed`]): a renderer chooses them per drawn object (Unity by the layer of
14//!   the object's GameObject), not per material.
15//!
16//! A material a draw draws whose program the source could not bind says why ([`StepProgram::Unbound`]): the draw then
17//! draws something no binding states, and a consumer says what it does there.
18
19use serde::Serialize;
20use serde_json::Value;
21
22use crate::render_graph::{RenderGraph, SceneMembers, ScenePlacements};
23use crate::shader::ShaderBinding;
24use crate::{Component, Entity, Instance, JsonError, Mesh, World};
25
26/// One draw of the frame's scene steps that draws a material, and the program it draws it through.
27#[derive(Clone, Debug, PartialEq, Serialize)]
28pub struct StepBinding {
29    /// The draw, as the scene steps name it ([`crate::render_graph::PassWork::Scene::draw`]).
30    pub draw: String,
31    /// The source's name for the material's pass the draw goes through (Unity's `LightMode` tag of the subshader's
32    /// pass, `DepthNormalsOnly`), where the source names its passes.
33    pub pass: Option<String>,
34    pub program: StepProgram,
35}
36
37/// The program a draw draws a material through.
38#[derive(Clone, Debug, PartialEq, Serialize)]
39#[serde(rename_all = "snake_case")]
40pub enum StepProgram {
41    /// The material's main draw's, its `core.shader_binding`.
42    Main,
43    /// Another pass's, bound here and held to what a material's `core.shader_binding` is held to.
44    Bound(ShaderBinding),
45    /// The draw draws the material, and the source could not bind the program: why.
46    Unbound { why: String },
47}
48
49impl StepBinding {
50    /// The binding the draw draws the material through, where the source bound one: the material's own
51    /// `core.shader_binding` for [`StepProgram::Main`].
52    pub fn binding<'a>(&'a self, material: Entity, w: &'a World) -> Option<&'a ShaderBinding> {
53        match &self.program {
54            StepProgram::Main => w.get::<ShaderBinding>(material),
55            StepProgram::Bound(b) => Some(b),
56            StepProgram::Unbound { .. } => None,
57        }
58    }
59}
60
61/// `core.step_bindings`, on a material entity: the draws of the frame's scene steps that draw it and the program each
62/// draws it through: a draw once, or once for each pass of the material it draws, in the order it draws them.
63#[derive(Clone, Debug, Default, PartialEq, Serialize)]
64pub struct StepBindings(pub Vec<StepBinding>);
65
66impl StepBindings {
67    /// How `draw` draws the material: none where it does not, one binding for each pass it draws it through.
68    pub fn of<'a>(&'a self, draw: &'a str) -> impl Iterator<Item = &'a StepBinding> + 'a {
69        self.0.iter().filter(move |b| b.draw == draw)
70    }
71}
72
73impl Component for StepBindings {
74    const NAME: &'static str = "core.step_bindings";
75    const GEOMETRY: crate::Geometry = crate::Geometry::None;
76    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
77        crate::world::Linearity::Invariant
78    }
79    fn to_json(&self) -> Result<Option<Value>, JsonError> {
80        crate::json::to_value(self).map(Some)
81    }
82    fn references(&self, out: &mut Vec<Entity>) {
83        for b in &self.0 {
84            if let StepProgram::Bound(binding) = &b.program {
85                binding.references(out);
86            }
87        }
88    }
89    /// Not empty; a draw named once, or once for each of several passes, each named (not empty) and once; each draw
90    /// one of the scene steps' draws that state what they draw where the world has a `core.render_graph`; a main-draw
91    /// binding on a material that has its `core.shader_binding`, another pass's verified as a material's binding, and
92    /// an unbound one saying why.
93    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
94        if self.0.is_empty() {
95            return Err("step bindings that bind no draw".into());
96        }
97        let draws: Option<Vec<(&str, &SceneMembers)>> = w.resource::<RenderGraph>().map(|g| g.scene_draws());
98        for (i, b) in self.0.iter().enumerate() {
99            if b.draw.trim().is_empty() {
100                return Err("a step binding of no draw".into());
101            }
102            if self.0[..i]
103                .iter()
104                .any(|o| o.draw == b.draw && (o.pass.is_none() || o.pass == b.pass))
105            {
106                return Err(format!(
107                    "the draw {} bound twice{}",
108                    b.draw,
109                    b.pass
110                        .as_deref()
111                        .map(|p| format!(" through the pass {p}"))
112                        .unwrap_or_default()
113                ));
114            }
115            if b.pass.is_none() && self.0.iter().filter(|o| o.draw == b.draw).count() > 1 {
116                return Err(format!(
117                    "the draw {} bound several times, not each through a named pass",
118                    b.draw
119                ));
120            }
121            if let Some(draws) = &draws {
122                match draws.iter().find(|(d, _)| *d == b.draw).map(|(_, m)| m) {
123                    None => {
124                        return Err(format!(
125                            "the draw {} is no scene step's of the world's core.render_graph",
126                            b.draw
127                        ));
128                    }
129                    Some(SceneMembers::NotRead { .. }) => {
130                        return Err(format!(
131                            "the draw {} is a scene step's of the world's core.render_graph that does not read what \
132                             it draws",
133                            b.draw
134                        ));
135                    }
136                    Some(SceneMembers::Stated { .. }) => {}
137                }
138            }
139            if b.pass.as_deref().is_some_and(|p| p.trim().is_empty()) {
140                return Err(format!("the draw {}'s pass has an empty name", b.draw));
141            }
142            match &b.program {
143                StepProgram::Main if !w.has::<ShaderBinding>(e) => {
144                    return Err(format!(
145                        "the draw {} draws the material's main binding, and it has no core.shader_binding",
146                        b.draw
147                    ));
148                }
149                StepProgram::Main => {}
150                StepProgram::Bound(binding) => binding
151                    .verify(e, w)
152                    .map_err(|why| format!("the draw {}: {why}", b.draw))?,
153                StepProgram::Unbound { why } if why.trim().is_empty() => {
154                    return Err(format!("the draw {} is unbound and says not why", b.draw));
155                }
156                StepProgram::Unbound { .. } => {}
157            }
158        }
159        Ok(())
160    }
161}
162
163/// `core.step_draws`, on a placement (a `core.instance`) or an entity that draws its own mesh: the draws of the scene
164/// steps that pick their placements one by one ([`ScenePlacements::Listed`]) that draw it, each once. A step whose
165/// placements are listed draws no placement without it.
166#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
167pub struct StepDraws(pub Vec<String>);
168
169impl StepDraws {
170    /// Whether the steps making `draw` draw it.
171    pub fn draws_in(&self, draw: &str) -> bool {
172        self.0.iter().any(|d| d == draw)
173    }
174}
175
176impl Component for StepDraws {
177    const NAME: &'static str = "core.step_draws";
178    const GEOMETRY: crate::Geometry = crate::Geometry::None;
179    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
180        crate::world::Linearity::Invariant
181    }
182    fn to_json(&self) -> Result<Option<Value>, JsonError> {
183        crate::json::to_value(self).map(Some)
184    }
185    /// On something drawn; not empty, each draw once, and the draw of a scene step that lists its placements where the
186    /// world has a `core.render_graph`.
187    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
188        if !w.has::<Instance>(e) && !w.has::<Mesh>(e) {
189            return Err("step draws on an entity that draws nothing (no core.instance or core.mesh)".into());
190        }
191        if self.0.is_empty() {
192            return Err("step draws that name no draw".into());
193        }
194        let listed: Option<Vec<&str>> = w.resource::<RenderGraph>().map(|g| {
195            g.scene_draws()
196                .into_iter()
197                .filter(|(_, m)| {
198                    **m == SceneMembers::Stated {
199                        placements: ScenePlacements::Listed,
200                    }
201                })
202                .map(|(d, _)| d)
203                .collect()
204        });
205        for (i, d) in self.0.iter().enumerate() {
206            if self.0[..i].contains(d) {
207                return Err(format!("the draw {d} named twice"));
208            }
209            if let Some(listed) = &listed
210                && !listed.contains(&d.as_str())
211            {
212                return Err(format!(
213                    "the draw {d} is no scene step's of the world's core.render_graph that lists its placements"
214                ));
215            }
216        }
217        Ok(())
218    }
219}
220
221/// The step bindings and step draws that name a draw the world's `core.render_graph` no longer makes, taken away (a
222/// component left naming none goes with them): what they named when the steps making their draws went with entities
223/// that left the world ([`RenderGraph::retain_steps`]): each (material, draw) and (placement, draw) dropped, in entity
224/// order, as [`crate::Dropped`] states them. Nothing changes in a world without a graph.
225pub(crate) fn retain_made(w: &mut World) -> crate::Dropped {
226    let Some(graph) = w.resource::<RenderGraph>() else {
227        return crate::Dropped::default();
228    };
229    let made: std::collections::BTreeSet<String> = graph
230        .scene_draws()
231        .into_iter()
232        .map(|(d, _)| d.to_owned())
233        .collect();
234    let stale_bindings: Vec<Entity> = w
235        .iter::<StepBindings>()
236        .filter(|(e, _)| w.is_alive(*e))
237        .filter(|(_, b)| b.0.iter().any(|b| !made.contains(&b.draw)))
238        .map(|(e, _)| e)
239        .collect();
240    let stale_draws: Vec<Entity> = w
241        .iter::<StepDraws>()
242        .filter(|(e, _)| w.is_alive(*e))
243        .filter(|(_, d)| d.0.iter().any(|d| !made.contains(d)))
244        .map(|(e, _)| e)
245        .collect();
246    let mut bindings = Vec::new();
247    for e in stale_bindings {
248        let Some(mut b) = w.remove::<StepBindings>(e) else {
249            continue;
250        };
251        b.0.retain(|b| {
252            let keep = made.contains(&b.draw);
253            if !keep {
254                bindings.push((e, b.draw.clone()));
255            }
256            keep
257        });
258        if !b.0.is_empty() {
259            w.insert(e, b);
260        }
261    }
262    let mut draws = Vec::new();
263    for e in stale_draws {
264        let Some(mut d) = w.remove::<StepDraws>(e) else {
265            continue;
266        };
267        d.0.retain(|d| {
268            let keep = made.contains(d);
269            if !keep {
270                draws.push((e, d.clone()));
271            }
272            keep
273        });
274        if !d.0.is_empty() {
275            w.insert(e, d);
276        }
277    }
278    crate::Dropped {
279        step_bindings: bindings,
280        step_draws: draws,
281        ..crate::Dropped::default()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::render_graph::{
289        DepthView, DrawBucket, DrawSorting, Outputs, PassWork, Presence, RenderPass, RenderTarget, Rounding,
290        Runs, SceneView, TargetFormat, TargetShape, TargetSize, TargetView,
291    };
292
293    fn graph(draws: &[(&str, SceneMembers)]) -> RenderGraph {
294        RenderGraph {
295            targets: vec![RenderTarget {
296                name: "depth".into(),
297                format: TargetFormat::Depth32Stencil8,
298                size: TargetSize::View {
299                    numerator: 1,
300                    denominator: 1,
301                    depth: 1,
302                    rounding: Rounding::Down,
303                },
304                shape: TargetShape::Plane,
305                mips: 1,
306                history: false,
307                shared: false,
308            }],
309            buffers: Vec::new(),
310            passes: draws
311                .iter()
312                .map(|(draw, members)| RenderPass {
313                    name: (*draw).into(),
314                    shared: false,
315                    presence: Presence::Always,
316                    runs: Runs::Once,
317                    work: PassWork::Scene {
318                        bucket: DrawBucket::Forward,
319                        view: SceneView::Camera,
320                        outputs: Outputs {
321                            colour: Vec::new(),
322                            depth: Some(DepthView {
323                                view: TargetView {
324                                    target: 0,
325                                    mip: 0,
326                                    first_layer: 0,
327                                    layers: 0,
328                                },
329                                read_only_depth: false,
330                                read_only_stencil: false,
331                            }),
332                        },
333                        draw: (*draw).into(),
334                        members: members.clone(),
335                        frame: Vec::new(),
336                        sorting: DrawSorting::NotStated {
337                            why: "a fixture".into(),
338                        },
339                    },
340                })
341                .collect(),
342            picture: None,
343            lighting: None,
344            local_shadow_atlas: None,
345            left_out: Vec::new(),
346        }
347    }
348
349    /// A material's step bindings name each draw once, a draw of the graph's scene steps; the main draw's needs the
350    /// material's binding, and an unbound one says why. A placement's step draws name listed draws of the graph, once
351    /// each, on something drawn.
352    #[test]
353    fn step_bindings_and_draws_verify_their_own_rules() {
354        let mut w = World::new();
355        let material = w.spawn();
356        let bound = |draw: &str, program| StepBinding {
357            draw: draw.into(),
358            pass: Some("DepthNormals".into()),
359            program,
360        };
361        let unbound = |draw: &str, why: &str| bound(draw, StepProgram::Unbound { why: why.into() });
362        assert!(
363            StepBindings(vec![unbound("depth_normals", "not read")])
364                .verify(material, &w)
365                .is_ok()
366        );
367        let err = |b: StepBindings, w: &World| b.verify(material, w).unwrap_err();
368        assert!(err(StepBindings(Vec::new()), &w).contains("no draw"));
369        assert!(err(StepBindings(vec![unbound("a", "x"), unbound("a", "y")]), &w).contains("twice"));
370        // one draw through two passes of the material, each named
371        let through = |pass: Option<&str>| StepBinding {
372            draw: "a".into(),
373            pass: pass.map(str::to_owned),
374            program: StepProgram::Unbound { why: "x".into() },
375        };
376        let two = StepBindings(vec![
377            through(Some("DepthNormals")),
378            through(Some("DepthNormalsOnly")),
379        ]);
380        assert!(two.verify(material, &w).is_ok());
381        assert_eq!(two.of("a").count(), 2);
382        assert!(
383            err(
384                StepBindings(vec![through(Some("DepthNormals")), through(None)]),
385                &w
386            )
387            .contains("named pass")
388        );
389        assert!(
390            err(
391                StepBindings(vec![through(None), through(Some("DepthNormals"))]),
392                &w
393            )
394            .contains("named pass")
395        );
396        assert!(err(StepBindings(vec![unbound("a", " ")]), &w).contains("not why"));
397        assert!(err(StepBindings(vec![bound("a", StepProgram::Main)]), &w).contains("core.shader_binding"));
398        w.insert(material, ShaderBinding::default());
399        assert!(
400            StepBindings(vec![bound("a", StepProgram::Main)])
401                .verify(material, &w)
402                .is_ok()
403        );
404        assert_eq!(
405            StepBindings(vec![bound("a", StepProgram::Main)]).0[0].binding(material, &w),
406            Some(&ShaderBinding::default())
407        );
408        // with a graph, only its scene steps' draws
409        let stated = |placements| SceneMembers::Stated { placements };
410        w.insert_resource(graph(&[
411            ("forward", stated(ScenePlacements::All)),
412            ("depth_normals", stated(ScenePlacements::Listed)),
413            (
414                "velocity",
415                SceneMembers::NotRead {
416                    why: "a fixture".into(),
417                },
418            ),
419        ]));
420        assert!(
421            StepBindings(vec![bound("forward", StepProgram::Main)])
422                .verify(material, &w)
423                .is_ok()
424        );
425        assert!(err(StepBindings(vec![bound("a", StepProgram::Main)]), &w).contains("no scene step's"));
426        // a draw whose surfaces the source does not read binds none
427        assert!(err(StepBindings(vec![unbound("velocity", "x")]), &w).contains("does not read"));
428
429        let placement = w.spawn();
430        let draws = |d: &[&str]| StepDraws(d.iter().map(|s| (*s).to_owned()).collect());
431        assert!(
432            draws(&["depth_normals"])
433                .verify(placement, &w)
434                .unwrap_err()
435                .contains("draws nothing")
436        );
437        let prototype = w.spawn();
438        w.insert(
439            placement,
440            Instance {
441                prototype,
442                variant: 0,
443            },
444        );
445        assert!(draws(&["depth_normals"]).verify(placement, &w).is_ok());
446        assert!(draws(&["depth_normals"]).draws_in("depth_normals"));
447        assert!(draws(&[]).verify(placement, &w).unwrap_err().contains("no draw"));
448        assert!(
449            draws(&["depth_normals", "depth_normals"])
450                .verify(placement, &w)
451                .unwrap_err()
452                .contains("twice")
453        );
454        // a draw whose steps draw every placement is not listed per placement
455        assert!(
456            draws(&["forward"])
457                .verify(placement, &w)
458                .unwrap_err()
459                .contains("lists its placements")
460        );
461    }
462
463    /// A binding of draw `draw` with no program.
464    fn unbound(draw: &str) -> StepBinding {
465        StepBinding {
466            draw: draw.into(),
467            pass: None,
468            program: StepProgram::Unbound {
469                why: "a fixture".into(),
470            },
471        }
472    }
473
474    /// A world whose graph draws `forward` on its own and `depth_normals` in a group after a screen step of `program`
475    /// (which writes the picture): `lit` binds both draws, `depth_only` the depth normals one, and `placement` is drawn
476    /// by it. Returns the world, [program, lit, depth_only, placement], and the group step.
477    fn grouped() -> (World, [Entity; 4], RenderPass) {
478        use iridia_interface::Stage;
479        let mut w = World::new();
480        let program = w.spawn();
481        let (lit, depth_only, placement) = (w.spawn(), w.spawn(), w.spawn());
482        let stated = |placements| SceneMembers::Stated { placements };
483        let mut g = graph(&[
484            ("forward", stated(ScenePlacements::All)),
485            ("depth_normals", stated(ScenePlacements::Listed)),
486        ]);
487        // the depth normals draw runs in a group after a screen step of `program`, which writes the picture
488        let depth_step = g.passes.remove(1);
489        let screen = RenderPass {
490            name: "resolve".into(),
491            shared: false,
492            presence: Presence::Always,
493            runs: Runs::Once,
494            work: PassWork::Screen {
495                vertices: 3,
496                stages: vec![crate::shader::StageBinding::new(Stage::Pixel, program)],
497                state: Default::default(),
498                outputs: Outputs {
499                    colour: Vec::new(),
500                    depth: Some(DepthView {
501                        view: TargetView {
502                            target: 0,
503                            mip: 0,
504                            first_layer: 0,
505                            layers: 0,
506                        },
507                        read_only_depth: false,
508                        read_only_stencil: false,
509                    }),
510                },
511                region: None,
512            },
513        };
514        g.passes.push(RenderPass {
515            name: "depth group".into(),
516            shared: false,
517            presence: Presence::Always,
518            runs: Runs::Once,
519            work: PassWork::Group {
520                steps: vec![screen, depth_step],
521            },
522        });
523        g.picture = Some(0);
524        let group = g.passes[1].clone();
525        w.insert_resource(g);
526        w.insert(
527            lit,
528            StepBindings(vec![unbound("forward"), unbound("depth_normals")]),
529        );
530        w.insert(depth_only, StepBindings(vec![unbound("depth_normals")]));
531        w.insert(placement, Mesh::new(Vec::new()));
532        w.insert(placement, StepDraws(vec!["depth_normals".into()]));
533        (w, [program, lit, depth_only, placement], group)
534    }
535
536    /// A part that leaves out what a step of the graph draws with drops that step whole (a group with all its steps),
537    /// the picture it wrote, and the step bindings and step draws naming the draws only it made, and says so; the part
538    /// verifies. `subset` panics naming them; a despawn drops the same and records it for the pass manager. The
539    /// lighting pipeline's selections are renumbered past a dropped step, and it goes where it selected one.
540    #[test]
541    fn a_part_drops_the_steps_reaching_outside_it_and_the_bindings_naming_their_draws() {
542        use crate::render_graph::LightingPipeline;
543        let (w, [program, lit, depth_only, placement], group) = grouped();
544        let stated = |placements| SceneMembers::Stated { placements };
545
546        let keep = std::collections::BTreeSet::from([lit, depth_only, placement]);
547        let part = w.part(&keep);
548        assert_eq!(
549            part.dropped.graph.steps,
550            vec![("depth group".to_owned(), program)]
551        );
552        assert!(part.dropped.graph.picture && !part.dropped.graph.lighting);
553        assert_eq!(
554            part.dropped.step_bindings,
555            vec![
556                (lit, "depth_normals".to_owned()),
557                (depth_only, "depth_normals".to_owned())
558            ]
559        );
560        assert_eq!(
561            part.dropped.step_draws,
562            vec![(placement, "depth_normals".to_owned())]
563        );
564        let p = &part.world;
565        assert_eq!(p.verify_components(), Vec::new());
566        assert_eq!(p.verify_resources(), Vec::new());
567        assert_eq!(
568            p.get::<StepBindings>(lit),
569            Some(&StepBindings(vec![unbound("forward")]))
570        );
571        assert!(!p.has::<StepBindings>(depth_only) && !p.has::<StepDraws>(placement));
572        let kept = p.resource::<RenderGraph>().unwrap();
573        assert_eq!((kept.passes.len(), kept.picture), (1, None));
574        // what keeps everything drops nothing
575        assert!(w.part(&w.entities().collect()).dropped.is_empty());
576
577        // subset names what it would drop
578        let panicked =
579            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| w.subset(&keep))).unwrap_err();
580        let message = panicked.downcast_ref::<String>().unwrap();
581        assert!(
582            message.contains("depth group")
583                && message.contains("step bindings")
584                && message.contains("depth_normals"),
585            "{message}"
586        );
587
588        // a despawn drops the same in place
589        let mut despawned = w.clone();
590        despawned.despawn(program);
591        assert_eq!(despawned.take_dropped(), part.dropped);
592        assert_eq!(despawned.verify_components(), Vec::new());
593        assert!(despawned.take_dropped().is_empty());
594
595        // split_off: steps pruned from the part's copy of the graph are no loss (the world keeps every step), so moving
596        // an entity that names none of their draws goes through and is counted; stripping the moved entities' bindings
597        // is a loss, and panics naming it
598        let mut split = w.clone();
599        let plain = split.spawn();
600        split.insert(plain, Mesh::new(Vec::new()));
601        let mut moved = split.clone();
602        let counted = moved.split_part(&std::collections::BTreeSet::from([plain]));
603        assert_eq!(counted.dropped.graph.steps, part.dropped.graph.steps);
604        assert!(counted.dropped.step_bindings.is_empty() && counted.dropped.step_draws.is_empty());
605        let off = split.split_off(&std::collections::BTreeSet::from([plain]));
606        assert_eq!(split.resource::<RenderGraph>().map(|g| g.passes.len()), Some(2));
607        split.absorb(off).unwrap();
608        let panicked =
609            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| split.split_off(&keep))).unwrap_err();
610        let message = panicked.downcast_ref::<String>().unwrap();
611        assert!(
612            message.contains("split_off stripped") && message.contains("depth_normals"),
613            "{message}"
614        );
615
616        // the lighting pipeline: renumbered past a dropped step, gone where it selected one
617        let mut g = graph(&[
618            ("a", stated(ScenePlacements::All)),
619            ("b", stated(ScenePlacements::All)),
620        ]);
621        g.passes.insert(0, group);
622        g.lighting = Some(LightingPipeline {
623            passes: vec![2],
624            ..LightingPipeline::default()
625        });
626        let mut renumbered = g.clone();
627        assert_eq!(renumbered.retain_steps(&|e| e != program).steps.len(), 1);
628        assert_eq!(renumbered.lighting.map(|l| l.passes), Some(vec![1]));
629        g.lighting = Some(LightingPipeline {
630            passes: vec![0, 2],
631            ..LightingPipeline::default()
632        });
633        assert!(g.retain_steps(&|e| e != program).lighting);
634        assert_eq!(g.lighting, None);
635    }
636
637    /// What the world drops outside any pass (a despawn before the run) is recorded in the run's report by the pass that
638    /// ran next, and not in that pass's log; what a pass's own despawn drops is counted in its log, and the removals of
639    /// the step bindings and step draws it took are its, with the pass checks on.
640    #[test]
641    fn a_run_records_what_the_world_dropped_outside_its_passes() {
642        use crate::pass::{Pass, PassLog, PassManager, PassRegistry, Pipeline, RunReport};
643        struct Despawns(Entity);
644        impl Pass for Despawns {
645            fn name(&self) -> &'static str {
646                "despawn-program"
647            }
648            fn despawns_entities(&self) -> bool {
649                true
650            }
651            fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
652                w.despawn(self.0);
653                Ok(())
654            }
655        }
656        struct Nothing;
657        impl Pass for Nothing {
658            fn name(&self) -> &'static str {
659                "nothing"
660            }
661            fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
662                Ok(())
663            }
664        }
665        let manager = PassManager::new(PassRegistry::new()).with_pass_checks(true);
666        let steps = "render graph steps dropped: they drew with an entity that left the world";
667        let bindings = "step bindings dropped: only dropped render graph steps made their draws";
668
669        // in a pass: counted in its log
670        let (mut w, [program, ..], _) = grouped();
671        let mut report = RunReport::default();
672        let pipeline = Pipeline {
673            passes: vec![Box::new(Despawns(program))],
674        };
675        manager.run(&mut w, &pipeline, &mut report).unwrap();
676        let counts = &report.steps[0].log.counts;
677        assert_eq!((counts.get(steps), counts.get(bindings)), (Some(&1), Some(&2)));
678        assert!(report.dropped_outside.is_empty());
679
680        // outside any pass: the run records it before the next pass, whose log holds none of it
681        let (mut w, [program, ..], _) = grouped();
682        w.despawn(program);
683        let mut report = RunReport::default();
684        let pipeline = Pipeline {
685            passes: vec![Box::new(Nothing)],
686        };
687        manager.run(&mut w, &pipeline, &mut report).unwrap();
688        assert_eq!(report.dropped_outside.len(), 1);
689        let (next, dropped) = &report.dropped_outside[0];
690        assert_eq!(
691            (
692                next.as_str(),
693                dropped.graph.steps.len(),
694                dropped.step_bindings.len()
695            ),
696            ("nothing", 1, 2)
697        );
698        assert!(!report.steps[0].log.counts.contains_key(steps));
699        assert!(
700            report
701                .to_string()
702                .contains("outside any pass, before nothing: dropped render graph steps depth group")
703        );
704    }
705}