primase-core 0.2.0

primase core: the world store, core components, frames, the pass manager and gates
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
//! Which of the frame's scene steps draw a surface, and through which program: `core.step_bindings` (on materials)
//! and `core.step_draws` (on placements).
//!
//! A frame's scene steps ([`crate::render_graph::PassWork::Scene`]) each make a draw the source names (Unity's
//! `LightMode` passes of the step, Sekiro's buckets), and a renderer draws a surface in some of them, each time
//! through a program of its material: its main draw's (`core.shader_binding`), or another pass of it (a depth
//! prepass draws a material through its depth pass). A source states each part where it lies:
//!
//! - on a material, each draw that draws it and the program it is drawn through there ([`StepBindings`]): a material
//!   without a step binding for a draw is not drawn in it, and a draw that draws it through several of its passes (a
//!   Unity draw draws every pass whose `LightMode` is one of the draw's) binds it once for each pass;
//! - on a placement, the draws of the steps that pick their placements one by one ([`StepDraws`],
//!   [`crate::render_graph::ScenePlacements::Listed`]): a renderer chooses them per drawn object (Unity by the layer of
//!   the object's GameObject), not per material.
//!
//! A material a draw draws whose program the source could not bind says why ([`StepProgram::Unbound`]): the draw then
//! draws something no binding states, and a consumer says what it does there.

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};

/// One draw of the frame's scene steps that draws a material, and the program it draws it through.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct StepBinding {
    /// The draw, as the scene steps name it ([`crate::render_graph::PassWork::Scene::draw`]).
    pub draw: String,
    /// The source's name for the material's pass the draw goes through (Unity's `LightMode` tag of the subshader's
    /// pass, `DepthNormalsOnly`), where the source names its passes.
    pub pass: Option<String>,
    pub program: StepProgram,
}

/// The program a draw draws a material through.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StepProgram {
    /// The material's main draw's, its `core.shader_binding`.
    Main,
    /// Another pass's, bound here and held to what a material's `core.shader_binding` is held to.
    Bound(ShaderBinding),
    /// The draw draws the material, and the source could not bind the program: why.
    Unbound { why: String },
}

impl StepBinding {
    /// The binding the draw draws the material through, where the source bound one: the material's own
    /// `core.shader_binding` for [`StepProgram::Main`].
    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,
        }
    }
}

/// `core.step_bindings`, on a material entity: the draws of the frame's scene steps that draw it and the program each
/// draws it through: a draw once, or once for each pass of the material it draws, in the order it draws them.
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct StepBindings(pub Vec<StepBinding>);

impl StepBindings {
    /// How `draw` draws the material: none where it does not, one binding for each pass it draws it through.
    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);
            }
        }
    }
    /// Not empty; a draw named once, or once for each of several passes, each named (not empty) and once; each draw
    /// one of the scene steps' draws that state what they draw where the world has a `core.render_graph`; a main-draw
    /// binding on a material that has its `core.shader_binding`, another pass's verified as a material's binding, and
    /// an unbound one saying why.
    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(())
    }
}

/// `core.step_draws`, on a placement (a `core.instance`) or an entity that draws its own mesh: the draws of the scene
/// steps that pick their placements one by one ([`ScenePlacements::Listed`]) that draw it, each once. A step whose
/// placements are listed draws no placement without it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StepDraws(pub Vec<String>);

impl StepDraws {
    /// Whether the steps making `draw` draw it.
    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)
    }
    /// On something drawn; not empty, each draw once, and the draw of a scene step that lists its placements where the
    /// world has a `core.render_graph`.
    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(())
    }
}

/// The step bindings and step draws that name a draw the world's `core.render_graph` no longer makes, taken away (a
/// component left naming none goes with them): what they named when the steps making their draws went with entities
/// that left the world ([`RenderGraph::retain_steps`]): each (material, draw) and (placement, draw) dropped, in entity
/// order, as [`crate::Dropped`] states them. Nothing changes in a world without a graph.
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(),
        }
    }

    /// A material's step bindings name each draw once, a draw of the graph's scene steps; the main draw's needs the
    /// material's binding, and an unbound one says why. A placement's step draws name listed draws of the graph, once
    /// each, on something drawn.
    #[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"));
        // one draw through two passes of the material, each named
        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())
        );
        // with a graph, only its scene steps' draws
        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"));
        // a draw whose surfaces the source does not read binds none
        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")
        );
        // a draw whose steps draw every placement is not listed per placement
        assert!(
            draws(&["forward"])
                .verify(placement, &w)
                .unwrap_err()
                .contains("lists its placements")
        );
    }

    /// A binding of draw `draw` with no program.
    fn unbound(draw: &str) -> StepBinding {
        StepBinding {
            draw: draw.into(),
            pass: None,
            program: StepProgram::Unbound {
                why: "a fixture".into(),
            },
        }
    }

    /// A world whose graph draws `forward` on its own and `depth_normals` in a group after a screen step of `program`
    /// (which writes the picture): `lit` binds both draws, `depth_only` the depth normals one, and `placement` is drawn
    /// by it. Returns the world, [program, lit, depth_only, placement], and the group step.
    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)),
        ]);
        // the depth normals draw runs in a group after a screen step of `program`, which writes the picture
        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)
    }

    /// A part that leaves out what a step of the graph draws with drops that step whole (a group with all its steps),
    /// the picture it wrote, and the step bindings and step draws naming the draws only it made, and says so; the part
    /// verifies. `subset` panics naming them; a despawn drops the same and records it for the pass manager. The
    /// lighting pipeline's selections are renumbered past a dropped step, and it goes where it selected one.
    #[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));
        // what keeps everything drops nothing
        assert!(w.part(&w.entities().collect()).dropped.is_empty());

        // subset names what it would drop
        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}"
        );

        // a despawn drops the same in place
        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());

        // split_off: steps pruned from the part's copy of the graph are no loss (the world keeps every step), so moving
        // an entity that names none of their draws goes through and is counted; stripping the moved entities' bindings
        // is a loss, and panics naming it
        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}"
        );

        // the lighting pipeline: renumbered past a dropped step, gone where it selected one
        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);
    }

    /// What the world drops outside any pass (a despawn before the run) is recorded in the run's report by the pass that
    /// 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
    /// the step bindings and step draws it took are its, with the pass checks on.
    #[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";

        // in a pass: counted in its log
        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());

        // outside any pass: the run records it before the next pass, whose log holds none of it
        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")
        );
    }
}