use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use re_renderer::{FileResolver, FileSystem, SearchPath};
use wgpu::naga;
struct DiskFileSystem;
impl FileSystem for DiskFileSystem {
fn read_to_string(&self, path: impl AsRef<Path>) -> anyhow::Result<Cow<'static, str>> {
let path = path.as_ref();
std::fs::read_to_string(path)
.map(Into::into)
.map_err(|err| anyhow::anyhow!("failed to read {path:?}: {err}"))
}
fn canonicalize(&self, path: impl AsRef<Path>) -> anyhow::Result<PathBuf> {
let path = path.as_ref();
std::fs::canonicalize(path)
.map_err(|err| anyhow::anyhow!("failed to canonicalize {path:?}: {err}"))
}
fn exists(&self, path: impl AsRef<Path>) -> bool {
path.as_ref().exists()
}
}
fn collect_wgsl_files(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_wgsl_files(&path, out);
} else if path.extension().is_some_and(|ext| ext == "wgsl") {
out.push(path);
}
}
}
#[test]
fn all_wgsl_shaders_are_valid() {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let shader_dir = crate_dir.join("shader");
let workspace_root = crate_dir
.ancestors()
.nth(3)
.expect("re_renderer should live at crates/viewer/re_renderer");
let examples_dir = workspace_root.join("examples");
let mut search_path = SearchPath::default();
search_path.push(&shader_dir);
let resolver = FileResolver::with_search_path(DiskFileSystem, search_path);
let mut shaders = Vec::new();
collect_wgsl_files(&shader_dir, &mut shaders);
collect_wgsl_files(&examples_dir, &mut shaders);
shaders.sort();
assert!(
!shaders.is_empty(),
"found no .wgsl files to validate under {shader_dir:?} or {examples_dir:?}"
);
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
let mut failures = Vec::new();
let mut checked = 0;
let mut skipped = 0;
for shader in &shaders {
let rel = shader.strip_prefix(workspace_root).unwrap_or(shader);
let interpolated = match resolver.populate(shader) {
Ok(interpolated) => interpolated,
Err(err) => {
failures.push(format!(
"{}\n failed to resolve imports: {err}",
rel.display()
));
continue;
}
};
let has_entry_point = ["@vertex", "@fragment", "@compute"]
.iter()
.any(|kw| interpolated.contents.contains(kw));
if !has_entry_point {
skipped += 1;
continue;
}
let source = &interpolated.contents;
let module = match naga::front::wgsl::parse_str(source) {
Ok(module) => module,
Err(err) => {
failures.push(format!(
"{}\n{}",
rel.display(),
indent(&err.emit_to_string(source))
));
continue;
}
};
if let Err(err) = validator.validate(&module) {
failures.push(format!(
"{}\n{}",
rel.display(),
indent(&err.emit_to_string(source))
));
continue;
}
checked += 1;
}
eprintln!(
"Validated {checked} entry-point shader(s), skipped {skipped} include-only fragment(s)."
);
assert!(
failures.is_empty(),
"{} shader(s) failed validation:\n\n{}",
failures.len(),
failures.join("\n\n")
);
}
fn indent(s: &str) -> String {
s.lines()
.map(|line| format!(" {line}"))
.collect::<Vec<_>>()
.join("\n")
}