use super::alias::plan_aliasing_for;
use super::compile::CompiledGraph;
use super::frame::FrameGraphInputs;
use super::types::{ClearValue, PixelFormat, TextureUsage};
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Clone, Debug, PartialEq)]
pub struct TransientTexture {
pub label: &'static str,
pub width: u32,
pub height: u32,
pub depth: u32,
pub format: PixelFormat,
pub sample_count: u32,
pub array_layers: u32,
pub mip_levels: u32,
pub usage: TextureUsage,
pub clear: ClearValue,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TransientSlot {
pub members: Vec<TransientTexture>,
}
impl TransientSlot {
pub fn labels(&self) -> Vec<&'static str> {
self.members.iter().map(|m| m.label).collect()
}
}
pub(crate) fn planning_inputs(build: &FrameGraphInputs) -> FrameGraphInputs {
FrameGraphInputs {
world_hidden: false,
composite_reads_ao: true,
shadow_enabled: true,
bindless_cull_enabled: true,
auto_exposure_enabled: true,
velocity_enabled: true,
taa_enabled: true,
ssr_enabled: true,
particles_enabled: true,
fog_enabled: true,
decals_enabled: true,
ssr_prepass_enabled: true,
transparent_enabled: true,
lines_enabled: true,
raymarch_enabled: true,
two_pass_occlusion_enabled: true,
ssgi_enabled: true,
clustered_lighting_enabled: true,
hiz_build_enabled: true,
..*build
}
}
pub(crate) fn plan_transient_slots(
build: &FrameGraphInputs,
poolable: &dyn Fn(&str) -> bool,
drawable_w: u32,
drawable_h: u32,
) -> Option<Vec<TransientSlot>> {
let graph = super::frame::build_frame_graph(&planning_inputs(build)).ok()?;
let plan = plan_aliasing_for(&graph, drawable_w, drawable_h, poolable);
Some(
plan.slots
.iter()
.map(|slot| TransientSlot {
members: slot
.members
.iter()
.map(|&idx| resolve(&graph, idx, drawable_w, drawable_h))
.collect(),
})
.collect(),
)
}
pub fn pooled(label: &str) -> bool {
matches!(
label,
"ao_output"
| "bloom_top"
| "gbuffer_normal_depth"
| "gbuffer_roughness"
| "gbuffer_velocity"
)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PoolGates {
pub ssao: bool,
pub bloom: bool,
pub gbuffer: bool,
}
pub fn plan_pool_slots(
gates: PoolGates,
render_extent: (u32, u32),
output_extent: (u32, u32),
) -> Result<Vec<TransientSlot>, String> {
let mut build = FrameGraphInputs::all_off();
build.hdr_width = render_extent.0;
build.hdr_height = render_extent.1;
build.ssao_enabled = gates.ssao;
build.bloom_enabled = gates.bloom;
build.unified_gbuffer_prepass = gates.gbuffer;
build.velocity_enabled = gates.gbuffer;
plan_transient_slots(&build, &pooled, output_extent.0, output_extent.1)
.ok_or_else(|| "transient pool: the planning frame graph failed to compile".to_string())
}
fn resolve(
graph: &CompiledGraph,
idx: usize,
drawable_w: u32,
drawable_h: u32,
) -> TransientTexture {
let res = &graph.resources[idx];
let desc = res
.tex_desc
.expect("the planner only places resources carrying a texture desc");
let (width, height, depth) = desc.extent(drawable_w, drawable_h);
TransientTexture {
label: res.label,
width,
height,
depth,
format: desc.format,
sample_count: desc.sample_count.max(1),
array_layers: desc.array_layers.max(1),
mip_levels: desc.mip_levels.max(1),
usage: desc.usage,
clear: desc.clear,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) struct SlotConflict {
pub slot: usize,
pub a: &'static str,
pub b: &'static str,
}
impl core::fmt::Display for SlotConflict {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"slot {}: {} and {} are both live",
self.slot, self.a, self.b
)
}
}
pub(crate) fn slot_conflicts(
graph: &CompiledGraph,
slots: &[Vec<&'static str>],
) -> Vec<SlotConflict> {
let lifetime = |label: &str| {
graph
.resources
.iter()
.find(|r| r.label == label)
.map(|r| (r.lifetime.first, r.lifetime.last))
};
let mut conflicts = Vec::new();
for (slot, members) in slots.iter().enumerate() {
for (i, &a) in members.iter().enumerate() {
let Some((a_first, a_last)) = lifetime(a) else {
continue;
};
for &b in &members[i + 1..] {
let Some((b_first, b_last)) = lifetime(b) else {
continue;
};
if a_first <= b_last && b_first <= a_last {
conflicts.push(SlotConflict { slot, a, b });
}
}
}
}
conflicts
}
pub fn assert_slot_aliasing_sound(
graph: &CompiledGraph,
slot_labels: &[Vec<&'static str>],
backend: &str,
) {
let conflicts = slot_conflicts(graph, slot_labels);
assert!(
conflicts.is_empty(),
"transient pool ({backend}): alias slot members are simultaneously live: {}",
conflicts
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_graph::frame::build_frame_graph;
use concinnity_core::gfx::view_modes::{ShowFlags, ViewMode};
fn build_inputs(ssao: bool, bloom: bool) -> FrameGraphInputs {
build_inputs_with(ssao, bloom, true)
}
fn build_inputs_with(ssao: bool, bloom: bool, gbuffer: bool) -> FrameGraphInputs {
let mut i = FrameGraphInputs::all_off();
i.ssao_enabled = ssao;
i.bloom_enabled = bloom;
i.unified_gbuffer_prepass = gbuffer;
i.velocity_enabled = gbuffer;
i.hdr_width = 1920;
i.hdr_height = 1080;
i
}
#[test]
fn slots_carry_the_graphs_own_shape() {
let slots = plan_transient_slots(&build_inputs(true, true), &pooled, 1920, 1080)
.expect("planning graph compiles");
let member = |label: &str| {
slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == label)
.unwrap_or_else(|| panic!("{label} pooled"))
.clone()
};
let ao = member("ao_output");
assert_eq!((ao.width, ao.height, ao.depth), (1920, 1080, 1));
assert_eq!(ao.format, PixelFormat::R8Unorm);
assert_eq!((ao.sample_count, ao.array_layers, ao.mip_levels), (1, 1, 1));
assert!(ao.usage.contains(TextureUsage::RENDER_TARGET));
assert!(ao.usage.contains(TextureUsage::SHADER_READ));
let bloom = member("bloom_top");
assert_eq!((bloom.width, bloom.height), (960, 540));
assert_eq!(bloom.format, PixelFormat::Rgba16Float);
}
#[test]
fn bloom_top_follows_the_drawable_not_the_render_resolution() {
let mut build = build_inputs(true, true);
build.hdr_width = 1280;
build.hdr_height = 720;
let slots =
plan_transient_slots(&build, &pooled, 2560, 1440).expect("planning graph compiles");
let member = |label: &str| {
slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == label)
.unwrap_or_else(|| panic!("{label} pooled"))
.clone()
};
assert_eq!(
(member("ao_output").width, member("ao_output").height),
(1280, 720)
);
assert_eq!(
(member("bloom_top").width, member("bloom_top").height),
(1280, 720),
"half of 2560x1440, which happens to equal the render resolution here"
);
}
#[test]
fn an_unpooled_label_gets_no_slot() {
let slots = plan_transient_slots(&build_inputs(true, true), &|_| false, 1920, 1080)
.expect("planning graph compiles");
assert!(slots.is_empty());
let only_ao =
plan_transient_slots(&build_inputs(true, true), &|l| l == "ao_output", 1920, 1080)
.expect("planning graph compiles");
assert_eq!(only_ao.len(), 1);
assert_eq!(only_ao[0].labels(), vec!["ao_output"]);
}
#[test]
fn ssao_off_leaves_only_the_bloom_target() {
let slots =
plan_transient_slots(&build_inputs_with(false, true, false), &pooled, 1920, 1080)
.expect("planning graph compiles");
let labels: Vec<&str> = slots.iter().flat_map(|s| s.labels()).collect();
assert_eq!(labels, vec!["bloom_top"]);
}
#[test]
fn nothing_pooled_when_neither_feature_is_built() {
let slots =
plan_transient_slots(&build_inputs_with(false, false, false), &pooled, 1920, 1080)
.expect("planning graph compiles");
assert!(slots.is_empty());
}
#[test]
fn the_gbuffer_gate_places_its_colour_targets() {
let off = plan_transient_slots(&build_inputs_with(true, true, false), &pooled, 1920, 1080)
.expect("planning graph compiles");
let off_labels: Vec<&str> = off.iter().flat_map(|s| s.labels()).collect();
assert!(
!off_labels.contains(&"gbuffer_normal_depth"),
"{off_labels:?}"
);
let on = plan_transient_slots(&build_inputs(true, true), &pooled, 1920, 1080)
.expect("planning graph compiles");
let on_labels: Vec<&str> = on.iter().flat_map(|s| s.labels()).collect();
for want in [
"gbuffer_normal_depth",
"gbuffer_roughness",
"gbuffer_velocity",
] {
assert!(on_labels.contains(&want), "{want}: {on_labels:?}");
}
}
#[test]
fn slot_conflicts_reports_an_overlapping_pair() {
let mut i = FrameGraphInputs::all_off();
i.ssao_enabled = true;
i.bloom_enabled = true;
i.composite_reads_ao = true;
let graph = build_frame_graph(&i).expect("compiles");
let grouped = vec![vec!["ao_output", "bloom_top"]];
let conflicts = slot_conflicts(&graph, &grouped);
assert_eq!(conflicts.len(), 1, "{conflicts:?}");
assert_eq!(conflicts[0].slot, 0);
let split = vec![vec!["ao_output"], vec!["bloom_top"]];
assert_eq!(slot_conflicts(&graph, &split), vec![]);
}
#[test]
fn a_label_absent_from_the_graph_is_not_a_conflict() {
let graph = build_frame_graph(&FrameGraphInputs::all_off()).expect("compiles");
let grouped = vec![vec!["ao_output", "bloom_top"]];
assert_eq!(slot_conflicts(&graph, &grouped), vec![]);
}
#[test]
fn planning_inputs_forces_the_gated_passes_on() {
let planned = planning_inputs(&build_inputs(false, false));
assert!(planned.ssgi_enabled);
assert!(planned.transparent_enabled);
assert!(planned.raymarch_enabled);
assert!(!planned.world_hidden, "masking off passes is not the risk");
assert!(planned.composite_reads_ao);
assert!(!planned.ssao_enabled);
assert!(!planned.bloom_enabled);
assert!(planning_inputs(&build_inputs(true, true)).ssao_enabled);
}
#[test]
fn the_pool_actually_aliases_something() {
let slots = plan_transient_slots(&build_inputs(true, true), &pooled, 1920, 1080)
.expect("planning graph compiles");
let shared: Vec<Vec<&'static str>> = slots
.iter()
.map(|s| s.labels())
.filter(|l| l.len() > 1)
.collect();
assert!(
!shared.is_empty(),
"no slot aliases anything, so the sweeps check nothing: {:?}",
labels_of(&slots)
);
assert!(
shared.iter().any(|l| l.contains(&"bloom_top")),
"bloom_top is the late resource that makes an alias possible: {:?}",
labels_of(&slots)
);
}
fn labels_of(slots: &[TransientSlot]) -> Vec<Vec<&'static str>> {
slots.iter().map(|s| s.labels()).collect()
}
fn slot_bytes(slots: &[TransientSlot]) -> (u64, u64) {
let member_bytes = |m: &TransientTexture| -> u64 {
let texels = (m.width as u64) * (m.height as u64) * (m.depth.max(1) as u64);
texels
* m.format.bytes_per_texel() as u64
* m.sample_count.max(1) as u64
* m.array_layers.max(1) as u64
};
let mut aliased = 0;
let mut unaliased = 0;
for slot in slots {
let mut largest = 0;
for m in &slot.members {
let b = member_bytes(m);
unaliased += b;
largest = largest.max(b);
}
aliased += largest;
}
(aliased, unaliased)
}
#[test]
fn the_pooled_set_reclaims_what_the_plan_says() {
let slots = plan_transient_slots(&build_inputs(true, true), &pooled, 1920, 1080)
.expect("planning graph compiles");
let (aliased, unaliased) = slot_bytes(&slots);
let saved = unaliased - aliased;
assert!(
saved >= 3 * 1024 * 1024,
"aliasing reclaims {} MiB (aliased {} MiB of {} MiB) from {:?}",
saved / (1024 * 1024),
aliased / (1024 * 1024),
unaliased / (1024 * 1024),
labels_of(&slots)
);
}
#[test]
fn the_prepass_depth_is_short_lived_only_in_the_planning_graph() {
let build = build_inputs(true, true);
let planned = build_frame_graph(&planning_inputs(&build)).expect("compiles");
let life = |g: &CompiledGraph, label: &str| {
let r = g
.resources
.iter()
.find(|r| r.label == label)
.unwrap_or_else(|| panic!("{label} declared"));
(r.lifetime.first, r.lifetime.last)
};
let (first, last) = life(&planned, "gbuffer_depth");
assert_eq!(
first, last,
"the planning graph gives the pre-pass depth a one-pass lifetime"
);
let mut upscaling = build;
upscaling.upscale_enabled = true;
let real = build_frame_graph(&planning_inputs(&upscaling)).expect("compiles");
let (up_first, up_last) = life(&real, "gbuffer_depth");
assert!(
up_last > up_first,
"the upscaler reads the pre-pass depth, so its real lifetime spans passes"
);
let (hdr_first, hdr_last) = life(&real, "hdr_depth");
assert!(
up_first <= hdr_last && hdr_first <= up_last,
"the two depth targets overlap once the upscale branch is modelled: \
gbuffer_depth [{up_first},{up_last}] vs hdr_depth [{hdr_first},{hdr_last}]"
);
}
type FlagSetter = fn(&mut FrameGraphInputs);
const FLAGS: &[(&str, FlagSetter)] = &[
("shadow", |i| i.shadow_enabled = true),
("bindless_cull", |i| i.bindless_cull_enabled = true),
("auto_exposure", |i| i.auto_exposure_enabled = true),
("bloom", |i| i.bloom_enabled = true),
("velocity", |i| i.velocity_enabled = true),
("taa", |i| i.taa_enabled = true),
("ssr", |i| i.ssr_enabled = true),
("particles", |i| i.particles_enabled = true),
("fog", |i| i.fog_enabled = true),
("decals", |i| i.decals_enabled = true),
("ssr_prepass", |i| i.ssr_prepass_enabled = true),
("ssao", |i| i.ssao_enabled = true),
("upscale", |i| i.upscale_enabled = true),
("transparent", |i| i.transparent_enabled = true),
("lines", |i| i.lines_enabled = true),
("raymarch", |i| i.raymarch_enabled = true),
("two_pass_occlusion", |i| {
i.two_pass_occlusion_enabled = true
}),
("ssgi", |i| i.ssgi_enabled = true),
("rt_reflections", |i| i.rt_reflections_enabled = true),
("unified_gbuffer", |i| i.unified_gbuffer_prepass = true),
("world_hidden", |i| i.world_hidden = true),
("clustered_lighting", |i| {
i.clustered_lighting_enabled = true
}),
("composite_reads_ao", |i| i.composite_reads_ao = true),
("shadowed_spots", |i| i.shadowed_spot_count = 2),
("hiz_build", |i| i.hiz_build_enabled = true),
];
fn assert_sound(build: &FrameGraphInputs, inputs: &FrameGraphInputs, what: &str) {
let slots =
plan_transient_slots(build, &pooled, 1920, 1080).expect("the planning graph compiles");
let grouped: Vec<Vec<&'static str>> = slots.iter().map(|s| s.labels()).collect();
let graph = build_frame_graph(inputs)
.unwrap_or_else(|e| panic!("graph failed to compile for {what}: {e}"));
let conflicts = slot_conflicts(&graph, &grouped);
assert!(
conflicts.is_empty(),
"aliasing conflict for {what}: {}",
conflicts
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
#[test]
fn slot_conflicts_over_reachable_graphs() {
let builds = [
build_inputs(false, false),
build_inputs(true, false),
build_inputs(false, true),
build_inputs(true, true),
];
for build in &builds {
for (i, (a_name, set_a)) in FLAGS.iter().enumerate() {
for (b_name, set_b) in FLAGS.iter().skip(i) {
let mut inputs = FrameGraphInputs::all_off();
set_a(&mut inputs);
set_b(&mut inputs);
let what = format!("{a_name} + {b_name}");
assert_sound(build, &inputs, &what);
for mode in ViewMode::ALL {
for show in [ShowFlags::all(), ShowFlags(0)] {
let masked = crate::render_graph::apply_view(&inputs, mode, show);
assert_sound(
build,
&masked,
&format!("{what} under {mode:?} / {show:?}"),
);
}
}
}
}
}
}
#[test]
fn slot_conflicts_over_the_fully_loaded_graph_in_every_view() {
let mut loaded = FrameGraphInputs::all_off();
for (name, set) in FLAGS {
if *name != "world_hidden" {
set(&mut loaded);
}
}
let build = build_inputs(true, true);
for mode in ViewMode::ALL {
for bits in 0..(1u32 << ShowFlags::LABELED.len()) {
let show = ShowFlags(bits);
let masked = crate::render_graph::apply_view(&loaded, mode, show);
assert_sound(
&build,
&masked,
&format!("loaded under {mode:?} / {bits:b}"),
);
}
}
}
}