Skip to main content

primase_core/
render_graph.rs

1//! `core.render_graph`: the frame a source's renderer draws, as data (a world resource): the images it makes and the
2//! passes that make them, in order, each a program of the source's with the role of everything it reads and writes. A
3//! consumer runs it with generic machinery: it allocates the targets, runs each pass's programs with the bindings the
4//! pass states, and gives each role its value; it knows no game. Where a renderer's meaning has no core role, the pass
5//! states the source's own words (`ConstantRole::Material`) that the source's lowering computed from its data.
6//!
7//! The scene's own draws (the G-buffer, the shadow casters, the forward and effect draws) are passes of kind
8//! [`PassWork::Scene`]. Each names the draw it makes (the source's name for it, [`PassWork::Scene::draw`]), and the
9//! source states which materials that draw draws and through which program on each material
10//! ([`crate::StepBindings`]: the material's `core.shader_binding`, another pass of it, or why none is bound): a
11//! material without a step binding for the draw is not drawn in it. Which placements it draws is every placement of
12//! such materials, only the ones that name the draw ([`crate::StepDraws`]) where the source picks them per placement,
13//! or the ones that cast shadows ([`ScenePlacements`]); a source that does not read what a draw draws says so and why
14//! ([`SceneMembers::NotRead`]); the images the draws' frame textures are in the step ([`PassWork::Scene::frame`])
15//! and the order it draws them in ([`DrawSorting`]) are the step's. Everything else is a full-screen draw, a compute
16//! dispatch, a clear or a copy of the renderer's own programs, or a slot where another feature draws
17//! ([`FeatureSlot`]). What the source's frame draws that the graph holds no step for is named in
18//! [`RenderGraph::left_out`].
19
20mod lighting;
21pub use lighting::{GeometryStencil, LightingBuffer, LightingPipeline, LightingPort};
22
23use serde::Serialize;
24use serde_json::Value;
25
26use crate::shader::{BindingPlace, BufferSource, FrameTexture, StageBinding, TextureSource};
27use crate::{Entity, JsonError, Resource, World};
28use iridia_interface::{Interface, RenderState, ResourceId, ResourceKind, TextureDim};
29
30/// A target's index in [`RenderGraph::targets`].
31pub type TargetId = u32;
32/// A buffer's index in [`RenderGraph::buffers`].
33pub type BufferId = u32;
34
35/// `core.render_graph`: see the module documentation.
36#[derive(Clone, Debug, Default, PartialEq, Serialize)]
37pub struct RenderGraph {
38    pub targets: Vec<RenderTarget>,
39    /// The buffers the frame's own steps write and read (the light lists a compute step builds for the lighting steps).
40    pub buffers: Vec<RenderBuffer>,
41    pub passes: Vec<RenderPass>,
42    /// The target that holds the finished picture, before any interface the game draws over it, and before what
43    /// [`RenderGraph::left_out`] names after it.
44    pub picture: Option<TargetId>,
45    /// A source-described lighting subgraph's imports, selected passes and linear HDR handoff.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub lighting: Option<LightingPipeline>,
48    /// The layout of the frame's local-light shadow atlas, where the source has one: the target the steps that run per
49    /// face ([`Items::LocalShadowFaces`]) draw into and the shadowed lights' steps ([`Items::ShadowedLights`]) read,
50    /// its regions, faces and projection. Which lights a frame shadows is the consumer's choice
51    /// ([`crate::shader::LocalShadowAtlas::place`]).
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub local_shadow_atlas: Option<crate::shader::LocalShadowAtlas>,
54    /// What the source's frame draws that the graph holds no step for, each named with why (its post-processing,
55    /// where that is not traced: the picture is then the image before it). Empty where the graph holds every step.
56    #[serde(skip_serializing_if = "Vec::is_empty")]
57    pub left_out: Vec<String>,
58}
59
60/// One image the frame makes (or keeps from the frame before).
61#[derive(Clone, Debug, PartialEq, Serialize)]
62pub struct RenderTarget {
63    /// The renderer's name for it where the source states one, else what it holds.
64    pub name: String,
65    pub format: TargetFormat,
66    pub size: TargetSize,
67    pub shape: TargetShape,
68    /// Mip levels, at least 1.
69    pub mips: u32,
70    /// The frame reads what the previous frame left in it (a history: temporal anti-aliasing, adapted exposure, a
71    /// re-projected occlusion): the consumer keeps it from one frame to the next, for each view.
72    pub history: bool,
73    /// One image for the frame, shared by every view (a shadow atlas, a look-up table), rather than one per view.
74    pub shared: bool,
75}
76
77/// One buffer the frame makes (or keeps from the frame before): its steps write it as an unordered-access view and read
78/// it as a shader resource, the step that writes it named in the graph's order ([`PassWork::Compute::buffer_writes`]).
79#[derive(Clone, Debug, PartialEq, Serialize)]
80pub struct RenderBuffer {
81    /// What it holds, as the step that writes it says (`GXLightOrganize16 u0`).
82    pub name: String,
83    pub size: BufferSize,
84    /// The bytes of one element, where the source's views of it state one (a structured buffer's stride).
85    pub stride: Option<u32>,
86    /// The elements the source's views of it span, where they state a count.
87    pub elements: Option<u64>,
88    /// The frame reads what the previous frame left in it.
89    pub history: bool,
90    /// One buffer for the frame, shared by every view, rather than one per view.
91    pub shared: bool,
92}
93
94/// A buffer's size.
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "snake_case")]
97pub enum BufferSize {
98    /// The same whatever the view.
99    Fixed { bytes: u64 },
100    /// The bytes the source's recorded frames had, drawn at the view size `view` (width, height): how the size follows
101    /// the view is not established (the light lists of a tiled renderer grow with the number of screen tiles). A
102    /// consumer drawing at another size states what it allocates.
103    Recorded { bytes: u64, view: [u32; 2] },
104}
105
106/// How a target stores a texel.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
108#[serde(rename_all = "snake_case")]
109pub enum TargetFormat {
110    Rgba32Float,
111    Rgba16Float,
112    Rgba16Unorm,
113    Rg32Float,
114    Rg16Float,
115    R32Float,
116    R32Uint,
117    R16Float,
118    R16Unorm,
119    Rgb10A2Unorm,
120    Rg11B10Float,
121    Rgba8Unorm,
122    Rgba8UnormSrgb,
123    Rg8Unorm,
124    R8Unorm,
125    /// One unsigned 8-bit integer.
126    R8Uint,
127    /// Depth as a 32-bit float with an 8-bit stencil.
128    Depth32Stencil8,
129    /// Depth as a 32-bit float.
130    Depth32,
131    /// Depth as 24-bit unorm with an 8-bit stencil.
132    Depth24Stencil8,
133    /// Depth as 16-bit unorm (a shadow map).
134    Depth16,
135}
136
137impl TargetFormat {
138    /// Whether the format holds depth (and so is a depth target, not a colour one).
139    pub fn is_depth(self) -> bool {
140        matches!(
141            self,
142            Self::Depth32Stencil8 | Self::Depth32 | Self::Depth24Stencil8 | Self::Depth16
143        )
144    }
145}
146
147/// A target's size.
148#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
149#[serde(rename_all = "snake_case")]
150pub enum TargetSize {
151    /// The view's size times `numerator / denominator`, each side rounded as `rounding` says, at least 1 (a
152    /// half-resolution target: 1/2). A volume's depth is stated in `depth`.
153    View {
154        numerator: u32,
155        denominator: u32,
156        depth: u32,
157        rounding: Rounding,
158    },
159    /// A fixed size whatever the view.
160    Fixed { width: u32, height: u32, depth: u32 },
161}
162
163/// How a view-relative size that is not a whole number of texels is rounded, side by side.
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "snake_case")]
166pub enum Rounding {
167    /// Down to the whole texel below.
168    Down,
169    /// Up to the whole texel above: a part of a texel counts as one (1080 / 16 is 68 texels).
170    Up,
171}
172
173/// A target's shape.
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
175#[serde(rename_all = "snake_case")]
176pub enum TargetShape {
177    Plane,
178    /// `layers` planes of the size.
179    Array {
180        layers: u32,
181    },
182    /// A volume `depth` deep (the size's depth).
183    Volume,
184    Cube,
185}
186
187/// One step of the frame.
188#[derive(Clone, Debug, PartialEq, Serialize)]
189pub struct RenderPass {
190    /// The renderer's name for the step where it has one, else its program's (the name a consumer times it by).
191    pub name: String,
192    /// Run once a frame for every view (what the step makes does not depend on the eye: shadow cascades, look-up tables,
193    /// the light lists of a view-independent volume), rather than once per view.
194    pub shared: bool,
195    /// Whether the frame has the step: a step the source draws only in some frames stands in its place in the order
196    /// all the same, so that a frame without it skips it and one with it draws it there.
197    pub presence: Presence,
198    /// How many times the step runs where the frame has it.
199    pub runs: Runs,
200    pub work: PassWork,
201}
202
203/// Whether a frame has a step.
204#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
205#[serde(rename_all = "snake_case")]
206pub enum Presence {
207    /// Every frame has it.
208    Always,
209    /// A frame has it where the condition holds, as the source's recorded frames show.
210    When(Trigger),
211    /// Some frames have it and what decides it is not known: the recorded frames that had it (the source's names for
212    /// them), so that a consumer can say why it drew or skipped the step. Never empty.
213    Unknown { frames: Vec<String> },
214}
215
216/// What makes the source draw a step that only some frames have.
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
218#[serde(rename_all = "snake_case")]
219pub enum Trigger {
220    /// A local light that casts shadows is in view: the frame draws its shadow casters and lights with its shadow.
221    LocalShadowCasters,
222    /// The frame draws water: the steps that draw for it (its interaction maps, the screen-space reflections whose
223    /// prepass draws the water) run.
224    Water,
225}
226
227/// How many times a step runs where the frame has it.
228#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
229#[serde(rename_all = "snake_case")]
230pub enum Runs {
231    /// Once.
232    Once,
233    /// Once for each item of the frame that `items` says, each run with that item's own values (its constants are the
234    /// roles its binding states); `seen` is how many runs the source's recorded frames had, one count per frame that
235    /// had the step.
236    Each { items: Items, seen: Vec<u32> },
237}
238
239/// What one run of a repeated step is for.
240#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
241#[serde(rename_all = "snake_case")]
242pub enum Items {
243    /// Not identified: the source's recorded frames run the step a varying number of times, and what one run draws
244    /// for is not established; `why` says what was examined.
245    Unknown { why: String },
246    /// One run per face of each light the consumer shadows this frame, light by light in the order it chose them and
247    /// each light's faces in the order of the graph's [`RenderGraph::local_shadow_atlas`]
248    /// ([`crate::shader::ShadowPlacement::faces`]): a run draws the step's scene work from that face's camera (the run's
249    /// view roles are the face's view and projection) into the face's region of the atlas (the run's viewport).
250    LocalShadowFaces,
251    /// One run per light the consumer shadows this frame, in the order it chose them: a run's
252    /// [`crate::shader::ConstantRole::Item`] constants are that light's, its record the one the frame's light table of
253    /// `table` packs for it with its shadow ([`crate::shader::LightTable::pack`]).
254    ShadowedLights { table: crate::shader::LightTableKind },
255}
256
257/// What a step does.
258#[derive(Clone, Debug, PartialEq, Serialize)]
259#[serde(rename_all = "snake_case")]
260pub enum PassWork {
261    /// A procedural primitive in the space of `object`'s `core.transform`. Object constant roles use that
262    /// object-to-world transform; vertex streams use the primitive's local attributes.
263    /// This is a volume/light draw, independent of scene meshes and their material buckets.
264    Geometry {
265        primitive: GraphPrimitive,
266        object: Entity,
267        stages: Vec<StageBinding>,
268        state: RenderState,
269        outputs: Outputs,
270    },
271    /// A draw of `vertices` vertices that cover the view (3: a triangle larger than the view; 4: a quad) through
272    /// `stages` (a vertex and a pixel program), with `state`, into `outputs`. The vertex program makes them from their
273    /// index, or reads them from the inputs its binding states as [`crate::shader::InputRole::ScreenTriangle`].
274    Screen {
275        vertices: u32,
276        stages: Vec<StageBinding>,
277        state: RenderState,
278        outputs: Outputs,
279        /// The rectangle of the outputs the draw covers (its viewport), where it is not the whole of them: a copy into
280        /// one cascade's quarter of a shadow atlas.
281        region: Option<Region>,
282    },
283    /// A dispatch of a compute program: `groups` thread groups, writing the targets its unordered-access resources name.
284    Compute {
285        stage: StageBinding,
286        groups: Groups,
287        writes: Vec<(ResourceId, TargetView)>,
288        /// The graph's buffers it writes, by the program's unordered-access resource.
289        buffer_writes: Vec<(ResourceId, BufferId)>,
290    },
291    /// The scene's draws of a bucket through their materials' programs, into `outputs`, seen from `view`.
292    Scene {
293        /// The kind of draw.
294        bucket: DrawBucket,
295        view: SceneView,
296        outputs: Outputs,
297        /// The source's name for the draw the step makes (Unity's `LightMode` pass of its draw, Sekiro's bucket). Steps
298        /// that make the same draw (the cascades of a shadow) draw the same surfaces.
299        draw: String,
300        /// Which surfaces it draws.
301        members: SceneMembers,
302        /// The image each frame texture its draws read is in this step ([`crate::shader::TextureSource::Frame`]), each
303        /// role once: a role not listed is no image of the graph's here, and a program reading it reads what its
304        /// binding's consumer says.
305        frame: Vec<(FrameTexture, TargetView)>,
306        /// The order it draws them in.
307        sorting: DrawSorting,
308    },
309    /// Set every texel of a target's view to a value.
310    Clear { target: TargetView, value: [f32; 4] },
311    /// Copy one target's view into another's of the same size and format.
312    Copy { from: TargetView, to: TargetView },
313    /// Set every element of a buffer to a value (its 32-bit words, as an unordered-access clear writes them).
314    ClearBuffer { buffer: BufferId, value: [u32; 4] },
315    /// Copy one buffer into another of the same size.
316    CopyBuffer { from: BufferId, to: BufferId },
317    /// Where another feature draws, into these outputs (its own programs and data).
318    Slot { slot: FeatureSlot, outputs: Outputs },
319    /// Steps that run together, in order: the unit a repeated step ([`Runs::Each`]) runs once per item, where the
320    /// source runs several steps for each item (a copy, then a draw that reads it). Never empty.
321    Group { steps: Vec<RenderPass> },
322}
323
324/// Geometry supplied by a render-graph consumer, in a declared local coordinate system.
325#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "snake_case")]
327pub enum GraphPrimitive {
328    /// The closed box [-1,1] on each axis, outward counterclockwise triangles. POSITION is its local xyz
329    /// (and w=1 when requested). It supplies no other vertex attribute; unsupported streams must be refused.
330    UnitBox,
331}
332
333/// The render targets and depth a draw writes.
334#[derive(Clone, Debug, Default, PartialEq, Serialize)]
335pub struct Outputs {
336    /// By render-target slot.
337    pub colour: Vec<(u32, TargetView)>,
338    pub depth: Option<DepthView>,
339}
340
341/// One subresource range of a target.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
343pub struct TargetView {
344    pub target: TargetId,
345    pub mip: u32,
346    pub first_layer: u32,
347    /// Layers (a volume's slices, a cube's faces); 0 for all.
348    pub layers: u32,
349}
350
351/// A depth target as a draw binds it.
352#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
353pub struct DepthView {
354    pub view: TargetView,
355    /// The draw only reads the depth (the target can be sampled at the same time).
356    pub read_only_depth: bool,
357    pub read_only_stencil: bool,
358}
359
360/// How many thread groups a dispatch runs.
361#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
362#[serde(rename_all = "snake_case")]
363pub enum Groups {
364    Fixed([u32; 3]),
365    /// Enough groups of `group` threads to cover a target's mip: `ceil(size / group)` per axis.
366    Cover {
367        target: TargetId,
368        mip: u32,
369        group: [u32; 3],
370    },
371}
372
373/// The kind of draw a scene step makes, as the source states it (what it draws is [`SceneMembers`]'s, never the
374/// kind's).
375#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
376#[serde(rename_all = "snake_case")]
377pub enum DrawBucket {
378    /// Opaque and alpha-tested surfaces into the G-buffer.
379    Deferred,
380    /// Decals over the G-buffer.
381    Decal,
382    /// Surfaces lit as they are drawn, after the deferred light (the blended ones, water).
383    Forward,
384    /// The sky: its dome, the sun's and moon's discs, cloud layers and far scenery drawn as part of the sky, each by its
385    /// own material, after the deferred light. Where its step stands against the source's fog composite (before it, so
386    /// the fog lies over the sky, or after it) is the graph's order of steps, as the source draws it.
387    Sky,
388    /// The shadow casters of a light.
389    ShadowCaster,
390    /// Surfaces that write motion only.
391    Velocity,
392}
393
394/// Which surfaces a scene step's draw draws.
395#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
396#[serde(rename_all = "snake_case")]
397pub enum SceneMembers {
398    /// The materials with a step binding for the draw ([`crate::StepBindings`]), each through that binding's program
399    /// (a material without one is not drawn in it), at the placements `placements` picks.
400    Stated { placements: ScenePlacements },
401    /// The source does not read which surfaces the draw draws or through which programs: why. No material binds the
402    /// draw, and a consumer says what it draws there.
403    NotRead { why: String },
404}
405
406/// Which placements a scene step draws, of the materials its draw draws.
407#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
408#[serde(rename_all = "snake_case")]
409pub enum ScenePlacements {
410    /// Every placement (a `core.instance`, or an entity drawing its own `core.mesh`) whose primitives' materials the
411    /// draw draws, each of those primitives, but a shadow proxy's (`core.shadows` casting
412    /// [`crate::ShadowCast::Only`]).
413    All,
414    /// Only the placements whose `core.step_draws` names the step's draw ([`crate::StepDraws`]): the source picks
415    /// them per placement (Unity culls each renderer by its GameObject's layer).
416    Listed,
417    /// The placements that cast shadows ([`crate::Shadows`]: `cast` not [`crate::ShadowCast::Off`]), shadow proxies
418    /// included: a shadow caster draw picks them so.
419    Casting,
420}
421
422/// The order a scene step draws its draws in, as the source states it.
423#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
424#[serde(rename_all = "snake_case")]
425pub enum DrawSorting {
426    /// The source's sort keys, every one it sets (Unity's `SortingCriteria` flags, in the order of the flags' bits):
427    /// draws equal under every key are drawn in an order the source leaves free. Never empty.
428    Keys(Vec<SortKey>),
429    /// The source's order is not established: why.
430    NotStated { why: String },
431}
432
433/// One key a source sorts a scene step's draws by.
434#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
435#[serde(rename_all = "snake_case")]
436pub enum SortKey {
437    /// By the renderer's sorting layer, then its order within it (Unity's `SortingLayer`).
438    SortingLayer,
439    /// By the material's render queue, lower first (`RenderQueue`).
440    RenderQueue,
441    /// The farthest from the camera first (`BackToFront`).
442    BackToFront,
443    /// The nearest first, by the distance rounded to coarse steps so that nearby draws group by state
444    /// (`QuantizedFrontToBack`).
445    QuantizedFrontToBack,
446    /// Draws of the same program and material together, where the other keys leave them equal
447    /// (`OptimizeStateChanges`).
448    OptimizeStateChanges,
449    /// By the order of the canvases of an interface (`CanvasOrder`).
450    CanvasOrder,
451    /// By the renderer's own priority (`RendererPriority`).
452    RendererPriority,
453}
454
455/// The view a scene step draws from.
456#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
457#[serde(rename_all = "snake_case")]
458pub enum SceneView {
459    /// The frame's camera.
460    Camera,
461    /// Cascade `cascade` of the sun's shadow (its matrix the role `ViewParameter::ShadowCascadeMatrix(cascade)`), drawn
462    /// into `region` of the depth target: an atlas holds several cascades side by side (Sekiro's four 2048 x 2048
463    /// quarters of one 4096 x 4096 target).
464    ShadowCascade { cascade: u8, region: Region },
465    /// A local light's view of the shadow casters around it, drawn into a region of the depth target that each run
466    /// places itself (its viewport): a step that runs once per region of a local lights' shadow atlas
467    /// ([`Runs::Each`]), Elden Ring's 64 x 64 regions of its 4096 x 4096 atlas. Which light and which face a run is for
468    /// is not stated.
469    LocalShadow,
470}
471
472/// A rectangle of a target's texels at its mip 0: its top-left corner `x`, `y` and its `width` and `height`.
473#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
474pub struct Region {
475    pub x: u32,
476    pub y: u32,
477    pub width: u32,
478    pub height: u32,
479}
480
481/// Another feature's place in the frame.
482#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
483#[serde(rename_all = "snake_case")]
484pub enum FeatureSlot {
485    /// Particles and effects, drawn into the lit picture after the opaque composite and the fog.
486    Effects,
487}
488
489impl RenderGraph {
490    fn view_ok(&self, v: &TargetView, what: &str) -> Result<(), String> {
491        let t = self
492            .targets
493            .get(v.target as usize)
494            .ok_or_else(|| format!("{what} names target {} of {}", v.target, self.targets.len()))?;
495        if v.mip >= t.mips {
496            return Err(format!(
497                "{what} names mip {} of target {} ({} mips)",
498                v.mip, t.name, t.mips
499            ));
500        }
501        Ok(())
502    }
503    fn outputs_ok(&self, o: &Outputs, what: &str) -> Result<(), String> {
504        for (slot, v) in &o.colour {
505            let what = format!("{what} target {slot}");
506            self.view_ok(v, &what)?;
507            if self.targets[v.target as usize].format.is_depth() {
508                return Err(format!(
509                    "{what} renders colour into the depth target {}",
510                    self.targets[v.target as usize].name
511                ));
512            }
513        }
514        if let Some(d) = &o.depth {
515            let what = format!("{what} depth");
516            self.view_ok(&d.view, &what)?;
517            if !self.targets[d.view.target as usize].format.is_depth() {
518                return Err(format!(
519                    "{what} uses {} as depth",
520                    self.targets[d.view.target as usize].name
521                ));
522            }
523        }
524        Ok(())
525    }
526    /// A full-screen draw's region: not empty, and inside every output whose size is fixed.
527    fn screen_region_ok(&self, o: &Outputs, region: &Region, what: &str) -> Result<(), String> {
528        if region.width == 0 || region.height == 0 {
529            return Err(format!("{what} draws into an empty region"));
530        }
531        let views = o
532            .colour
533            .iter()
534            .map(|(_, v)| v)
535            .chain(o.depth.as_ref().map(|d| &d.view));
536        for v in views {
537            if let TargetSize::Fixed { width, height, .. } = self.targets[v.target as usize].size {
538                let right = region.x.checked_add(region.width);
539                let bottom = region.y.checked_add(region.height);
540                if right.is_none_or(|r| r > width) || bottom.is_none_or(|b| b > height) {
541                    return Err(format!(
542                        "{what} draws outside its target {} ({width} x {height})",
543                        self.targets[v.target as usize].name
544                    ));
545                }
546            }
547        }
548        Ok(())
549    }
550    /// A shadow cascade's region: not empty, drawn into a depth target, and inside it where its size is fixed.
551    fn region_ok(&self, o: &Outputs, region: &Region, what: &str) -> Result<(), String> {
552        let depth = o
553            .depth
554            .as_ref()
555            .ok_or_else(|| format!("{what} draws a shadow cascade with no depth target"))?;
556        if region.width == 0 || region.height == 0 {
557            return Err(format!("{what} draws a shadow cascade into an empty region"));
558        }
559        if let TargetSize::Fixed { width, height, .. } = self.targets[depth.view.target as usize].size {
560            let right = region.x.checked_add(region.width);
561            let bottom = region.y.checked_add(region.height);
562            if right.is_none_or(|r| r > width) || bottom.is_none_or(|b| b > height) {
563                return Err(format!(
564                    "{what} draws a shadow cascade outside its depth target ({width} x {height})"
565                ));
566            }
567        }
568        Ok(())
569    }
570    /// A step run once per face of the shadowed lights: the scene's draws from a local light's view
571    /// ([`SceneView::LocalShadow`]) into the depth of the graph's local shadow atlas, and no colour.
572    fn face_step_ok(&self, p: &RenderPass, what: &str) -> Result<(), String> {
573        let atlas = self.local_shadow_atlas.as_ref().ok_or_else(|| {
574            format!("{what} runs once per local shadow face in a graph with no local shadow atlas")
575        })?;
576        let PassWork::Scene {
577            view: SceneView::LocalShadow,
578            outputs,
579            ..
580        } = &p.work
581        else {
582            return Err(format!(
583                "{what} runs once per local shadow face and is not a scene step from a local light's view"
584            ));
585        };
586        if outputs.depth.as_ref().map(|d| d.view.target) != Some(atlas.target) || !outputs.colour.is_empty() {
587            return Err(format!(
588                "{what} draws local shadow faces into other than the atlas's depth alone"
589            ));
590        }
591        Ok(())
592    }
593    fn buffer_ok(&self, b: BufferId, what: &str) -> Result<(), String> {
594        if b as usize >= self.buffers.len() {
595            return Err(format!("{what} names buffer {b} of {}", self.buffers.len()));
596        }
597        Ok(())
598    }
599    /// The views a stage's texture slots bind of the graph's targets name a target and a mip it has, and the graph's
600    /// buffers its buffer slots bind are the graph's.
601    fn stage_targets_ok(&self, stages: &[&StageBinding], what: &str) -> Result<(), String> {
602        for s in stages {
603            for t in &s.textures {
604                if let TextureSource::Target { view } = &t.source {
605                    self.view_ok(
606                        view,
607                        &format!("{what} {:?} texture slot {}", s.stage, t.resource.0),
608                    )?;
609                }
610            }
611            for b in &s.buffers {
612                if let BufferSource::Graph(id) = b.source {
613                    self.buffer_ok(id, &format!("{what} {:?} buffer slot {}", s.stage, b.resource.0))?;
614                }
615            }
616        }
617        Ok(())
618    }
619    /// One step: its presence and runs stated, its views and outputs well formed, a group's steps each in turn.
620    fn pass_ok(&self, p: &RenderPass) -> Result<(), String> {
621        let what = format!("pass {}", p.name);
622        if let Presence::Unknown { frames } = &p.presence
623            && frames.is_empty()
624        {
625            return Err(format!("{what} is in some frames, and names none"));
626        }
627        if let Runs::Each { items, seen } = &p.runs {
628            if seen.is_empty() {
629                return Err(format!(
630                    "{what} runs once per item and states no count it was seen with"
631                ));
632            }
633            match items {
634                Items::Unknown { why } if why.trim().is_empty() => {
635                    return Err(format!(
636                        "{what} runs once per item of an unknown kind with no why"
637                    ));
638                }
639                Items::Unknown { .. } => {}
640                Items::LocalShadowFaces => self.face_step_ok(p, &what)?,
641                Items::ShadowedLights { .. } if self.local_shadow_atlas.is_none() => {
642                    return Err(format!(
643                        "{what} runs once per shadowed light in a graph with no local shadow atlas"
644                    ));
645                }
646                Items::ShadowedLights { .. } => {}
647            }
648        }
649        match &p.work {
650            PassWork::Geometry { stages, outputs, .. } => {
651                self.outputs_ok(outputs, &what)?;
652                self.stage_targets_ok(&stages.iter().collect::<Vec<_>>(), &what)?;
653            }
654            PassWork::Screen {
655                vertices,
656                outputs,
657                stages,
658                region,
659                ..
660            } => {
661                if *vertices == 0 {
662                    return Err(format!("{what} draws no vertex"));
663                }
664                self.outputs_ok(outputs, &what)?;
665                if let Some(region) = region {
666                    self.screen_region_ok(outputs, region, &what)?;
667                }
668                self.stage_targets_ok(&stages.iter().collect::<Vec<_>>(), &what)?;
669            }
670            PassWork::Compute {
671                writes,
672                groups,
673                stage,
674                buffer_writes,
675            } => {
676                for (_, v) in writes {
677                    self.view_ok(v, &what)?;
678                }
679                for (_, b) in buffer_writes {
680                    self.buffer_ok(*b, &what)?;
681                }
682                if let Groups::Cover { target, mip, group } = groups {
683                    self.view_ok(
684                        &TargetView {
685                            target: *target,
686                            mip: *mip,
687                            first_layer: 0,
688                            layers: 0,
689                        },
690                        &what,
691                    )?;
692                    if group.contains(&0) {
693                        return Err(format!("{what} covers with an empty group"));
694                    }
695                }
696                self.stage_targets_ok(&[stage], &what)?;
697            }
698            PassWork::Scene {
699                outputs,
700                view,
701                draw,
702                members,
703                frame,
704                sorting,
705                ..
706            } => {
707                if draw.trim().is_empty() {
708                    return Err(format!("{what} draws the scene and names no draw"));
709                }
710                if let SceneMembers::NotRead { why } = members
711                    && why.trim().is_empty()
712                {
713                    return Err(format!(
714                        "{what} does not read what its draw draws and says not why"
715                    ));
716                }
717                for (i, (role, v)) in frame.iter().enumerate() {
718                    if frame[..i].iter().any(|(r, _)| r == role) {
719                        return Err(format!("{what} gives the frame texture {role:?} twice"));
720                    }
721                    self.view_ok(v, &format!("{what} frame texture {role:?}"))?;
722                    let target = &self.targets[v.target as usize];
723                    let dim = match target.shape {
724                        TargetShape::Plane | TargetShape::Array { .. } => TextureDim::D2,
725                        TargetShape::Volume => TextureDim::D3,
726                        TargetShape::Cube => TextureDim::Cube,
727                    };
728                    if dim != role.dim() {
729                        return Err(format!(
730                            "{what} gives the frame texture {role:?}, a {:?} image, as target {}, a {:?}",
731                            role.dim(),
732                            target.name,
733                            target.shape
734                        ));
735                    }
736                }
737                match sorting {
738                    DrawSorting::Keys(keys) => {
739                        if keys.is_empty() {
740                            return Err(format!("{what} sorts its draws by no key"));
741                        }
742                        for (i, k) in keys.iter().enumerate() {
743                            if keys[..i].contains(k) {
744                                return Err(format!("{what} sorts by {k:?} twice"));
745                            }
746                        }
747                    }
748                    DrawSorting::NotStated { why } if why.trim().is_empty() => {
749                        return Err(format!("{what} states no order of its draws and no why"));
750                    }
751                    DrawSorting::NotStated { .. } => {}
752                }
753                self.outputs_ok(outputs, &what)?;
754                match view {
755                    SceneView::ShadowCascade { region, .. } => self.region_ok(outputs, region, &what)?,
756                    SceneView::LocalShadow if outputs.depth.is_none() => {
757                        return Err(format!(
758                            "{what} draws a local light's shadow with no depth target"
759                        ));
760                    }
761                    SceneView::LocalShadow | SceneView::Camera => {}
762                }
763            }
764            PassWork::Slot { outputs, .. } => self.outputs_ok(outputs, &what)?,
765            PassWork::Clear { target, value } => {
766                self.view_ok(target, &what)?;
767                if !value.iter().all(|x| x.is_finite()) {
768                    return Err(format!("{what} clears to a value that is not finite"));
769                }
770            }
771            PassWork::Copy { from, to } => {
772                self.view_ok(from, &what)?;
773                self.view_ok(to, &what)?;
774            }
775            PassWork::ClearBuffer { buffer, .. } => self.buffer_ok(*buffer, &what)?,
776            PassWork::CopyBuffer { from, to } => {
777                self.buffer_ok(*from, &what)?;
778                self.buffer_ok(*to, &what)?;
779            }
780            PassWork::Group { steps } => {
781                if steps.is_empty() {
782                    return Err(format!("{what} is a group of no step"));
783                }
784                for s in steps {
785                    self.pass_ok(s)?;
786                }
787            }
788        }
789        Ok(())
790    }
791    /// The targets are well formed (a size and at least one mip), and the buffers (some bytes), every view names a target
792    /// and a mip it has and every buffer reference a buffer, colour and depth go to targets of their kind, a step in some
793    /// frames names them, a repeated step states its counts, a scene step names its draw (the steps that make one draw
794    /// drawing the same surfaces), why it does not read what the draw draws where it does not, gives each frame texture
795    /// once as a target of the role's shape and states its sort keys once each (or why it states none), what is left
796    /// out is named, and the picture is a target.
797    pub fn check(&self) -> Result<(), String> {
798        for t in &self.targets {
799            if t.mips == 0 {
800                return Err(format!("target {} has no mip", t.name));
801            }
802            match t.size {
803                TargetSize::View {
804                    numerator,
805                    denominator,
806                    depth,
807                    ..
808                } if numerator == 0 || denominator == 0 || depth == 0 => {
809                    return Err(format!("target {} has a zero size factor", t.name));
810                }
811                TargetSize::Fixed { width, height, depth } if width == 0 || height == 0 || depth == 0 => {
812                    return Err(format!("target {} has a zero size", t.name));
813                }
814                _ => {}
815            }
816        }
817        for b in &self.buffers {
818            let bytes = match b.size {
819                BufferSize::Fixed { bytes } | BufferSize::Recorded { bytes, .. } => bytes,
820            };
821            if bytes == 0 {
822                return Err(format!("buffer {} has no byte", b.name));
823            }
824            if let BufferSize::Recorded { view, .. } = b.size
825                && view.contains(&0)
826            {
827                return Err(format!("buffer {} was recorded at an empty view", b.name));
828            }
829            if b.stride == Some(0) || b.elements == Some(0) {
830                return Err(format!(
831                    "buffer {} states elements of no byte or no element",
832                    b.name
833                ));
834            }
835        }
836        for p in &self.passes {
837            self.pass_ok(p)?;
838            // a shadowed light's values are a run's of a step run once per shadowed light (its group's steps included)
839            if !matches!(
840                p.runs,
841                Runs::Each {
842                    items: Items::ShadowedLights { .. },
843                    ..
844                }
845            ) && let Some(c) = item_constant(p)
846            {
847                return Err(format!(
848                    "pass {} binds {} to a shadowed light's value, and does not run once per shadowed light",
849                    p.name,
850                    c.name.as_deref().unwrap_or("a constant")
851                ));
852            }
853        }
854        if let Some(atlas) = &self.local_shadow_atlas {
855            atlas.verify()?;
856            let t = self.targets.get(atlas.target as usize).ok_or_else(|| {
857                format!(
858                    "the local shadow atlas names target {} of {}",
859                    atlas.target,
860                    self.targets.len()
861                )
862            })?;
863            match t.size {
864                TargetSize::Fixed {
865                    width,
866                    height,
867                    depth: 1,
868                } if t.format.is_depth() && [width, height] == atlas.size => {}
869                _ => {
870                    return Err(format!(
871                        "the local shadow atlas is {} x {} of depth, and its target {} is not",
872                        atlas.size[0], atlas.size[1], t.name
873                    ));
874                }
875            }
876        }
877        let draws = self.scene_draws();
878        for (i, (draw, members)) in draws.iter().enumerate() {
879            if draws[..i].iter().any(|(d, m)| d == draw && m != members) {
880                return Err(format!(
881                    "the steps that make the draw {draw} draw different surfaces"
882                ));
883            }
884        }
885        if self.left_out.iter().any(|l| l.trim().is_empty()) {
886            return Err("a step left out of the graph without a name".into());
887        }
888        if let Some(p) = self.picture {
889            self.view_ok(
890                &TargetView {
891                    target: p,
892                    mip: 0,
893                    first_layer: 0,
894                    layers: 0,
895                },
896                "the picture",
897            )?;
898        }
899        if let Some(lighting) = &self.lighting {
900            lighting.check(self)?;
901        }
902        Ok(())
903    }
904
905    /// The stage bindings of the steps, a group's steps' included, each with its step's name.
906    fn stages(&self) -> Vec<(&str, &StageBinding)> {
907        fn walk<'a>(passes: &'a [RenderPass], out: &mut Vec<(&'a str, &'a StageBinding)>) {
908            for p in passes {
909                match &p.work {
910                    PassWork::Screen { stages, .. } | PassWork::Geometry { stages, .. } => {
911                        out.extend(stages.iter().map(|s| (p.name.as_str(), s)))
912                    }
913                    PassWork::Compute { stage, .. } => out.push((p.name.as_str(), stage)),
914                    PassWork::Group { steps } => walk(steps, out),
915                    PassWork::Scene { .. }
916                    | PassWork::Clear { .. }
917                    | PassWork::Copy { .. }
918                    | PassWork::ClearBuffer { .. }
919                    | PassWork::CopyBuffer { .. }
920                    | PassWork::Slot { .. } => {}
921                }
922            }
923        }
924        let mut out = Vec::new();
925        walk(&self.passes, &mut out);
926        out
927    }
928
929    /// The draws the scene steps make, a group's steps' included, each with the surfaces it draws: a draw several steps
930    /// make is listed once for each.
931    pub fn scene_draws(&self) -> Vec<(&str, &SceneMembers)> {
932        fn walk<'a>(passes: &'a [RenderPass], out: &mut Vec<(&'a str, &'a SceneMembers)>) {
933            for p in passes {
934                match &p.work {
935                    PassWork::Scene { draw, members, .. } => out.push((draw.as_str(), members)),
936                    PassWork::Group { steps } => walk(steps, out),
937                    _ => {}
938                }
939            }
940        }
941        let mut out = Vec::new();
942        walk(&self.passes, &mut out);
943        out
944    }
945
946    /// Keep the steps that draw with entities `keep` accepts alone; drop each other top-level step whole (a group's
947    /// steps run together, so a group goes with any of its steps). The lighting pipeline goes where it selected a
948    /// dropped step, and its other selections are renumbered; the picture goes where a dropped step wrote it. The
949    /// targets and buffers stay. The steps left draw what they drew: a step reading what a dropped one made reads
950    /// the target as the frame leaves it, as the graph states for a step a frame does not have.
951    pub fn retain_steps(&mut self, keep: &dyn Fn(Entity) -> bool) -> StepsDropped {
952        let mut dropped = StepsDropped::default();
953        let mut renumbered = Vec::with_capacity(self.passes.len());
954        let mut kept = Vec::with_capacity(self.passes.len());
955        for pass in std::mem::take(&mut self.passes) {
956            let mut references = Vec::new();
957            pass_references(&pass, &mut references);
958            match references.into_iter().find(|e| !keep(*e)) {
959                Some(gone) => {
960                    renumbered.push(None);
961                    if self.picture.is_some_and(|t| writes_target(&pass, t)) {
962                        dropped.picture = true;
963                    }
964                    dropped.steps.push((pass.name, gone));
965                }
966                None => {
967                    renumbered.push(u32::try_from(kept.len()).ok());
968                    kept.push(pass);
969                }
970            }
971        }
972        self.passes = kept;
973        if dropped.picture {
974            self.picture = None;
975        }
976        if let Some(lighting) = &mut self.lighting {
977            let selected: Option<Vec<u32>> = lighting
978                .passes
979                .iter()
980                .map(|&i| renumbered.get(i as usize).copied().flatten())
981                .collect();
982            match selected {
983                Some(passes) => lighting.passes = passes,
984                None => {
985                    self.lighting = None;
986                    dropped.lighting = true;
987                }
988            }
989        }
990        dropped
991    }
992
993    fn objects(&self) -> Vec<Entity> {
994        fn walk(passes: &[RenderPass], out: &mut Vec<Entity>) {
995            for pass in passes {
996                match &pass.work {
997                    PassWork::Geometry { object, .. } => out.push(*object),
998                    PassWork::Group { steps } => walk(steps, out),
999                    _ => {}
1000                }
1001            }
1002        }
1003        let mut out = Vec::new();
1004        walk(&self.passes, &mut out);
1005        out
1006    }
1007
1008    /// A stage's texture slots that bind a target read it as the target's shape: a plane as a 2D texture, an array of
1009    /// planes as an arrayed one, a volume as a 3D texture, a cube as a cube.
1010    fn target_dims_ok(&self, step: &str, s: &StageBinding, interface: &Interface) -> Result<(), String> {
1011        for t in &s.textures {
1012            let TextureSource::Target { view } = &t.source else {
1013                continue;
1014            };
1015            let Some(target) = self.targets.get(view.target as usize) else {
1016                continue;
1017            };
1018            let Some(ResourceKind::Texture { dim, arrayed, .. }) =
1019                interface.resource(t.resource).map(|r| &r.kind)
1020            else {
1021                continue;
1022            };
1023            let shape = match target.shape {
1024                TargetShape::Plane => (TextureDim::D2, false),
1025                TargetShape::Array { .. } => (TextureDim::D2, true),
1026                TargetShape::Volume => (TextureDim::D3, false),
1027                TargetShape::Cube => (TextureDim::Cube, false),
1028            };
1029            if (*dim, *arrayed) != shape {
1030                return Err(format!(
1031                    "pass {step} {:?} texture slot {} is a {dim:?} texture (arrayed {arrayed}) and binds target {}, a {:?}",
1032                    s.stage, t.resource.0, target.name, target.shape
1033                ));
1034            }
1035        }
1036        Ok(())
1037    }
1038}
1039
1040/// A constant of a step's programs (its group's steps' included) bound to a shadowed light's value
1041/// ([`crate::shader::ConstantRole::Item`]).
1042fn item_constant(p: &RenderPass) -> Option<&crate::shader::ConstantBinding> {
1043    let stages: Vec<&StageBinding> = match &p.work {
1044        PassWork::Screen { stages, .. } | PassWork::Geometry { stages, .. } => stages.iter().collect(),
1045        PassWork::Compute { stage, .. } => vec![stage],
1046        PassWork::Group { steps } => return steps.iter().find_map(item_constant),
1047        _ => Vec::new(),
1048    };
1049    stages
1050        .into_iter()
1051        .flat_map(|s| &s.constants)
1052        .find(|c| matches!(c.role, crate::shader::ConstantRole::Item(_)))
1053}
1054
1055impl Resource for RenderGraph {
1056    const NAME: &'static str = "core.render_graph";
1057    fn to_json(&self) -> Result<Option<Value>, JsonError> {
1058        crate::json::to_value(self).map(Some)
1059    }
1060    /// [`RenderGraph::check`], and every step's stage binding verified against its program as a material's is, with the
1061    /// graph's targets allowed as textures (a slot reads a target as the target's shape).
1062    fn verify(&self, w: &World) -> Result<(), String> {
1063        self.check()?;
1064        for object in self.objects() {
1065            let transform = w
1066                .get::<crate::Transform>(object)
1067                .ok_or("graph geometry object has no transform")?;
1068            if !transform.cols.iter().flatten().all(|v| v.is_finite())
1069                || crate::Linear::of(transform).singular()
1070            {
1071                return Err("graph geometry object has a nonfinite or singular transform".into());
1072            }
1073        }
1074        for (step, s) in self.stages() {
1075            s.verify_in(w, BindingPlace::GraphStep)
1076                .map_err(|e| format!("pass {step}: {e}"))?;
1077            if let Some(shader) = w.get::<crate::shader::Shader>(s.shader) {
1078                self.target_dims_ok(step, s, &shader.interface)?;
1079            }
1080        }
1081        Ok(())
1082    }
1083    fn references(&self, out: &mut Vec<Entity>) {
1084        for pass in &self.passes {
1085            pass_references(pass, out);
1086        }
1087    }
1088    /// The steps that draw with an entity that left go ([`RenderGraph::retain_steps`]); the world counts them and the
1089    /// step bindings that named their draws ([`crate::Dropped`]).
1090    fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
1091        self.retain_steps(keep);
1092    }
1093}
1094
1095/// What [`RenderGraph::retain_steps`] dropped.
1096#[derive(Clone, Debug, Default, PartialEq, Eq)]
1097pub struct StepsDropped {
1098    /// Each top-level step dropped, by name, with an entity it drew with that left.
1099    pub steps: Vec<(String, Entity)>,
1100    /// The lighting pipeline, which selected a dropped step.
1101    pub lighting: bool,
1102    /// The picture, which a dropped step wrote.
1103    pub picture: bool,
1104}
1105
1106impl StepsDropped {
1107    pub fn is_empty(&self) -> bool {
1108        self.steps.is_empty() && !self.lighting && !self.picture
1109    }
1110}
1111
1112/// The entities a step draws with, a group's steps' included: its programs' `core.shader` entities, the textures its
1113/// bindings sample and a geometry step's object.
1114fn pass_references(pass: &RenderPass, out: &mut Vec<Entity>) {
1115    let stage = |s: &StageBinding, out: &mut Vec<Entity>| {
1116        out.push(s.shader);
1117        for texture in &s.textures {
1118            if let TextureSource::Texture { texture, .. } = texture.source {
1119                out.push(texture);
1120            }
1121        }
1122    };
1123    match &pass.work {
1124        PassWork::Geometry { object, stages, .. } => {
1125            out.push(*object);
1126            for s in stages {
1127                stage(s, out);
1128            }
1129        }
1130        PassWork::Screen { stages, .. } => {
1131            for s in stages {
1132                stage(s, out);
1133            }
1134        }
1135        PassWork::Compute { stage: s, .. } => stage(s, out),
1136        PassWork::Group { steps } => {
1137            for step in steps {
1138                pass_references(step, out);
1139            }
1140        }
1141        PassWork::Scene { .. }
1142        | PassWork::Clear { .. }
1143        | PassWork::Copy { .. }
1144        | PassWork::ClearBuffer { .. }
1145        | PassWork::CopyBuffer { .. }
1146        | PassWork::Slot { .. } => {}
1147    }
1148}
1149
1150/// Whether a step (a group's steps included) writes target `target`.
1151fn writes_target(pass: &RenderPass, target: TargetId) -> bool {
1152    let outputs = |o: &Outputs| {
1153        o.colour.iter().any(|(_, v)| v.target == target)
1154            || o.depth.as_ref().is_some_and(|d| d.view.target == target)
1155    };
1156    match &pass.work {
1157        PassWork::Geometry { outputs: o, .. }
1158        | PassWork::Screen { outputs: o, .. }
1159        | PassWork::Scene { outputs: o, .. }
1160        | PassWork::Slot { outputs: o, .. } => outputs(o),
1161        PassWork::Compute { writes, .. } => writes.iter().any(|(_, v)| v.target == target),
1162        PassWork::Clear { target: v, .. } | PassWork::Copy { to: v, .. } => v.target == target,
1163        PassWork::Group { steps } => steps.iter().any(|s| writes_target(s, target)),
1164        PassWork::ClearBuffer { .. } | PassWork::CopyBuffer { .. } => false,
1165    }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170    use super::*;
1171
1172    #[test]
1173    fn graph_volume_draw_retains_its_object_and_requires_a_nonsingular_transform() {
1174        let mut world = World::new();
1175        let object = world.spawn();
1176        let mut graph = RenderGraph {
1177            targets: vec![target("hdr", TargetFormat::Rgba16Float)],
1178            passes: vec![RenderPass {
1179                name: "probe volume".into(),
1180                shared: false,
1181                presence: Presence::Always,
1182                runs: Runs::Once,
1183                work: PassWork::Geometry {
1184                    primitive: GraphPrimitive::UnitBox,
1185                    object,
1186                    stages: Vec::new(),
1187                    state: RenderState::default(),
1188                    outputs: Outputs {
1189                        colour: vec![(0, view(0))],
1190                        depth: None,
1191                    },
1192                },
1193            }],
1194            lighting: Some(LightingPipeline {
1195                passes: vec![0],
1196                exports: vec![(LightingPort::HdrColour, view(0))],
1197                ..LightingPipeline::default()
1198            }),
1199            ..RenderGraph::default()
1200        };
1201        assert!(graph.verify(&world).unwrap_err().contains("no transform"));
1202        world.insert(object, crate::Transform::IDENTITY);
1203        assert_eq!(graph.verify(&world), Ok(()));
1204        let mut references = Vec::new();
1205        graph.references(&mut references);
1206        assert_eq!(references, vec![object]);
1207        let mut singular = crate::Transform::IDENTITY;
1208        singular.cols[0] = [0.; 3];
1209        world.insert(object, singular);
1210        assert!(graph.verify(&world).unwrap_err().contains("singular"));
1211        graph.retain_references(&|e| e != object);
1212        assert!(graph.passes.is_empty() && graph.lighting.is_none());
1213    }
1214
1215    fn target(name: &str, format: TargetFormat) -> RenderTarget {
1216        RenderTarget {
1217            name: name.into(),
1218            format,
1219            size: TargetSize::View {
1220                numerator: 1,
1221                denominator: 1,
1222                depth: 1,
1223                rounding: Rounding::Down,
1224            },
1225            shape: TargetShape::Plane,
1226            mips: 1,
1227            history: false,
1228            shared: false,
1229        }
1230    }
1231    fn view(target: TargetId) -> TargetView {
1232        TargetView {
1233            target,
1234            mip: 0,
1235            first_layer: 0,
1236            layers: 0,
1237        }
1238    }
1239
1240    #[test]
1241    fn views_name_targets_and_mips_they_have_and_depth_goes_to_depth() {
1242        let mut g = RenderGraph {
1243            targets: vec![
1244                target("light", TargetFormat::Rg11B10Float),
1245                target("depth", TargetFormat::Depth32Stencil8),
1246            ],
1247            buffers: Vec::new(),
1248            passes: vec![RenderPass {
1249                name: "clear".into(),
1250                shared: false,
1251                presence: Presence::Always,
1252                runs: Runs::Once,
1253                work: PassWork::Clear {
1254                    target: view(0),
1255                    value: [0.0; 4],
1256                },
1257            }],
1258            picture: Some(0),
1259            lighting: None,
1260            local_shadow_atlas: None,
1261            left_out: Vec::new(),
1262        };
1263        assert_eq!(g.check(), Ok(()));
1264        g.passes[0].work = PassWork::Clear {
1265            target: TargetView { mip: 1, ..view(0) },
1266            value: [0.0; 4],
1267        };
1268        assert_eq!(
1269            g.check(),
1270            Err("pass clear names mip 1 of target light (1 mips)".into())
1271        );
1272        g.passes[0].work = PassWork::Slot {
1273            slot: FeatureSlot::Effects,
1274            outputs: Outputs {
1275                colour: vec![(0, view(1))],
1276                depth: None,
1277            },
1278        };
1279        assert_eq!(
1280            g.check(),
1281            Err("pass clear target 0 renders colour into the depth target depth".into())
1282        );
1283        g.passes[0].work = PassWork::Slot {
1284            slot: FeatureSlot::Effects,
1285            outputs: Outputs {
1286                colour: vec![(0, view(0))],
1287                depth: Some(DepthView {
1288                    view: view(0),
1289                    read_only_depth: true,
1290                    read_only_stencil: true,
1291                }),
1292            },
1293        };
1294        assert_eq!(g.check(), Err("pass clear depth uses light as depth".into()));
1295        g.picture = Some(5);
1296        g.passes.clear();
1297        assert_eq!(g.check(), Err("the picture names target 5 of 2".into()));
1298    }
1299
1300    /// Sekiro's shadow atlas (a recorded frame: one 4096 x 4096 R16 depth target, its cascades' draws in the four
1301    /// 2048 x 2048 quarters): a cascade's region must be inside the atlas.
1302    /// A scene step names its draw, gives each frame texture once as a target of the role's shape, and sorts by each key
1303    /// once or says why it states no order; what is left out of the graph is named.
1304    #[test]
1305    fn a_scene_step_names_its_draw_its_frame_textures_and_its_order() {
1306        let mut cube = target("probe", TargetFormat::Rgba16Float);
1307        cube.shape = TargetShape::Cube;
1308        let step = |draw: &str, frame: Vec<(FrameTexture, TargetView)>, sorting: DrawSorting| RenderPass {
1309            name: "opaque".into(),
1310            shared: false,
1311            presence: Presence::Always,
1312            runs: Runs::Once,
1313            work: PassWork::Scene {
1314                bucket: DrawBucket::Forward,
1315                view: SceneView::Camera,
1316                outputs: Outputs {
1317                    colour: vec![(0, view(0))],
1318                    depth: Some(DepthView {
1319                        view: view(1),
1320                        read_only_depth: false,
1321                        read_only_stencil: false,
1322                    }),
1323                },
1324                draw: draw.into(),
1325                members: SceneMembers::Stated {
1326                    placements: ScenePlacements::All,
1327                },
1328                frame,
1329                sorting,
1330            },
1331        };
1332        let opaque = || DrawSorting::Keys(vec![SortKey::RenderQueue, SortKey::QuantizedFrontToBack]);
1333        let mut g = RenderGraph {
1334            targets: vec![
1335                target("colour", TargetFormat::Rgba16Float),
1336                target("depth", TargetFormat::Depth32Stencil8),
1337                target("prepass depth", TargetFormat::Depth32Stencil8),
1338                cube,
1339            ],
1340            buffers: Vec::new(),
1341            passes: vec![step(
1342                "forward_opaque",
1343                vec![(FrameTexture::SceneDepth, view(2))],
1344                opaque(),
1345            )],
1346            picture: Some(0),
1347            lighting: None,
1348            local_shadow_atlas: None,
1349            left_out: vec!["post-processing: not traced".into()],
1350        };
1351        assert_eq!(g.check(), Ok(()));
1352        assert_eq!(
1353            g.scene_draws(),
1354            vec![(
1355                "forward_opaque",
1356                &SceneMembers::Stated {
1357                    placements: ScenePlacements::All
1358                }
1359            )]
1360        );
1361        let refused = |p: RenderPass| {
1362            let mut g = g.clone();
1363            g.passes = vec![p];
1364            g.check().unwrap_err()
1365        };
1366        assert_eq!(
1367            refused(step(" ", Vec::new(), opaque())),
1368            "pass opaque draws the scene and names no draw"
1369        );
1370        assert_eq!(
1371            refused(step(
1372                "forward_opaque",
1373                vec![
1374                    (FrameTexture::SceneDepth, view(2)),
1375                    (FrameTexture::SceneDepth, view(1))
1376                ],
1377                opaque()
1378            )),
1379            "pass opaque gives the frame texture SceneDepth twice"
1380        );
1381        assert!(
1382            refused(step(
1383                "forward_opaque",
1384                vec![(FrameTexture::DecalBuffer(0), view(3))],
1385                opaque()
1386            ))
1387            .contains("a D2 image, as target probe, a Cube")
1388        );
1389        assert_eq!(
1390            refused(step("forward_opaque", Vec::new(), DrawSorting::Keys(Vec::new()))),
1391            "pass opaque sorts its draws by no key"
1392        );
1393        assert_eq!(
1394            refused(step(
1395                "forward_opaque",
1396                Vec::new(),
1397                DrawSorting::Keys(vec![SortKey::RenderQueue, SortKey::RenderQueue])
1398            )),
1399            "pass opaque sorts by RenderQueue twice"
1400        );
1401        assert_eq!(
1402            refused(step(
1403                "forward_opaque",
1404                Vec::new(),
1405                DrawSorting::NotStated { why: String::new() }
1406            )),
1407            "pass opaque states no order of its draws and no why"
1408        );
1409        g.left_out.push(String::new());
1410        assert_eq!(
1411            g.check(),
1412            Err("a step left out of the graph without a name".into())
1413        );
1414    }
1415
1416    #[test]
1417    fn a_shadow_cascade_draws_into_a_region_of_its_atlas() {
1418        let mut atlas = target("shadow", TargetFormat::Depth16);
1419        atlas.size = TargetSize::Fixed {
1420            width: 4096,
1421            height: 4096,
1422            depth: 1,
1423        };
1424        let cascade = |region: Region| RenderPass {
1425            name: "cascade".into(),
1426            shared: true,
1427            presence: Presence::Always,
1428            runs: Runs::Once,
1429            work: PassWork::Scene {
1430                bucket: DrawBucket::ShadowCaster,
1431                view: SceneView::ShadowCascade { cascade: 3, region },
1432                draw: "shadow_caster".into(),
1433                members: SceneMembers::Stated {
1434                    placements: ScenePlacements::All,
1435                },
1436                frame: Vec::new(),
1437                sorting: DrawSorting::NotStated {
1438                    why: "a fixture".into(),
1439                },
1440                outputs: Outputs {
1441                    colour: Vec::new(),
1442                    depth: Some(DepthView {
1443                        view: view(0),
1444                        read_only_depth: false,
1445                        read_only_stencil: false,
1446                    }),
1447                },
1448            },
1449        };
1450        let quarter = Region {
1451            x: 2048,
1452            y: 2048,
1453            width: 2048,
1454            height: 2048,
1455        };
1456        let mut g = RenderGraph {
1457            targets: vec![atlas],
1458            buffers: Vec::new(),
1459            passes: vec![cascade(quarter)],
1460            picture: None,
1461            lighting: None,
1462            local_shadow_atlas: None,
1463            left_out: Vec::new(),
1464        };
1465        assert_eq!(g.check(), Ok(()));
1466        g.passes[0] = cascade(Region { x: 2049, ..quarter });
1467        assert_eq!(
1468            g.check(),
1469            Err("pass cascade draws a shadow cascade outside its depth target (4096 x 4096)".into())
1470        );
1471        g.passes[0] = cascade(Region { width: 0, ..quarter });
1472        assert_eq!(
1473            g.check(),
1474            Err("pass cascade draws a shadow cascade into an empty region".into())
1475        );
1476    }
1477
1478    /// A step only some frames have names them, a repeated step states the counts it was seen with and why its items
1479    /// are unknown, and a group holds steps, each checked in turn.
1480    #[test]
1481    fn a_step_in_some_frames_and_a_repeated_group_state_their_evidence() {
1482        let clear = |name: &str| RenderPass {
1483            name: name.into(),
1484            shared: false,
1485            presence: Presence::Always,
1486            runs: Runs::Once,
1487            work: PassWork::Clear {
1488                target: view(0),
1489                value: [0.0; 4],
1490            },
1491        };
1492        let mut g = RenderGraph {
1493            targets: vec![target("light", TargetFormat::Rg11B10Float)],
1494            buffers: Vec::new(),
1495            passes: vec![clear("a")],
1496            picture: Some(0),
1497            lighting: None,
1498            local_shadow_atlas: None,
1499            left_out: Vec::new(),
1500        };
1501        g.passes[0].presence = Presence::Unknown { frames: Vec::new() };
1502        assert_eq!(g.check(), Err("pass a is in some frames, and names none".into()));
1503        g.passes[0].presence = Presence::Unknown {
1504            frames: vec!["frame 1".into()],
1505        };
1506        assert_eq!(g.check(), Ok(()));
1507        g.passes[0].runs = Runs::Each {
1508            items: Items::Unknown {
1509                why: "regions".into(),
1510            },
1511            seen: Vec::new(),
1512        };
1513        assert_eq!(
1514            g.check(),
1515            Err("pass a runs once per item and states no count it was seen with".into())
1516        );
1517        g.passes[0].runs = Runs::Each {
1518            items: Items::Unknown { why: " ".into() },
1519            seen: vec![3],
1520        };
1521        assert_eq!(
1522            g.check(),
1523            Err("pass a runs once per item of an unknown kind with no why".into())
1524        );
1525        g.passes[0].runs = Runs::Each {
1526            items: Items::Unknown {
1527                why: "regions".into(),
1528            },
1529            seen: vec![3, 12],
1530        };
1531        assert_eq!(g.check(), Ok(()));
1532        // a group: not empty, and its steps are checked as the graph's own
1533        g.passes[0].work = PassWork::Group { steps: Vec::new() };
1534        assert_eq!(g.check(), Err("pass a is a group of no step".into()));
1535        let mut inner = clear("b");
1536        inner.work = PassWork::Clear {
1537            target: view(4),
1538            value: [0.0; 4],
1539        };
1540        g.passes[0].work = PassWork::Group {
1541            steps: vec![clear("c"), inner],
1542        };
1543        assert_eq!(g.check(), Err("pass b names target 4 of 1".into()));
1544    }
1545
1546    /// A step's texture slot that binds one of the graph's targets names a target and a mip the graph has.
1547    #[test]
1548    fn a_step_reads_targets_the_graph_has() {
1549        let mut w = World::new();
1550        let program = w.spawn();
1551        let read = |target: TargetId, mip: u32| crate::shader::TextureBinding {
1552            resource: ResourceId(0),
1553            sampler: None,
1554            source: TextureSource::Target {
1555                view: TargetView { mip, ..view(target) },
1556            },
1557        };
1558        let stage = |target: TargetId, mip: u32| StageBinding {
1559            textures: vec![read(target, mip)],
1560            ..StageBinding::new(iridia_interface::Stage::Pixel, program)
1561        };
1562        let screen = |target: TargetId, mip: u32| RenderPass {
1563            name: "composite".into(),
1564            shared: false,
1565            presence: Presence::Always,
1566            runs: Runs::Once,
1567            work: PassWork::Screen {
1568                vertices: 3,
1569                stages: vec![stage(target, mip)],
1570                state: RenderState::default(),
1571                outputs: Outputs {
1572                    colour: vec![(0, view(0))],
1573                    depth: None,
1574                },
1575                region: None,
1576            },
1577        };
1578        let mut g = RenderGraph {
1579            targets: vec![
1580                target("light", TargetFormat::Rg11B10Float),
1581                target("fog", TargetFormat::Rgba16Float),
1582            ],
1583            buffers: Vec::new(),
1584            passes: vec![screen(1, 0)],
1585            picture: Some(0),
1586            lighting: None,
1587            local_shadow_atlas: None,
1588            left_out: Vec::new(),
1589        };
1590        assert_eq!(g.check(), Ok(()));
1591        g.passes[0] = screen(2, 0);
1592        assert_eq!(
1593            g.check(),
1594            Err("pass composite Pixel texture slot 0 names target 2 of 2".into())
1595        );
1596        g.passes[0] = screen(1, 1);
1597        assert_eq!(
1598            g.check(),
1599            Err("pass composite Pixel texture slot 0 names mip 1 of target fog (1 mips)".into())
1600        );
1601    }
1602
1603    /// Sekiro's cascade copies (a recorded frame: four full-screen draws into the four 2048 x 2048 quarters of the 4096 x
1604    /// 4096 shadow atlas, each by its viewport): a full-screen draw's region is inside its fixed-size outputs.
1605    #[test]
1606    fn a_full_screen_draw_covers_a_region_inside_its_outputs() {
1607        let mut atlas = target("shadow", TargetFormat::Depth16);
1608        atlas.size = TargetSize::Fixed {
1609            width: 4096,
1610            height: 4096,
1611            depth: 1,
1612        };
1613        let copy = |region: Option<Region>| RenderPass {
1614            name: "copy cascade".into(),
1615            shared: true,
1616            presence: Presence::Always,
1617            runs: Runs::Once,
1618            work: PassWork::Screen {
1619                vertices: 3,
1620                stages: Vec::new(),
1621                state: RenderState::default(),
1622                outputs: Outputs {
1623                    colour: Vec::new(),
1624                    depth: Some(DepthView {
1625                        view: view(0),
1626                        read_only_depth: false,
1627                        read_only_stencil: false,
1628                    }),
1629                },
1630                region,
1631            },
1632        };
1633        let quarter = Region {
1634            x: 2048,
1635            y: 0,
1636            width: 2048,
1637            height: 2048,
1638        };
1639        let mut g = RenderGraph {
1640            targets: vec![atlas],
1641            buffers: Vec::new(),
1642            passes: vec![copy(None), copy(Some(quarter))],
1643            picture: None,
1644            lighting: None,
1645            local_shadow_atlas: None,
1646            left_out: Vec::new(),
1647        };
1648        assert_eq!(g.check(), Ok(()));
1649        g.passes[1] = copy(Some(Region { y: 2049, ..quarter }));
1650        assert_eq!(
1651            g.check(),
1652            Err("pass copy cascade draws outside its target shadow (4096 x 4096)".into())
1653        );
1654        g.passes[1] = copy(Some(Region { height: 0, ..quarter }));
1655        assert_eq!(
1656            g.check(),
1657            Err("pass copy cascade draws into an empty region".into())
1658        );
1659    }
1660
1661    /// Sekiro's light lists (a recorded frame at 1920 x 1080: GXLightOrganize16 writes four buffers, 522,240 bytes the
1662    /// first, that the tiled lighting reads): a buffer has bytes, a step names buffers the graph has, and a buffer's clear
1663    /// and copy name buffers too.
1664    #[test]
1665    fn steps_write_and_read_buffers_the_graph_has() {
1666        let mut w = World::new();
1667        let program = w.spawn();
1668        let lists = RenderBuffer {
1669            name: "GXLightOrganize16 u0".into(),
1670            size: BufferSize::Recorded {
1671                bytes: 522_240,
1672                view: [1920, 1080],
1673            },
1674            stride: None,
1675            elements: None,
1676            history: false,
1677            shared: false,
1678        };
1679        let organize = |buffer: BufferId| RenderPass {
1680            name: "GXLightOrganize16".into(),
1681            shared: false,
1682            presence: Presence::Always,
1683            runs: Runs::Once,
1684            work: PassWork::Compute {
1685                stage: StageBinding {
1686                    buffers: vec![crate::shader::BufferBinding {
1687                        resource: ResourceId(1),
1688                        source: BufferSource::Graph(buffer),
1689                    }],
1690                    ..StageBinding::new(iridia_interface::Stage::Compute, program)
1691                },
1692                groups: Groups::Fixed([120, 68, 1]),
1693                writes: Vec::new(),
1694                buffer_writes: vec![(ResourceId(0), buffer)],
1695            },
1696        };
1697        let step = |work: PassWork| RenderPass {
1698            name: "lists".into(),
1699            shared: false,
1700            presence: Presence::Always,
1701            runs: Runs::Once,
1702            work,
1703        };
1704        let mut g = RenderGraph {
1705            targets: Vec::new(),
1706            buffers: vec![lists.clone(), lists.clone()],
1707            passes: vec![
1708                step(PassWork::ClearBuffer {
1709                    buffer: 0,
1710                    value: [0; 4],
1711                }),
1712                organize(0),
1713                step(PassWork::CopyBuffer { from: 0, to: 1 }),
1714            ],
1715            picture: None,
1716            lighting: None,
1717            local_shadow_atlas: None,
1718            left_out: Vec::new(),
1719        };
1720        assert_eq!(g.check(), Ok(()));
1721        g.passes[1] = organize(2);
1722        assert_eq!(
1723            g.check(),
1724            Err("pass GXLightOrganize16 names buffer 2 of 2".into())
1725        );
1726        g.passes[1] = organize(0);
1727        g.passes[2] = step(PassWork::CopyBuffer { from: 0, to: 5 });
1728        assert_eq!(g.check(), Err("pass lists names buffer 5 of 2".into()));
1729        g.passes.truncate(2);
1730        g.buffers[1].size = BufferSize::Fixed { bytes: 0 };
1731        assert_eq!(g.check(), Err("buffer GXLightOrganize16 u0 has no byte".into()));
1732    }
1733}