use super::compile::CompiledGraph;
use super::types::{ResourceOrigin, TextureDesc};
use alloc::vec;
use alloc::vec::Vec;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct SlotClass {
depth: bool,
sample_count: u32,
}
impl SlotClass {
fn of(desc: &TextureDesc) -> Self {
Self {
depth: desc.format.is_depth(),
sample_count: desc.sample_count.max(1),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AliasSlot {
pub byte_size: u64,
pub members: Vec<usize>,
}
#[derive(Debug, Clone)]
pub(crate) struct AliasPlan {
pub slots: Vec<AliasSlot>,
#[cfg_attr(
not(test),
expect(dead_code, reason = "measured output asserted by this module's tests")
)]
pub assignment: Vec<Option<usize>>,
#[cfg_attr(
not(test),
expect(dead_code, reason = "measured output asserted by this module's tests")
)]
pub aliased_bytes: u64,
#[cfg_attr(
not(test),
expect(dead_code, reason = "measured output asserted by this module's tests")
)]
pub unaliased_bytes: u64,
}
impl AliasPlan {
#[cfg(test)]
pub(crate) fn saved_bytes(&self) -> u64 {
self.unaliased_bytes.saturating_sub(self.aliased_bytes)
}
}
pub(crate) fn plan_aliasing_for(
graph: &CompiledGraph,
drawable_w: u32,
drawable_h: u32,
poolable: &dyn Fn(&str) -> bool,
) -> AliasPlan {
struct Cand {
idx: usize,
first: usize,
last: usize,
size: u64,
class: SlotClass,
}
let mut cands: Vec<Cand> = Vec::new();
for (idx, res) in graph.resources.iter().enumerate() {
if res.origin != ResourceOrigin::Transient || !poolable(res.label) {
continue;
}
let Some(desc) = res.tex_desc else {
continue;
};
cands.push(Cand {
idx,
first: res.lifetime.first,
last: res.lifetime.last,
size: desc.byte_size(drawable_w, drawable_h),
class: SlotClass::of(&desc),
});
}
let unaliased_bytes: u64 = cands.iter().map(|c| c.size).sum();
cands.sort_by(|a, b| a.first.cmp(&b.first).then(a.idx.cmp(&b.idx)));
struct SlotMeta {
class: SlotClass,
free_at: usize,
byte_size: u64,
members: Vec<usize>,
}
let mut slots: Vec<SlotMeta> = Vec::new();
let mut assignment: Vec<Option<usize>> = vec![None; graph.resources.len()];
for c in &cands {
let chosen = slots
.iter()
.position(|s| s.class == c.class && s.free_at < c.first);
let si = match chosen {
Some(si) => {
let s = &mut slots[si];
s.free_at = c.last;
s.byte_size = s.byte_size.max(c.size);
s.members.push(c.idx);
si
}
None => {
slots.push(SlotMeta {
class: c.class,
free_at: c.last,
byte_size: c.size,
members: vec![c.idx],
});
slots.len() - 1
}
};
assignment[c.idx] = Some(si);
}
let aliased_bytes: u64 = slots.iter().map(|s| s.byte_size).sum();
let slots: Vec<AliasSlot> = slots
.into_iter()
.map(|s| AliasSlot {
byte_size: s.byte_size,
members: s.members,
})
.collect();
AliasPlan {
slots,
assignment,
aliased_bytes,
unaliased_bytes,
}
}
#[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::passes::PassId;
use crate::render::render_graph::types::{
PassKind, PixelFormat, TextureDesc, TextureSize, TextureUsage,
};
fn plan_aliasing(graph: &CompiledGraph, drawable_w: u32, drawable_h: u32) -> AliasPlan {
plan_aliasing_for(graph, drawable_w, drawable_h, &|_| true)
}
fn all_off() -> FrameGraphInputs {
FrameGraphInputs {
hdr_width: 1920,
hdr_height: 1080,
..FrameGraphInputs::all_off()
}
}
fn tex(format: PixelFormat) -> TextureDesc {
TextureDesc::texture_2d(
TextureSize::Drawable,
TextureSize::Drawable,
format,
TextureUsage::SHADER_READ | TextureUsage::RENDER_TARGET,
)
}
fn size_at_100(format: PixelFormat) -> u64 {
100 * 100 * format.bytes_per_texel() as u64
}
#[test]
fn byte_size_resolves_drawable_and_format() {
let d = tex(PixelFormat::Rgba16Float);
assert_eq!(d.byte_size(64, 32), 64 * 32 * 8);
let half_r8 = TextureDesc::texture_2d(
TextureSize::DrawableScaled(0.5),
TextureSize::DrawableScaled(0.5),
PixelFormat::R8Unorm,
TextureUsage::SHADER_READ,
);
assert_eq!(half_r8.byte_size(64, 64), 32 * 32);
let msaa = tex(PixelFormat::Rgba8Unorm)
.with_sample_count(4)
.with_array_layers(2);
assert_eq!(msaa.byte_size(10, 10), 10 * 10 * 4 * 4 * 2);
}
#[test]
fn disjoint_transients_share_one_slot_sized_to_largest() {
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex(PixelFormat::R8Unorm));
let b = g.create_texture("b", tex(PixelFormat::Rgba16Float));
let a1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(a);
g.add_pass(PassId::SsaoBlur, PassKind::Render)
.read_texture(a1);
let b1 = g.add_pass(PassId::Fog, PassKind::Render).write_texture(b);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(b1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.slots.len(), 1, "disjoint a + b share one slot");
assert_eq!(
plan.slots[0].byte_size,
size_at_100(PixelFormat::Rgba16Float)
);
assert_eq!(plan.slots[0].members.len(), 2);
assert_eq!(plan.assignment[a.resource.index()], Some(0));
assert_eq!(plan.assignment[b.resource.index()], Some(0));
assert_eq!(plan.saved_bytes(), size_at_100(PixelFormat::R8Unorm));
assert_eq!(
plan.unaliased_bytes,
size_at_100(PixelFormat::R8Unorm) + size_at_100(PixelFormat::Rgba16Float)
);
}
#[test]
fn overlapping_transients_get_separate_slots() {
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex(PixelFormat::Rgba16Float));
let b = g.create_texture("b", tex(PixelFormat::Rgba16Float));
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 g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.slots.len(), 2, "overlapping a + b need two slots");
assert_eq!(plan.saved_bytes(), 0);
}
#[test]
fn touching_lifetimes_do_not_alias() {
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex(PixelFormat::Rgba16Float));
let b = g.create_texture("b", tex(PixelFormat::Rgba16Float));
let a1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(a);
let b1 = {
let mut p = g.add_pass(PassId::Decals, PassKind::Render);
p.read_texture(a1);
p.write_texture(b)
};
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(b1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.slots.len(), 2, "touching lifetimes overlap at pass 1");
}
#[test]
fn depth_and_colour_do_not_share() {
let mut g = GraphBuilder::new();
let depth = g.create_texture("depth", tex(PixelFormat::Depth32Float));
let colour = g.create_texture("colour", tex(PixelFormat::Rgba16Float));
let d1 = g
.add_pass(PassId::Shadow, PassKind::Render)
.write_texture(depth);
g.add_pass(PassId::SsaoBlur, PassKind::Render)
.read_texture(d1);
let c1 = g
.add_pass(PassId::Fog, PassKind::Render)
.write_texture(colour);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(c1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.slots.len(), 2, "depth + colour never share a slot");
assert_eq!(plan.saved_bytes(), 0);
}
#[test]
fn differing_sample_counts_do_not_share() {
let mut g = GraphBuilder::new();
let multi = g.create_texture("multi", tex(PixelFormat::Rgba16Float).with_sample_count(4));
let single = g.create_texture("single", tex(PixelFormat::Rgba16Float));
let m1 = g
.add_pass(PassId::Main, PassKind::Render)
.write_texture(multi);
g.add_pass(PassId::SsaoBlur, PassKind::Render)
.read_texture(m1);
let s1 = g
.add_pass(PassId::Fog, PassKind::Render)
.write_texture(single);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(s1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(
plan.slots.len(),
2,
"a multisample target never shares with a single-sample one"
);
assert_eq!(plan.saved_bytes(), 0);
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex(PixelFormat::Rgba16Float).with_sample_count(4));
let b = g.create_texture("b", tex(PixelFormat::Rgba16Float).with_sample_count(4));
let a1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(a);
g.add_pass(PassId::SsaoBlur, PassKind::Render)
.read_texture(a1);
let b1 = g.add_pass(PassId::Fog, PassKind::Render).write_texture(b);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(b1)
.presents();
let g = g.compile().expect("compiles");
assert_eq!(plan_aliasing(&g, 100, 100).slots.len(), 1);
}
#[test]
fn imported_resources_are_not_placed() {
let mut g = GraphBuilder::new();
let imported = g.import_texture("imported", tex(PixelFormat::Rgba16Float));
let transient = g.create_texture("transient", tex(PixelFormat::Rgba16Float));
let i1 = {
let mut p = g.add_pass(PassId::Main, PassKind::Render);
p.read_texture(imported);
p.write_texture(transient)
};
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(i1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.assignment[imported.resource.index()], None);
assert!(plan.assignment[transient.resource.index()].is_some());
}
#[test]
fn three_disjoint_chain_packs_into_one_slot() {
let mut g = GraphBuilder::new();
let a = g.create_texture("a", tex(PixelFormat::Rgba16Float));
let b = g.create_texture("b", tex(PixelFormat::Rgba16Float));
let c = g.create_texture("c", tex(PixelFormat::Rgba16Float));
let a1 = g.add_pass(PassId::Main, PassKind::Render).write_texture(a);
g.add_pass(PassId::Decals, PassKind::Render)
.read_texture(a1);
let b1 = g.add_pass(PassId::Fog, PassKind::Render).write_texture(b);
g.add_pass(PassId::ParticlesDraw, PassKind::Render)
.read_texture(b1);
let c1 = g
.add_pass(PassId::SsrResolve, PassKind::Render)
.write_texture(c);
g.add_pass(PassId::Composite, PassKind::Render)
.read_texture(c1)
.presents();
let g = g.compile().expect("compiles");
let plan = plan_aliasing(&g, 100, 100);
assert_eq!(plan.slots.len(), 1);
assert_eq!(plan.slots[0].members.len(), 3);
assert_eq!(
plan.saved_bytes(),
2 * size_at_100(PixelFormat::Rgba16Float)
);
}
#[test]
fn real_frame_graph_aliases_some_transients() {
let mut inputs = all_off();
inputs.ssao_enabled = true;
inputs.bloom_enabled = true;
let g = build_frame_graph(&inputs).expect("frame graph compiles");
let plan = plan_aliasing(&g, 1920, 1080);
assert!(
plan.saved_bytes() > 0,
"ao_output + bloom_top have disjoint lifetimes and should alias"
);
}
}