concinnity_render/render_graph/
validate.rs1use super::compile::CompiledGraph;
16use super::passes::PassId;
17use super::types::{ReadStages, ResourceState};
18use alloc::vec;
19use alloc::vec::Vec;
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq)]
23pub enum GapKind {
24 UncoveredRead,
27 UncoveredWrite,
30 MissingReadStage,
34}
35
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
38pub struct BarrierGap {
39 pub pass: PassId,
40 pub resource_label: &'static str,
41 pub kind: GapKind,
42}
43
44impl core::fmt::Display for BarrierGap {
45 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46 let what = match self.kind {
47 GapKind::UncoveredRead => "reads",
48 GapKind::UncoveredWrite => "writes",
49 GapKind::MissingReadStage => "reads (stage not in the run union)",
50 };
51 write!(f, "pass {:?} {} {}", self.pass, what, self.resource_label)
52 }
53}
54
55#[cfg(test)]
58pub(crate) fn barrier_coverage_gaps(graph: &CompiledGraph) -> Vec<BarrierGap> {
59 gaps_over(graph, &|_| true)
60}
61
62pub fn barrier_coverage_gaps_for_driven(graph: &CompiledGraph, driven: &[bool]) -> Vec<BarrierGap> {
69 gaps_over(graph, &|i| driven.get(i).copied().unwrap_or(false))
70}
71
72pub fn final_states(graph: &CompiledGraph) -> Vec<(ResourceState, ReadStages)> {
82 let mut state = vec![(ResourceState::Undefined, ReadStages::empty()); graph.resources.len()];
83 for pass in &graph.passes {
84 for op in &pass.barriers_before {
85 state[op.resource_index()] = (op.to_state(), op.read_stages());
86 }
87 }
88 state
89}
90
91fn gaps_over(graph: &CompiledGraph, driven: &dyn Fn(usize) -> bool) -> Vec<BarrierGap> {
92 let mut state = vec![ResourceState::Undefined; graph.resources.len()];
93 let mut run_stages = vec![ReadStages::empty(); graph.resources.len()];
96 let mut gaps = Vec::new();
97
98 for pass in &graph.passes {
99 for op in &pass.barriers_before {
100 let i = op.resource_index();
101 state[i] = op.to_state();
102 if op.to_state() == ResourceState::Read {
103 run_stages[i] = op.read_stages();
104 }
105 }
106
107 let stage = ReadStages::for_pass_kind(pass.kind);
108 for w in &pass.writes {
111 let i = w.resource_index();
112 if !driven(i) {
113 continue;
114 }
115 if state[i] != ResourceState::Write {
116 gaps.push(BarrierGap {
117 pass: pass.id,
118 resource_label: graph.resources[i].label,
119 kind: GapKind::UncoveredWrite,
120 });
121 }
122 }
123 for r in &pass.reads {
124 let i = r.resource_index();
125 if !driven(i) || pass.writes.iter().any(|w| w.resource_index() == i) {
126 continue;
127 }
128 if state[i] != ResourceState::Read {
129 gaps.push(BarrierGap {
130 pass: pass.id,
131 resource_label: graph.resources[i].label,
132 kind: GapKind::UncoveredRead,
133 });
134 } else if !run_stages[i].contains(stage) {
135 gaps.push(BarrierGap {
136 pass: pass.id,
137 resource_label: graph.resources[i].label,
138 kind: GapKind::MissingReadStage,
139 });
140 }
141 }
142 }
143
144 gaps
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::render_graph::builder::GraphBuilder;
151 use crate::render_graph::frame::{FrameGraphInputs, build_frame_graph};
152 use crate::render_graph::types::{
153 PassKind, PixelFormat, TextureDesc, TextureSize, TextureUsage,
154 };
155 use alloc::string::ToString;
156
157 use super::super::frame::GATED_FLAGS as FLAGS;
161
162 fn assert_covered(combo: &[usize]) {
164 let mut inputs = FrameGraphInputs::all_off();
165 for &f in combo {
166 FLAGS[f].1(&mut inputs);
167 }
168 let names: Vec<&str> = combo.iter().map(|&f| FLAGS[f].0).collect();
169 let graph = build_frame_graph(&inputs)
170 .unwrap_or_else(|e| panic!("graph failed to compile for {names:?}: {e}"));
171 let gaps = barrier_coverage_gaps(&graph);
172 assert!(
173 gaps.is_empty(),
174 "barrier gaps for {names:?}: {}",
175 gaps.iter()
176 .map(|g| g.to_string())
177 .collect::<Vec<_>>()
178 .join(", ")
179 );
180 }
181
182 #[test]
183 fn every_single_and_paired_flag_graph_is_fully_covered() {
184 assert_covered(&[]);
190 for a in 0..FLAGS.len() {
191 assert_covered(&[a]);
192 for b in (a + 1)..FLAGS.len() {
193 assert_covered(&[a, b]);
194 }
195 }
196 }
197
198 #[test]
199 fn the_driven_subset_check_ignores_resources_outside_it() {
200 let mut g = GraphBuilder::new();
206 let a = g.create_texture("a", tex());
207 let b = g.create_texture("b", tex());
208 let (a1, b1) = {
209 let mut p = g.add_pass(PassId::Main, PassKind::Render);
210 (p.write_texture(a), p.write_texture(b))
211 };
212 g.add_pass(PassId::Composite, PassKind::Render)
213 .read_texture(a1)
214 .read_texture(b1)
215 .presents();
216 let mut g = g.compile().expect("compiles");
217 let composite = g.passes.len() - 1;
218 g.passes[composite].barriers_before.clear();
219
220 let only_a = {
221 let mut d = vec![false; g.resources.len()];
222 d[a.resource.index()] = true;
223 d
224 };
225 let gaps = barrier_coverage_gaps_for_driven(&g, &only_a);
226 assert_eq!(gaps.len(), 1, "{gaps:?}");
227 assert_eq!(gaps[0].resource_label, "a");
228
229 let none = vec![false; g.resources.len()];
230 assert_eq!(barrier_coverage_gaps_for_driven(&g, &none), vec![]);
231 }
232
233 #[test]
234 fn every_frame_graph_resource_classifies_as_its_backend_expects() {
235 use crate::render_graph::types::GraphResourceClass as C;
240
241 let mut inputs = FrameGraphInputs::all_off();
242 for (name, set) in FLAGS {
243 if *name != "world_hidden" {
244 set(&mut inputs);
245 }
246 }
247 inputs.hdr_sample_count = 4;
251 let graph = build_frame_graph(&inputs).expect("compiles");
252
253 let expected = [
254 ("draw_args", C::IndirectBuffer),
255 ("draw_args2", C::IndirectBuffer),
256 ("cull_status", C::UnorderedBuffer),
257 ("cluster_light_list", C::StorageBuffer),
258 ("ao_output", C::ColorTarget),
259 ("shadow_map", C::DepthTarget),
260 ("spot_shadow_map", C::DepthTarget),
261 ("fog_froxel_volume", C::StorageImage),
262 ("hdr_depth", C::DepthTarget),
263 ("hdr_color", C::ColorTarget),
264 ("hiz_pyramid", C::StorageImage),
265 ("gbuffer_normal_depth", C::ColorTarget),
269 ("gbuffer_roughness", C::ColorTarget),
270 ("gbuffer_velocity", C::ColorTarget),
271 ("gbuffer_depth", C::DepthTarget),
272 ];
273 for (label, want) in expected {
274 let res = graph
275 .resources
276 .iter()
277 .find(|r| r.label == label)
278 .unwrap_or_else(|| panic!("{label} missing from the fully-loaded graph"));
279 assert_eq!(res.class(), Some(want), "{label}");
280 }
281
282 for res in &graph.resources {
285 assert!(res.class().is_some(), "{} has no class", res.label);
286 }
287 }
288
289 #[test]
290 fn the_fully_loaded_graph_is_covered() {
291 let all: Vec<usize> = (0..FLAGS.len())
294 .filter(|&f| FLAGS[f].0 != "world_hidden")
295 .collect();
296 assert_covered(&all);
297 }
298
299 fn tex() -> TextureDesc {
300 TextureDesc::texture_2d(
301 TextureSize::Drawable,
302 TextureSize::Drawable,
303 PixelFormat::Rgba16Float,
304 TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET,
305 )
306 }
307
308 #[test]
309 fn a_well_formed_graph_has_no_gaps() {
310 let mut g = GraphBuilder::new();
311 let t = g.create_texture("t", tex());
312 let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
313 g.add_pass(PassId::Composite, PassKind::Render)
314 .read_texture(t1)
315 .presents();
316 let g = g.compile().expect("compiles");
317 assert_eq!(barrier_coverage_gaps(&g), vec![]);
318 }
319
320 #[test]
321 fn a_mixed_stage_read_run_is_covered_for_both_consumers() {
322 let mut g = GraphBuilder::new();
326 let t = g.create_texture("t", tex());
327 let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
328 g.add_pass(PassId::AutoExposure, PassKind::Compute)
329 .read_texture(t1);
330 g.add_pass(PassId::Composite, PassKind::Render)
331 .read_texture(t1)
332 .presents();
333 let g = g.compile().expect("compiles");
334 assert_eq!(barrier_coverage_gaps(&g), vec![]);
335 }
336
337 #[test]
338 fn a_dropped_barrier_is_reported() {
339 let mut g = GraphBuilder::new();
343 let t = g.create_texture("t", tex());
344 let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
345 g.add_pass(PassId::Composite, PassKind::Render)
346 .read_texture(t1)
347 .presents();
348 let mut g = g.compile().expect("compiles");
349 let composite = g.passes.len() - 1;
350 g.passes[composite].barriers_before.clear();
351
352 let gaps = barrier_coverage_gaps(&g);
353 assert_eq!(gaps.len(), 1, "{gaps:?}");
354 assert_eq!(gaps[0].kind, GapKind::UncoveredRead);
355 assert_eq!(gaps[0].pass, PassId::Composite);
356 assert_eq!(gaps[0].resource_label, "t");
357 }
358
359 #[test]
360 fn a_read_run_missing_a_consumer_stage_is_reported() {
361 let mut g = GraphBuilder::new();
365 let t = g.create_texture("t", tex());
366 let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
367 g.add_pass(PassId::AutoExposure, PassKind::Compute)
368 .read_texture(t1);
369 g.add_pass(PassId::Composite, PassKind::Render)
370 .read_texture(t1)
371 .presents();
372 let mut g = g.compile().expect("compiles");
373 for pass in &mut g.passes {
374 for op in &mut pass.barriers_before {
375 if op.to_state() == ResourceState::Read {
376 op.read_stages = ReadStages::FRAGMENT;
377 }
378 }
379 }
380
381 let gaps = barrier_coverage_gaps(&g);
382 assert_eq!(gaps.len(), 1, "{gaps:?}");
383 assert_eq!(gaps[0].kind, GapKind::MissingReadStage);
384 assert_eq!(gaps[0].pass, PassId::AutoExposure);
385 }
386}