use super::compile::CompiledGraph;
use super::passes::PassId;
use super::types::{ReadStages, ResourceState};
use alloc::vec;
use alloc::vec::Vec;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GapKind {
UncoveredRead,
UncoveredWrite,
MissingReadStage,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BarrierGap {
pub pass: PassId,
pub resource_label: &'static str,
pub kind: GapKind,
}
impl core::fmt::Display for BarrierGap {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let what = match self.kind {
GapKind::UncoveredRead => "reads",
GapKind::UncoveredWrite => "writes",
GapKind::MissingReadStage => "reads (stage not in the run union)",
};
write!(f, "pass {:?} {} {}", self.pass, what, self.resource_label)
}
}
#[cfg(test)]
pub(crate) fn barrier_coverage_gaps(graph: &CompiledGraph) -> Vec<BarrierGap> {
gaps_over(graph, &|_| true)
}
pub fn barrier_coverage_gaps_for_driven(graph: &CompiledGraph, driven: &[bool]) -> Vec<BarrierGap> {
gaps_over(graph, &|i| driven.get(i).copied().unwrap_or(false))
}
pub fn final_states(graph: &CompiledGraph) -> Vec<(ResourceState, ReadStages)> {
let mut state = vec![(ResourceState::Undefined, ReadStages::empty()); graph.resources.len()];
for pass in &graph.passes {
for op in &pass.barriers_before {
state[op.resource_index()] = (op.to_state(), op.read_stages());
}
}
state
}
fn gaps_over(graph: &CompiledGraph, driven: &dyn Fn(usize) -> bool) -> Vec<BarrierGap> {
let mut state = vec![ResourceState::Undefined; graph.resources.len()];
let mut run_stages = vec![ReadStages::empty(); graph.resources.len()];
let mut gaps = Vec::new();
for pass in &graph.passes {
for op in &pass.barriers_before {
let i = op.resource_index();
state[i] = op.to_state();
if op.to_state() == ResourceState::Read {
run_stages[i] = op.read_stages();
}
}
let stage = ReadStages::for_pass_kind(pass.kind);
for w in &pass.writes {
let i = w.resource_index();
if !driven(i) {
continue;
}
if state[i] != ResourceState::Write {
gaps.push(BarrierGap {
pass: pass.id,
resource_label: graph.resources[i].label,
kind: GapKind::UncoveredWrite,
});
}
}
for r in &pass.reads {
let i = r.resource_index();
if !driven(i) || pass.writes.iter().any(|w| w.resource_index() == i) {
continue;
}
if state[i] != ResourceState::Read {
gaps.push(BarrierGap {
pass: pass.id,
resource_label: graph.resources[i].label,
kind: GapKind::UncoveredRead,
});
} else if !run_stages[i].contains(stage) {
gaps.push(BarrierGap {
pass: pass.id,
resource_label: graph.resources[i].label,
kind: GapKind::MissingReadStage,
});
}
}
}
gaps
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::render_graph::builder::GraphBuilder;
use crate::render::render_graph::frame::{FrameGraphInputs, build_frame_graph};
use crate::render::render_graph::types::{
PassKind, PixelFormat, TextureDesc, TextureSize, TextureUsage,
};
use alloc::string::ToString;
use super::super::frame::GATED_FLAGS as FLAGS;
fn assert_covered(combo: &[usize]) {
let mut inputs = FrameGraphInputs::all_off();
for &f in combo {
FLAGS[f].1(&mut inputs);
}
let names: Vec<&str> = combo.iter().map(|&f| FLAGS[f].0).collect();
let graph = build_frame_graph(&inputs)
.unwrap_or_else(|e| panic!("graph failed to compile for {names:?}: {e}"));
let gaps = barrier_coverage_gaps(&graph);
assert!(
gaps.is_empty(),
"barrier gaps for {names:?}: {}",
gaps.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
#[test]
fn every_single_and_paired_flag_graph_is_fully_covered() {
assert_covered(&[]);
for a in 0..FLAGS.len() {
assert_covered(&[a]);
for b in (a + 1)..FLAGS.len() {
assert_covered(&[a, b]);
}
}
}
#[test]
fn the_driven_subset_check_ignores_resources_outside_it() {
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex());
let b = g.create_texture("b", tex());
let (a1, b1) = {
let mut p = g.add_pass(PassId::Main, PassKind::Render);
(p.write_texture(a), p.write_texture(b))
};
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(a1)
.read_texture(b1)
.presents();
let mut g = g.compile().expect("compiles");
let composite = g.passes.len() - 1;
g.passes[composite].barriers_before.clear();
let only_a = {
let mut d = vec![false; g.resources.len()];
d[a.resource.index()] = true;
d
};
let gaps = barrier_coverage_gaps_for_driven(&g, &only_a);
assert_eq!(gaps.len(), 1, "{gaps:?}");
assert_eq!(gaps[0].resource_label, "a");
let none = vec![false; g.resources.len()];
assert_eq!(barrier_coverage_gaps_for_driven(&g, &none), vec![]);
}
#[test]
fn every_frame_graph_resource_classifies_as_its_backend_expects() {
use crate::render::render_graph::types::GraphResourceClass as C;
let mut inputs = FrameGraphInputs::all_off();
for (name, set) in FLAGS {
if *name != "world_hidden" {
set(&mut inputs);
}
}
inputs.hdr_sample_count = 4;
let graph = build_frame_graph(&inputs).expect("compiles");
let expected = [
("draw_args", C::IndirectBuffer),
("draw_args2", C::IndirectBuffer),
("cull_status", C::UnorderedBuffer),
("cluster_light_list", C::StorageBuffer),
("ao_output", C::ColorTarget),
("shadow_map", C::DepthTarget),
("spot_shadow_map", C::DepthTarget),
("fog_froxel_volume", C::StorageImage),
("hdr_depth", C::DepthTarget),
("hdr_color", C::ColorTarget),
("hiz_pyramid", C::StorageImage),
("gbuffer_normal_depth", C::ColorTarget),
("gbuffer_roughness", C::ColorTarget),
("gbuffer_velocity", C::ColorTarget),
("gbuffer_depth", C::DepthTarget),
];
for (label, want) in expected {
let res = graph
.resources
.iter()
.find(|r| r.label == label)
.unwrap_or_else(|| panic!("{label} missing from the fully-loaded graph"));
assert_eq!(res.class(), Some(want), "{label}");
}
for res in &graph.resources {
assert!(res.class().is_some(), "{} has no class", res.label);
}
}
#[test]
fn the_fully_loaded_graph_is_covered() {
let all: Vec<usize> = (0..FLAGS.len())
.filter(|&f| FLAGS[f].0 != "world_hidden")
.collect();
assert_covered(&all);
}
fn tex() -> TextureDesc {
TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Drawable,
PixelFormat::Rgba16Float,
TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET,
)
}
#[test]
fn a_well_formed_graph_has_no_gaps() {
let mut g = GraphBuilder::new();
let t = g.create_texture("t", tex());
let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(t1)
.presents();
let g = g.compile().expect("compiles");
assert_eq!(barrier_coverage_gaps(&g), vec![]);
}
#[test]
fn a_mixed_stage_read_run_is_covered_for_both_consumers() {
let mut g = GraphBuilder::new();
let t = g.create_texture("t", tex());
let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
g.add_pass(PassId::AutoExposure, PassKind::Compute)
.read_texture(t1);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(t1)
.presents();
let g = g.compile().expect("compiles");
assert_eq!(barrier_coverage_gaps(&g), vec![]);
}
#[test]
fn a_dropped_barrier_is_reported() {
let mut g = GraphBuilder::new();
let t = g.create_texture("t", tex());
let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(t1)
.presents();
let mut g = g.compile().expect("compiles");
let composite = g.passes.len() - 1;
g.passes[composite].barriers_before.clear();
let gaps = barrier_coverage_gaps(&g);
assert_eq!(gaps.len(), 1, "{gaps:?}");
assert_eq!(gaps[0].kind, GapKind::UncoveredRead);
assert_eq!(gaps[0].pass, PassId::Composite);
assert_eq!(gaps[0].resource_label, "t");
}
#[test]
fn a_read_run_missing_a_consumer_stage_is_reported() {
let mut g = GraphBuilder::new();
let t = g.create_texture("t", tex());
let t1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(t);
g.add_pass(PassId::AutoExposure, PassKind::Compute)
.read_texture(t1);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(t1)
.presents();
let mut g = g.compile().expect("compiles");
for pass in &mut g.passes {
for op in &mut pass.barriers_before {
if op.to_state() == ResourceState::Read {
op.read_stages = ReadStages::FRAGMENT;
}
}
}
let gaps = barrier_coverage_gaps(&g);
assert_eq!(gaps.len(), 1, "{gaps:?}");
assert_eq!(gaps[0].kind, GapKind::MissingReadStage);
assert_eq!(gaps[0].pass, PassId::AutoExposure);
}
}