1mod 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
30pub type TargetId = u32;
32pub type BufferId = u32;
34
35#[derive(Clone, Debug, Default, PartialEq, Serialize)]
37pub struct RenderGraph {
38 pub targets: Vec<RenderTarget>,
39 pub buffers: Vec<RenderBuffer>,
41 pub passes: Vec<RenderPass>,
42 pub picture: Option<TargetId>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub lighting: Option<LightingPipeline>,
48 #[serde(skip_serializing_if = "Option::is_none")]
53 pub local_shadow_atlas: Option<crate::shader::LocalShadowAtlas>,
54 #[serde(skip_serializing_if = "Vec::is_empty")]
57 pub left_out: Vec<String>,
58}
59
60#[derive(Clone, Debug, PartialEq, Serialize)]
62pub struct RenderTarget {
63 pub name: String,
65 pub format: TargetFormat,
66 pub size: TargetSize,
67 pub shape: TargetShape,
68 pub mips: u32,
70 pub history: bool,
73 pub shared: bool,
75}
76
77#[derive(Clone, Debug, PartialEq, Serialize)]
80pub struct RenderBuffer {
81 pub name: String,
83 pub size: BufferSize,
84 pub stride: Option<u32>,
86 pub elements: Option<u64>,
88 pub history: bool,
90 pub shared: bool,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "snake_case")]
97pub enum BufferSize {
98 Fixed { bytes: u64 },
100 Recorded { bytes: u64, view: [u32; 2] },
104}
105
106#[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 R8Uint,
127 Depth32Stencil8,
129 Depth32,
131 Depth24Stencil8,
133 Depth16,
135}
136
137impl TargetFormat {
138 pub fn is_depth(self) -> bool {
140 matches!(
141 self,
142 Self::Depth32Stencil8 | Self::Depth32 | Self::Depth24Stencil8 | Self::Depth16
143 )
144 }
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
149#[serde(rename_all = "snake_case")]
150pub enum TargetSize {
151 View {
154 numerator: u32,
155 denominator: u32,
156 depth: u32,
157 rounding: Rounding,
158 },
159 Fixed { width: u32, height: u32, depth: u32 },
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "snake_case")]
166pub enum Rounding {
167 Down,
169 Up,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
175#[serde(rename_all = "snake_case")]
176pub enum TargetShape {
177 Plane,
178 Array {
180 layers: u32,
181 },
182 Volume,
184 Cube,
185}
186
187#[derive(Clone, Debug, PartialEq, Serialize)]
189pub struct RenderPass {
190 pub name: String,
192 pub shared: bool,
195 pub presence: Presence,
198 pub runs: Runs,
200 pub work: PassWork,
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
205#[serde(rename_all = "snake_case")]
206pub enum Presence {
207 Always,
209 When(Trigger),
211 Unknown { frames: Vec<String> },
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
218#[serde(rename_all = "snake_case")]
219pub enum Trigger {
220 LocalShadowCasters,
222 Water,
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
229#[serde(rename_all = "snake_case")]
230pub enum Runs {
231 Once,
233 Each { items: Items, seen: Vec<u32> },
237}
238
239#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
241#[serde(rename_all = "snake_case")]
242pub enum Items {
243 Unknown { why: String },
246 LocalShadowFaces,
251 ShadowedLights { table: crate::shader::LightTableKind },
255}
256
257#[derive(Clone, Debug, PartialEq, Serialize)]
259#[serde(rename_all = "snake_case")]
260pub enum PassWork {
261 Geometry {
265 primitive: GraphPrimitive,
266 object: Entity,
267 stages: Vec<StageBinding>,
268 state: RenderState,
269 outputs: Outputs,
270 },
271 Screen {
275 vertices: u32,
276 stages: Vec<StageBinding>,
277 state: RenderState,
278 outputs: Outputs,
279 region: Option<Region>,
282 },
283 Compute {
285 stage: StageBinding,
286 groups: Groups,
287 writes: Vec<(ResourceId, TargetView)>,
288 buffer_writes: Vec<(ResourceId, BufferId)>,
290 },
291 Scene {
293 bucket: DrawBucket,
295 view: SceneView,
296 outputs: Outputs,
297 draw: String,
300 members: SceneMembers,
302 frame: Vec<(FrameTexture, TargetView)>,
306 sorting: DrawSorting,
308 },
309 Clear { target: TargetView, value: [f32; 4] },
311 Copy { from: TargetView, to: TargetView },
313 ClearBuffer { buffer: BufferId, value: [u32; 4] },
315 CopyBuffer { from: BufferId, to: BufferId },
317 Slot { slot: FeatureSlot, outputs: Outputs },
319 Group { steps: Vec<RenderPass> },
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "snake_case")]
327pub enum GraphPrimitive {
328 UnitBox,
331}
332
333#[derive(Clone, Debug, Default, PartialEq, Serialize)]
335pub struct Outputs {
336 pub colour: Vec<(u32, TargetView)>,
338 pub depth: Option<DepthView>,
339}
340
341#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
343pub struct TargetView {
344 pub target: TargetId,
345 pub mip: u32,
346 pub first_layer: u32,
347 pub layers: u32,
349}
350
351#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
353pub struct DepthView {
354 pub view: TargetView,
355 pub read_only_depth: bool,
357 pub read_only_stencil: bool,
358}
359
360#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
362#[serde(rename_all = "snake_case")]
363pub enum Groups {
364 Fixed([u32; 3]),
365 Cover {
367 target: TargetId,
368 mip: u32,
369 group: [u32; 3],
370 },
371}
372
373#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
376#[serde(rename_all = "snake_case")]
377pub enum DrawBucket {
378 Deferred,
380 Decal,
382 Forward,
384 Sky,
388 ShadowCaster,
390 Velocity,
392}
393
394#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
396#[serde(rename_all = "snake_case")]
397pub enum SceneMembers {
398 Stated { placements: ScenePlacements },
401 NotRead { why: String },
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
408#[serde(rename_all = "snake_case")]
409pub enum ScenePlacements {
410 All,
414 Listed,
417 Casting,
420}
421
422#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
424#[serde(rename_all = "snake_case")]
425pub enum DrawSorting {
426 Keys(Vec<SortKey>),
429 NotStated { why: String },
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
435#[serde(rename_all = "snake_case")]
436pub enum SortKey {
437 SortingLayer,
439 RenderQueue,
441 BackToFront,
443 QuantizedFrontToBack,
446 OptimizeStateChanges,
449 CanvasOrder,
451 RendererPriority,
453}
454
455#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
457#[serde(rename_all = "snake_case")]
458pub enum SceneView {
459 Camera,
461 ShadowCascade { cascade: u8, region: Region },
465 LocalShadow,
470}
471
472#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
483#[serde(rename_all = "snake_case")]
484pub enum FeatureSlot {
485 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 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 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 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 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 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 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 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 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 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 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 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
1040fn 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 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 fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
1091 self.retain_steps(keep);
1092 }
1093}
1094
1095#[derive(Clone, Debug, Default, PartialEq, Eq)]
1097pub struct StepsDropped {
1098 pub steps: Vec<(String, Entity)>,
1100 pub lighting: bool,
1102 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
1112fn 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
1150fn 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 #[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 #[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 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 #[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 #[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 #[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}