use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const VULKAN_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/vulkan");
const DIRECTX_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/directx");
const EXECUTOR: &str = "graph_exec.rs";
struct BackendRegistry {
backend: &'static str,
root: &'static str,
fields: &'static [(&'static str, &'static str)],
markers: &'static [&'static str],
}
const REGISTRIES: &[BackendRegistry] = &[
BackendRegistry {
backend: "vulkan",
root: VULKAN_ROOT,
fields: &[
("draw_args", "cull.indirect_buffers"),
("draw_args2", "cull.indirect_buffers2"),
("cull_status", "cull.cull_status_buffers"),
("cluster_light_list", "light_cull.cluster_buffer"),
("ao_output", "transient_pool.image_for"),
("shadow_map", "shadow.map.image"),
("spot_shadow_map", "spot_shadow.map.image"),
("fog_froxel_volume", "volume.image"),
("hdr_depth", "depth_images"),
("hiz_pyramid", ".pyramid.image()"),
],
markers: &[
"vk::ImageMemoryBarrier::default()",
"vk::BufferMemoryBarrier::default()",
"depth_barrier(",
"color_barrier(",
],
},
BackendRegistry {
backend: "directx",
root: DIRECTX_ROOT,
fields: &[
("draw_args", "cull.indirect_cmd_buffers"),
("draw_args2", "cull.indirect_cmd_buffers_2"),
("cull_status", "cull.cull_status_buffers"),
("cluster_light_list", "light_cull.cluster_buffer"),
("ao_output", "transient_pool.resource_for"),
("shadow_map", "shadow.resource"),
("spot_shadow_map", "spot_shadow.resource"),
("fog_froxel_volume", "volume_resource"),
("hdr_depth", "depth_resource"),
("hiz_pyramid", "hiz.texture"),
("hdr_color", "hdr.color"),
("hdr_resolve", "hdr_scene_target()"),
("scene_pre_taa", "rc.output"),
("scene_pre_taa", "post_scene_target()"),
("scene_color", "taa.history"),
("bloom_top", "bloom.mips"),
("gbuffer_normal_depth", "gb.normal_depth"),
("gbuffer_roughness", "gb.roughness"),
("gbuffer_velocity", "gb.velocity"),
],
markers: &["transition_barrier(", "uav_barrier(", "aliasing_barrier("],
},
];
const ALLOWED: &[(&str, &str, &str)] = &[
("directx", "hiz.rs", "hiz.texture"),
("directx", "draw/main.rs", "hdr.color"),
("directx", "draw/main.rs", "hdr_scene_target()"),
("directx", "raymarch.rs", "hdr.color"),
("directx", "raymarch.rs", "hdr_scene_target()"),
("directx", "post/ssgi.rs", "hdr_scene_target()"),
("directx", "transparent.rs", "post_scene_target()"),
("directx", "post/bloom.rs", "bloom.mips"),
];
fn rust_sources(dir: &Path, prefix: &str, out: &mut Vec<(String, PathBuf)>) {
let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()));
for entry in entries {
let path = entry.expect("dir entry").path();
let name = path
.file_name()
.and_then(|n| n.to_str())
.expect("utf-8 file name")
.to_string();
let rel = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
if path.is_dir() {
rust_sources(&path, &rel, out);
} else if name.ends_with(".rs") {
out.push((rel, path));
}
}
}
fn code_only(source: &str) -> String {
source
.lines()
.map(|l| l.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n")
}
fn squeeze(code: &str) -> String {
code.chars().filter(|c| !c.is_whitespace()).collect()
}
fn barrier_targets<'a>(code: &'a str, markers: &[&str]) -> Vec<&'a str> {
let mut out = Vec::new();
for marker in markers {
let mut from = 0;
while let Some(rel) = code[from..].find(marker) {
let start = from + rel;
let end = code[start..]
.find(';')
.map(|e| start + e)
.unwrap_or(code.len());
out.push(&code[start..end]);
from = start + marker.len();
}
}
out
}
fn double_driven(registry: &BackendRegistry) -> BTreeSet<(String, &'static str)> {
let mut files = Vec::new();
rust_sources(Path::new(registry.root), "", &mut files);
let mut found = BTreeSet::new();
for (rel, path) in files {
if rel == EXECUTOR {
continue;
}
let code = squeeze(&code_only(
&std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{rel}: {e}")),
));
for target in barrier_targets(&code, registry.markers) {
for (_, token) in registry.fields {
if target.contains(token) {
found.insert((rel.clone(), *token));
}
}
}
}
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_encoder_transitions_a_graph_driven_resource() {
let mut failures = Vec::new();
for registry in REGISTRIES {
let found = double_driven(registry);
let allowed: BTreeSet<(String, &str)> = ALLOWED
.iter()
.filter(|(b, ..)| *b == registry.backend)
.map(|(_, file, field)| (file.to_string(), *field))
.collect();
for (file, field) in &found {
if allowed.contains(&(file.clone(), *field)) {
continue;
}
let label = registry
.fields
.iter()
.find(|(_, f)| f == field)
.map(|(l, _)| *l)
.unwrap_or("?");
failures.push(format!(
"{}/{file}: emits a barrier targeting `{field}`, which the graph executor \
already drives as `{label}`. Both will run every frame and the second will \
name a state the resource has already left. Remove the inline transition, \
or add it to ALLOWED with the reason it is finer than the graph can express.",
registry.backend
));
}
for (file, field) in &allowed {
if !found.contains(&(file.clone(), *field)) {
failures.push(format!(
"{}/{file}: ALLOWED lists `{field}` but no barrier there targets it; \
drop the entry",
registry.backend
));
}
}
}
assert!(failures.is_empty(), "\n{}", failures.join("\n"));
}
#[test]
fn the_registry_field_table_matches_the_resolver() {
for registry in REGISTRIES {
let mut files = Vec::new();
rust_sources(Path::new(registry.root), "", &mut files);
let all: String = squeeze(
&files
.iter()
.map(|(rel, path)| {
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {rel}: {e}"))
})
.collect::<String>(),
);
let resolver = std::fs::read_to_string(Path::new(registry.root).join(EXECUTOR))
.unwrap_or_else(|e| panic!("read {EXECUTOR}: {e}"));
for (label, token) in registry.fields {
assert!(
resolver.contains(&format!("\"{label}\"")),
"{}: the executor no longer resolves {label}; drop it from this table \
(its encoder owns its barriers again)",
registry.backend
);
assert!(
all.contains(token),
"{}: no code names `{token}` (backing {label}); the field was renamed and \
this guard stopped checking it",
registry.backend
);
}
}
}
#[test]
fn the_scan_finds_a_planted_double_drive() {
let code = squeeze(&code_only(
"let b = vk::ImageMemoryBarrier::default()\n .image(self.depth_images[i].image);\n",
));
let targets = barrier_targets(&code, &["vk::ImageMemoryBarrier::default()"]);
assert_eq!(targets.len(), 1);
assert!(targets[0].contains("depth_images"));
let code = squeeze(&code_only(
"let b = vk::ImageMemoryBarrier::default().image(planar.target);\n",
));
let targets = barrier_targets(&code, &["vk::ImageMemoryBarrier::default()"]);
assert!(!targets[0].contains("depth_images"));
}
}