From 928ed9764880fdefeba65fd9a8b43da2a4f47f1a Mon Sep 17 00:00:00 2001 From: SupaMaggie70Incorporated Date: Fri, 15 Aug 2025 13:20:58 -0500 Subject: [PATCH 1/2] Initial changes --- .github/workflows/ci.yml | 6 +- Cargo.lock | 8 +- benches/Cargo.toml | 10 + benches/benches/wgpu-benchmark/shader.rs | 288 +++++++------ naga/Cargo.toml | 23 +- naga/tests/naga/snapshots.rs | 507 ++--------------------- tests/Cargo.toml | 18 + tests/src/lib.rs | 1 + tests/src/naga.rs | 454 ++++++++++++++++++++ 9 files changed, 709 insertions(+), 606 deletions(-) create mode 100644 tests/src/naga.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e3422e62bd..bf8f00b4cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -262,9 +262,9 @@ jobs: set -e # build for WebGPU - cargo clippy --target ${{ matrix.target }} ${{ matrix.extra-flags }} --tests --features glsl,spirv,fragile-send-sync-non-atomic-wasm - cargo clippy --target ${{ matrix.target }} ${{ matrix.extra-flags }} --tests --features glsl,spirv - cargo doc --target ${{ matrix.target }} ${{ matrix.extra-flags }} --no-deps --features glsl,spirv + cargo clippy --target ${{ matrix.target }} ${{ matrix.extra-flags }} --tests --features glsl,fragile-send-sync-non-atomic-wasm + cargo clippy --target ${{ matrix.target }} ${{ matrix.extra-flags }} --tests --features glsl + cargo doc --target ${{ matrix.target }} ${{ matrix.extra-flags }} --no-deps --features glsl # check with only the web feature cargo clippy --target ${{ matrix.target }} ${{ matrix.extra-flags }} --no-default-features --features=web diff --git a/Cargo.lock b/Cargo.lock index 1f0c5b5ab22..a23867607aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2522,9 +2522,9 @@ dependencies = [ "spirv", "strum 0.27.2", "thiserror 2.0.14", - "toml 0.9.5", "unicode-ident", "walkdir", + "wgpu-test", ] [[package]] @@ -4940,6 +4940,7 @@ dependencies = [ "rayon", "tracy-client", "wgpu", + "wgpu-test", ] [[package]] @@ -5161,15 +5162,20 @@ dependencies = [ "js-sys", "libtest-mimic", "log", + "naga", "nanorand 0.8.0", "nv-flip", "parking_lot", "png", "pollster", "profiling", + "ron", + "rspirv", "serde", "serde_json", + "spirv", "strum 0.27.2", + "toml 0.9.5", "trybuild", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 9af4cf4ae7d..53c992ce86a 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -47,3 +47,13 @@ profiling.workspace = true rayon.workspace = true tracy-client = { workspace = true, optional = true } wgpu.workspace = true +wgpu-test = { workspace = true, features = [ + "wgsl-in", + "spv-in", + "glsl-in", + "spv-out", + "msl-out", + "hlsl-out", + "glsl-out", + "wgsl-out", +] } diff --git a/benches/benches/wgpu-benchmark/shader.rs b/benches/benches/wgpu-benchmark/shader.rs index b98cef01ae5..3ccc5b01ced 100644 --- a/benches/benches/wgpu-benchmark/shader.rs +++ b/benches/benches/wgpu-benchmark/shader.rs @@ -1,57 +1,56 @@ use criterion::*; -use std::{fs, path::PathBuf, process::Command}; +use std::{fs, process::Command}; -struct Input { - filename: String, - size: u64, +const DIR_IN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../naga/tests/in"); + +use wgpu_test::naga::*; + +struct InputWithInfo { + inner: Input, data: Vec, string: Option, + options: Parameters, module: Option, module_info: Option, } +impl From for InputWithInfo { + fn from(value: Input) -> Self { + let mut options = value.read_parameters(DIR_IN); + options.targets = Some(options.targets.unwrap_or(Targets::all())); + Self { + options, + inner: value, + data: Vec::new(), + string: None, + module: None, + module_info: None, + } + } +} +impl InputWithInfo { + fn filename(&self) -> &str { + self.inner.file_name.file_name().unwrap().to_str().unwrap() + } +} struct Inputs { - inner: Vec, + inner: Vec, } impl Inputs { #[track_caller] fn from_dir(folder: &str, extension: &str) -> Self { - let mut inputs = Vec::new(); - let read_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(folder) - .read_dir() - .unwrap(); - - for file_entry in read_dir { - match file_entry { - Ok(entry) => match entry.path().extension() { - Some(ostr) if ostr == extension => { - let path = entry.path(); - - inputs.push(Input { - filename: path.to_string_lossy().into_owned(), - size: entry.metadata().unwrap().len(), - string: None, - data: vec![], - module: None, - module_info: None, - }); - } - _ => continue, - }, - Err(e) => { - eprintln!("Skipping file: {e:?}"); - continue; - } - } - } + let inputs: Vec = Input::files_in_dir(folder, &[extension], DIR_IN) + .map(|a| a.into()) + .collect(); Self { inner: inputs } } - fn bytes(&self) -> u64 { - self.inner.iter().map(|input| input.size).sum() + self.inner + .iter() + .map(|input| input.inner.bytes(DIR_IN)) + .sum() } fn load(&mut self) { @@ -60,7 +59,7 @@ impl Inputs { continue; } - input.data = fs::read(&input.filename).unwrap_or_default(); + input.data = fs::read(input.inner.input_path(DIR_IN)).unwrap_or_default(); } } @@ -85,6 +84,8 @@ impl Inputs { continue; } + parser.set_options((&input.options.wgsl_in).into()); + input.module = Some(parser.parse(input.string.as_ref().unwrap()).unwrap()); } } @@ -122,22 +123,22 @@ fn parse_glsl(stage: naga::ShaderStage, inputs: &Inputs) { }; for input in &inputs.inner { parser - .parse(&options, input.string.as_deref().unwrap()) + .parse(&options, &input.inner.read_source(DIR_IN, false)) .unwrap(); } } fn get_wgsl_inputs() -> Inputs { - let mut inputs = Inputs::from_dir("../naga/tests/in/wgsl", "wgsl"); + let mut inputs: Vec = Input::files_in_dir("wgsl", &["wgsl"], DIR_IN) + .map(|a| a.into()) + .collect(); // remove "large-source" tests, they skew the results - inputs - .inner - .retain(|input| !input.filename.contains("large-source")); + inputs.retain(|input| !input.filename().contains("large-source")); assert!(!inputs.is_empty()); - inputs + Inputs { inner: inputs } } fn frontends(c: &mut Criterion) { @@ -178,19 +179,20 @@ fn frontends(c: &mut Criterion) { let mut frontend = naga::front::wgsl::Frontend::new(); b.iter(|| { for input in &inputs_wgsl.inner { + frontend.set_options((&input.options.wgsl_in).into()); frontend.parse(input.string.as_ref().unwrap()).unwrap(); } }); }); - let inputs_spirv = Inputs::from_dir("../naga/tests/in/spv", "spvasm"); + let inputs_spirv = Inputs::from_dir("spv", "spvasm"); assert!(!inputs_spirv.is_empty()); // Assemble all the SPIR-V assembly. let mut assembled_spirv = Vec::>::new(); 'spirv: for input in &inputs_spirv.inner { let output = match Command::new("spirv-as") - .arg(&input.filename) + .arg(input.inner.input_path(DIR_IN)) .arg("-o") .arg("-") .output() @@ -220,19 +222,32 @@ fn frontends(c: &mut Criterion) { let total_bytes = assembled_spirv.iter().map(|spv| spv.len() as u64).sum(); + assert!(assembled_spirv.len() == inputs_spirv.inner.len() || assembled_spirv.is_empty()); + group.throughput(Throughput::Bytes(total_bytes)); group.bench_function("shader: spv-in", |b| { b.iter(|| { - let options = naga::front::spv::Options::default(); - for input in &assembled_spirv { - let parser = naga::front::spv::Frontend::new(input.iter().cloned(), &options); + for (i, input) in assembled_spirv.iter().enumerate() { + let params = &inputs_spirv.inner[i].options; + let SpirvInParameters { + adjust_coordinate_space, + } = params.spv_in; + + let parser = naga::front::spv::Frontend::new( + input.iter().cloned(), + &naga::front::spv::Options { + adjust_coordinate_space, + strict_capabilities: true, + ..Default::default() + }, + ); parser.parse().unwrap(); } }); }); - let mut inputs_vertex = Inputs::from_dir("../naga/tests/in/glsl", "vert"); - let mut inputs_fragment = Inputs::from_dir("../naga/tests/in/glsl", "frag"); + let mut inputs_vertex = Inputs::from_dir("glsl", "vert"); + let mut inputs_fragment = Inputs::from_dir("glsl", "frag"); assert!(!inputs_vertex.is_empty()); assert!(!inputs_fragment.is_empty()); // let mut inputs_compute = Inputs::from_dir("../naga/tests/in/glsl", "comp"); @@ -312,14 +327,16 @@ fn backends(c: &mut Criterion) { group.bench_function("shader: wgsl-out", |b| { b.iter(|| { let mut string = String::new(); - let flags = naga::back::wgsl::WriterFlags::empty(); for input in &inputs.inner { - let mut writer = naga::back::wgsl::Writer::new(&mut string, flags); - let _ = writer.write( - input.module.as_ref().unwrap(), - input.module_info.as_ref().unwrap(), - ); - string.clear(); + if input.options.targets.unwrap().contains(Targets::WGSL) { + let mut writer = + naga::back::wgsl::Writer::new(&mut string, (&input.options.wgsl).into()); + let _ = writer.write( + input.module.as_ref().unwrap(), + input.module_info.as_ref().unwrap(), + ); + string.clear(); + } } }); }); @@ -327,21 +344,28 @@ fn backends(c: &mut Criterion) { group.bench_function("shader: spv-out", |b| { b.iter(|| { let mut data = Vec::new(); - let options = naga::back::spv::Options::default(); + let mut writer = naga::back::spv::Writer::new(&Default::default()).unwrap(); for input in &inputs.inner { - if input.filename.contains("pointer-function-arg") { - // These fail due to https://github.com/gfx-rs/wgpu/issues/7315 - continue; + if input.options.targets.unwrap().contains(Targets::SPIRV) { + if input.filename().contains("pointer-function-arg") { + // These fail due to https://github.com/gfx-rs/wgpu/issues/7315 + continue; + } + let opt = input + .options + .spv + .to_options(input.options.bounds_check_policies, None); + if writer.set_options(&opt).is_ok() { + let _ = writer.write( + input.module.as_ref().unwrap(), + input.module_info.as_ref().unwrap(), + None, + &None, + &mut data, + ); + data.clear(); + } } - let mut writer = naga::back::spv::Writer::new(&options).unwrap(); - let _ = writer.write( - input.module.as_ref().unwrap(), - input.module_info.as_ref().unwrap(), - None, - &None, - &mut data, - ); - data.clear(); } }); }); @@ -350,25 +374,27 @@ fn backends(c: &mut Criterion) { let mut data = Vec::new(); let options = naga::back::spv::Options::default(); for input in &inputs.inner { - if input.filename.contains("pointer-function-arg") { - // These fail due to https://github.com/gfx-rs/wgpu/issues/7315 - continue; - } - let mut writer = naga::back::spv::Writer::new(&options).unwrap(); - let module = input.module.as_ref().unwrap(); - for ep in module.entry_points.iter() { - let pipeline_options = naga::back::spv::PipelineOptions { - shader_stage: ep.stage, - entry_point: ep.name.clone(), - }; - let _ = writer.write( - input.module.as_ref().unwrap(), - input.module_info.as_ref().unwrap(), - Some(&pipeline_options), - &None, - &mut data, - ); - data.clear(); + if input.options.targets.unwrap().contains(Targets::SPIRV) { + if input.filename().contains("pointer-function-arg") { + // These fail due to https://github.com/gfx-rs/wgpu/issues/7315 + continue; + } + let mut writer = naga::back::spv::Writer::new(&options).unwrap(); + let module = input.module.as_ref().unwrap(); + for ep in module.entry_points.iter() { + let pipeline_options = naga::back::spv::PipelineOptions { + shader_stage: ep.stage, + entry_point: ep.name.clone(), + }; + let _ = writer.write( + input.module.as_ref().unwrap(), + input.module_info.as_ref().unwrap(), + Some(&pipeline_options), + &None, + &mut data, + ); + data.clear(); + } } } }); @@ -379,15 +405,17 @@ fn backends(c: &mut Criterion) { let mut string = String::new(); let options = naga::back::msl::Options::default(); for input in &inputs.inner { - let pipeline_options = naga::back::msl::PipelineOptions::default(); - let mut writer = naga::back::msl::Writer::new(&mut string); - let _ = writer.write( - input.module.as_ref().unwrap(), - input.module_info.as_ref().unwrap(), - &options, - &pipeline_options, - ); - string.clear(); + if input.options.targets.unwrap().contains(Targets::METAL) { + let pipeline_options = naga::back::msl::PipelineOptions::default(); + let mut writer = naga::back::msl::Writer::new(&mut string); + let _ = writer.write( + input.module.as_ref().unwrap(), + input.module_info.as_ref().unwrap(), + &options, + &pipeline_options, + ); + string.clear(); + } } }); }); @@ -397,15 +425,17 @@ fn backends(c: &mut Criterion) { let options = naga::back::hlsl::Options::default(); let mut string = String::new(); for input in &inputs.inner { - let pipeline_options = Default::default(); - let mut writer = - naga::back::hlsl::Writer::new(&mut string, &options, &pipeline_options); - let _ = writer.write( - input.module.as_ref().unwrap(), - input.module_info.as_ref().unwrap(), - None, - ); // may fail on unimplemented things - string.clear(); + if input.options.targets.unwrap().contains(Targets::HLSL) { + let pipeline_options = Default::default(); + let mut writer = + naga::back::hlsl::Writer::new(&mut string, &options, &pipeline_options); + let _ = writer.write( + input.module.as_ref().unwrap(), + input.module_info.as_ref().unwrap(), + None, + ); // may fail on unimplemented things + string.clear(); + } } }); }); @@ -420,28 +450,30 @@ fn backends(c: &mut Criterion) { zero_initialize_workgroup_memory: true, }; for input in &inputs.inner { - let module = input.module.as_ref().unwrap(); - let info = input.module_info.as_ref().unwrap(); - for ep in module.entry_points.iter() { - let pipeline_options = naga::back::glsl::PipelineOptions { - shader_stage: ep.stage, - entry_point: ep.name.clone(), - multiview: None, - }; - - // might be `Err` if missing features - if let Ok(mut writer) = naga::back::glsl::Writer::new( - &mut string, - module, - info, - &options, - &pipeline_options, - naga::proc::BoundsCheckPolicies::default(), - ) { - let _ = writer.write(); // might be `Err` if unsupported + if input.options.targets.unwrap().contains(Targets::GLSL) { + let module = input.module.as_ref().unwrap(); + let info = input.module_info.as_ref().unwrap(); + for ep in module.entry_points.iter() { + let pipeline_options = naga::back::glsl::PipelineOptions { + shader_stage: ep.stage, + entry_point: ep.name.clone(), + multiview: None, + }; + + // might be `Err` if missing features + if let Ok(mut writer) = naga::back::glsl::Writer::new( + &mut string, + module, + info, + &options, + &pipeline_options, + naga::proc::BoundsCheckPolicies::default(), + ) { + let _ = writer.write(); // might be `Err` if unsupported + } + + string.clear(); } - - string.clear(); } } }); diff --git a/naga/Cargo.toml b/naga/Cargo.toml index 02eda4c198a..824bc9d6d87 100644 --- a/naga/Cargo.toml +++ b/naga/Cargo.toml @@ -20,20 +20,20 @@ all-features = true [features] default = [] -dot-out = [] -glsl-in = ["dep:pp-rs"] -glsl-out = [] +dot-out = ["wgpu-test/dot-out"] +glsl-in = ["dep:pp-rs", "wgpu-test/glsl-in"] +glsl-out = ["wgpu-test/glsl-out"] ## Enables outputting to the Metal Shading Language (MSL). ## ## This enables MSL output regardless of the target platform. ## If you want to enable it only when targeting iOS/tvOS/watchOS/macOS, use `naga/msl-out-if-target-apple`. -msl-out = [] +msl-out = ["wgpu-test/msl-out"] ## Enables outputting to the Metal Shading Language (MSL) only if the target platform is iOS/tvOS/watchOS/macOS. ## ## If you want to enable MSL output it regardless of the target platform, use `naga/msl-out`. -msl-out-if-target-apple = [] +msl-out-if-target-apple = ["wgpu-test/msl-out"] serialize = [ "dep:serde", @@ -56,16 +56,16 @@ arbitrary = [ "half/arbitrary", "half/std", ] -spv-in = ["dep:petgraph", "petgraph/graphmap", "dep:spirv"] -spv-out = ["dep:spirv"] -wgsl-in = ["dep:hexf-parse", "dep:unicode-ident"] -wgsl-out = [] +spv-in = ["dep:petgraph", "petgraph/graphmap", "dep:spirv", "wgpu-test/spv-in"] +spv-out = ["dep:spirv", "wgpu-test/spv-out"] +wgsl-in = ["dep:hexf-parse", "dep:unicode-ident", "wgpu-test/wgsl-in"] +wgsl-out = ["wgpu-test/wgsl-out"] ## Enables outputting to HLSL (Microsoft's High-Level Shader Language). ## ## This enables HLSL output regardless of the target platform. ## If you want to enable it only when targeting Windows, use `hlsl-out-if-target-windows`. -hlsl-out = [] +hlsl-out = ["wgpu-test/hlsl-out"] ## Enables outputting to HLSL (Microsoft's High-Level Shader Language) only if the target platform is Windows. ## @@ -116,10 +116,9 @@ itertools.workspace = true ron.workspace = true rspirv.workspace = true serde = { workspace = true, features = ["default", "derive"] } -spirv = { workspace = true, features = ["deserialize"] } strum = { workspace = true } -toml.workspace = true walkdir.workspace = true +wgpu-test.workspace = true [lints.clippy] std_instead_of_alloc = "warn" diff --git a/naga/tests/naga/snapshots.rs b/naga/tests/naga/snapshots.rs index e2288eee918..f08dbcb59dd 100644 --- a/naga/tests/naga/snapshots.rs +++ b/naga/tests/naga/snapshots.rs @@ -1,389 +1,12 @@ -// A lot of the code can be unused based on configuration flags, -// the corresponding warnings aren't helpful. -#![allow(dead_code, unused_imports)] - -use core::fmt::Write; - -use std::{ - fs, - path::{Path, PathBuf}, -}; - use naga::compact::KeepUnused; -use ron::de; - -const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR"); -const BASE_DIR_IN: &str = "tests/in"; -const BASE_DIR_OUT: &str = "tests/out"; - -bitflags::bitflags! { - #[derive(Clone, Copy, serde::Deserialize)] - #[serde(transparent)] - #[derive(Debug, Eq, PartialEq)] - struct Targets: u32 { - /// A serialization of the `naga::Module`, in RON format. - const IR = 1; - - /// A serialization of the `naga::valid::ModuleInfo`, in RON format. - const ANALYSIS = 1 << 1; - - const SPIRV = 1 << 2; - const METAL = 1 << 3; - const GLSL = 1 << 4; - const DOT = 1 << 5; - const HLSL = 1 << 6; - const WGSL = 1 << 7; - const NO_VALIDATION = 1 << 8; - } -} - -impl Targets { - /// Defaults for `spv` and `glsl` snapshots. - fn non_wgsl_default() -> Self { - Targets::WGSL - } - - /// Defaults for `wgsl` snapshots. - fn wgsl_default() -> Self { - Targets::HLSL | Targets::SPIRV | Targets::GLSL | Targets::METAL | Targets::WGSL - } -} - -#[derive(serde::Deserialize)] -struct SpvOutVersion(u8, u8); -impl Default for SpvOutVersion { - fn default() -> Self { - SpvOutVersion(1, 1) - } -} - -#[cfg(all(feature = "deserialize", spv_out))] -#[derive(serde::Deserialize)] -struct BindingMapSerialization { - resource_binding: naga::ResourceBinding, - bind_target: naga::back::spv::BindingInfo, -} - -#[cfg(all(feature = "deserialize", spv_out))] -fn deserialize_binding_map<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize; - - let vec = Vec::::deserialize(deserializer)?; - let mut map = naga::back::spv::BindingMap::default(); - for item in vec { - map.insert(item.resource_binding, item.bind_target); - } - Ok(map) -} - -#[derive(Default, serde::Deserialize)] -#[serde(default)] -struct WgslInParameters { - parse_doc_comments: bool, -} - -#[derive(Default, serde::Deserialize)] -#[serde(default)] -struct SpirvInParameters { - adjust_coordinate_space: bool, -} - -#[derive(Default, serde::Deserialize)] -#[serde(default)] -struct SpirvOutParameters { - version: SpvOutVersion, - capabilities: naga::FastHashSet, - debug: bool, - adjust_coordinate_space: bool, - force_point_size: bool, - clamp_frag_depth: bool, - separate_entry_points: bool, - #[cfg(all(feature = "deserialize", spv_out))] - #[serde(deserialize_with = "deserialize_binding_map")] - binding_map: naga::back::spv::BindingMap, -} - -#[derive(Default, serde::Deserialize)] -#[serde(default)] -struct WgslOutParameters { - explicit_types: bool, -} - -#[derive(Default, serde::Deserialize)] -struct FragmentModule { - path: String, - entry_point: String, -} - -#[derive(Default, serde::Deserialize)] -#[serde(default)] -struct Parameters { - // -- GOD MODE -- - god_mode: bool, - - // -- wgsl-in options -- - #[serde(rename = "wgsl-in")] - wgsl_in: WgslInParameters, - - // -- spirv-in options -- - #[serde(rename = "spv-in")] - spv_in: SpirvInParameters, - - // -- SPIR-V options -- - spv: SpirvOutParameters, - - /// Defaults to [`Targets::non_wgsl_default()`] for `spv` and `glsl` snapshots, - /// and [`Targets::wgsl_default()`] for `wgsl` snapshots. - targets: Option, - - // -- MSL options -- - #[cfg(all(feature = "deserialize", msl_out))] - msl: naga::back::msl::Options, - #[cfg(all(feature = "deserialize", msl_out))] - #[serde(default)] - msl_pipeline: naga::back::msl::PipelineOptions, - - // -- GLSL options -- - #[cfg(all(feature = "deserialize", glsl_out))] - glsl: naga::back::glsl::Options, - glsl_exclude_list: naga::FastHashSet, - #[cfg(all(feature = "deserialize", glsl_out))] - glsl_multiview: Option, - - // -- HLSL options -- - #[cfg(all(feature = "deserialize", hlsl_out))] - hlsl: naga::back::hlsl::Options, - - // -- WGSL options -- - wgsl: WgslOutParameters, - - // -- General options -- - - // Allow backends to be aware of the fragment module. - // Is the name of a WGSL file in the same directory as the test file. - fragment_module: Option, - - #[cfg(feature = "deserialize")] - bounds_check_policies: naga::proc::BoundsCheckPolicies, - - #[cfg(all(feature = "deserialize", any(hlsl_out, msl_out, spv_out, glsl_out)))] - pipeline_constants: naga::back::PipelineConstants, -} - -/// Information about a shader input file. -#[derive(Debug)] -struct Input { - /// The subdirectory of `tests/in` to which this input belongs, if any. - /// - /// If the subdirectory is omitted, we assume that the output goes - /// to "wgsl". - subdirectory: PathBuf, - - /// The input filename name, without a directory. - file_name: PathBuf, - - /// True if output filenames should add the output extension on top of - /// `file_name`'s existing extension, rather than replacing it. - /// - /// This is used by `convert_snapshots_glsl`, which wants to take input files - /// like `210-bevy-2d-shader.frag` and just add `.wgsl` to it, producing - /// `210-bevy-2d-shader.frag.wgsl`. - keep_input_extension: bool, -} - -impl Input { - /// Read an input file and its corresponding parameters file. - /// - /// Given `input`, the relative path of a shader input file, return - /// a `Source` value containing its path, code, and parameters. - /// - /// The `input` path is interpreted relative to the `BASE_DIR_IN` - /// subdirectory of the directory given by the `CARGO_MANIFEST_DIR` - /// environment variable. - fn new(subdirectory: &str, name: &str, extension: &str) -> Input { - Input { - subdirectory: PathBuf::from(subdirectory), - // Don't wipe out any extensions on `name`, as - // `with_extension` would do. - file_name: PathBuf::from(format!("{name}.{extension}")), - keep_input_extension: false, - } - } - - /// Return an iterator that produces an `Input` for each entry in `subdirectory`. - fn files_in_dir( - subdirectory: &'static str, - file_extensions: &'static [&'static str], - ) -> impl Iterator + 'static { - let input_directory = Path::new(CRATE_ROOT).join(BASE_DIR_IN).join(subdirectory); - - let entries = match std::fs::read_dir(&input_directory) { - Ok(entries) => entries, - Err(err) => panic!( - "Error opening directory '{}': {}", - input_directory.display(), - err - ), - }; - - entries.filter_map(move |result| { - let entry = result.expect("error reading directory"); - if !entry.file_type().unwrap().is_file() { - return None; - } +use wgpu_test::naga::*; - let file_name = PathBuf::from(entry.file_name()); - let extension = file_name - .extension() - .expect("all files in snapshot input directory should have extensions"); - - if !file_extensions.contains(&extension.to_str().unwrap()) { - return None; - } - - if let Ok(pat) = std::env::var("NAGA_SNAPSHOT") { - if !file_name.to_string_lossy().contains(&pat) { - return None; - } - } - - let input = Input::new( - subdirectory, - file_name.file_stem().unwrap().to_str().unwrap(), - extension.to_str().unwrap(), - ); - Some(input) - }) - } - - /// Return the path to the input directory. - fn input_directory(&self) -> PathBuf { - let mut dir = Path::new(CRATE_ROOT).join(BASE_DIR_IN); - dir.push(&self.subdirectory); - dir - } - - /// Return the path to the output directory. - fn output_directory(subdirectory: &str) -> PathBuf { - let mut dir = Path::new(CRATE_ROOT).join(BASE_DIR_OUT); - dir.push(subdirectory); - dir - } - - /// Return the path to the input file. - fn input_path(&self) -> PathBuf { - let mut input = self.input_directory(); - input.push(&self.file_name); - input - } - - fn output_path(&self, subdirectory: &str, extension: &str) -> PathBuf { - let mut output = Self::output_directory(subdirectory); - if self.keep_input_extension { - let file_name = format!( - "{}-{}.{}", - self.subdirectory.display(), - self.file_name.display(), - extension - ); - - output.push(&file_name); - } else { - let file_name = format!( - "{}-{}", - self.subdirectory.display(), - self.file_name.display() - ); - - output.push(&file_name); - output.set_extension(extension); - } - output - } - - /// Return the contents of the input file as a string. - fn read_source(&self) -> String { - println!("Processing '{}'", self.file_name.display()); - let input_path = self.input_path(); - match fs::read_to_string(&input_path) { - Ok(source) => source, - Err(err) => { - panic!( - "Couldn't read shader input file `{}`: {}", - input_path.display(), - err - ); - } - } - } - - /// Return the contents of the input file as a vector of bytes. - fn read_bytes(&self) -> Vec { - println!("Processing '{}'", self.file_name.display()); - let input_path = self.input_path(); - match fs::read(&input_path) { - Ok(bytes) => bytes, - Err(err) => { - panic!( - "Couldn't read shader input file `{}`: {}", - input_path.display(), - err - ); - } - } - } - - /// Return this input's parameter file, parsed. - fn read_parameters(&self) -> Parameters { - let mut param_path = self.input_path(); - param_path.set_extension("toml"); - let mut params = match fs::read_to_string(¶m_path) { - Ok(string) => match toml::de::from_str(&string) { - Ok(params) => params, - Err(e) => panic!( - "Couldn't parse param file: {} due to: {e}", - param_path.display() - ), - }, - Err(_) => Parameters::default(), - }; - - if params.targets.is_none() { - match self.input_path().extension().unwrap().to_str().unwrap() { - "wgsl" => params.targets = Some(Targets::wgsl_default()), - "spvasm" => params.targets = Some(Targets::non_wgsl_default()), - "vert" | "frag" | "comp" => params.targets = Some(Targets::non_wgsl_default()), - e => { - panic!("Unknown extension: {e}"); - } - } - } - - params - } - - /// Write `data` to a file corresponding to this input file in - /// `subdirectory`, with `extension`. - fn write_output_file(&self, subdirectory: &str, extension: &str, data: impl AsRef<[u8]>) { - let output_path = self.output_path(subdirectory, extension); - fs::create_dir_all(output_path.parent().unwrap()).unwrap(); - if let Err(err) = fs::write(&output_path, data) { - panic!("Error writing {}: {}", output_path.display(), err); - } - } -} - -#[cfg(hlsl_out)] -type FragmentEntryPoint<'a> = naga::back::hlsl::FragmentEntryPoint<'a>; -#[cfg(not(hlsl_out))] -type FragmentEntryPoint<'a> = (); +const DIR_IN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/in"); +const DIR_OUT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/out"); #[allow(unused_variables)] fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<&str>) { - let params = input.read_parameters(); + let params = input.read_parameters(DIR_IN); let name = &input.file_name; let targets = params.targets.unwrap(); @@ -402,12 +25,11 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& ) }; - #[cfg(feature = "serialize")] { if targets.contains(Targets::IR) { let config = ron::ser::PrettyConfig::default().new_line("\n".to_string()); let string = ron::ser::to_string_pretty(module, config).unwrap(); - input.write_output_file("ir", "ron", string); + input.write_output_file("ir", "ron", string, DIR_OUT); } } @@ -438,12 +60,11 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& // snapshots makes the output independent of unused arena entries. naga::compact::compact(module, KeepUnused::No); - #[cfg(feature = "serialize")] { if targets.contains(Targets::IR) { let config = ron::ser::PrettyConfig::default().new_line("\n".to_string()); let string = ron::ser::to_string_pretty(module, config).unwrap(); - input.write_output_file("ir", "compact.ron", string); + input.write_output_file("ir", "compact.ron", string, DIR_OUT); } } @@ -460,16 +81,15 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& }) }; - #[cfg(feature = "serialize")] { if targets.contains(Targets::ANALYSIS) { let config = ron::ser::PrettyConfig::default().new_line("\n".to_string()); let string = ron::ser::to_string_pretty(&info, config).unwrap(); - input.write_output_file("analysis", "info.ron", string); + input.write_output_file("analysis", "info.ron", string, DIR_OUT); } } - #[cfg(all(feature = "deserialize", spv_out))] + #[cfg(feature = "spv-out")] { if targets.contains(Targets::SPIRV) { let mut debug_info = None; @@ -495,7 +115,7 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& ); } } - #[cfg(all(feature = "deserialize", msl_out))] + #[cfg(feature = "msl-out")] { if targets.contains(Targets::METAL) { write_output_msl( @@ -509,7 +129,7 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& ); } } - #[cfg(all(feature = "deserialize", glsl_out))] + #[cfg(feature = "glsl-out")] { if targets.contains(Targets::GLSL) { for ep in module.entry_points.iter() { @@ -530,20 +150,20 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& } } } - #[cfg(dot_out)] + #[cfg(feature = "dot-out")] { if targets.contains(Targets::DOT) { let string = naga::back::dot::write(module, Some(&info), Default::default()).unwrap(); - input.write_output_file("dot", "dot", string); + input.write_output_file("dot", "dot", string, DIR_OUT); } } - #[cfg(all(feature = "deserialize", hlsl_out))] + #[cfg(feature = "hlsl-out")] { if targets.contains(Targets::HLSL) { let frag_module; let mut frag_ep = None; if let Some(ref module_spec) = params.fragment_module { - let full_path = input.input_directory().join(&module_spec.path); + let full_path = input.input_directory(DIR_IN).join(&module_spec.path); assert_eq!( full_path.extension().unwrap().to_string_lossy(), @@ -551,7 +171,7 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& "Currently all fragment modules must be in WGSL" ); - let frag_src = fs::read_to_string(full_path).unwrap(); + let frag_src = std::fs::read_to_string(full_path).unwrap(); frag_module = naga::front::wgsl::parse_str(&frag_src) .expect("Failed to parse fragment module"); @@ -575,7 +195,7 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& ); } } - #[cfg(all(feature = "deserialize", wgsl_out))] + #[cfg(feature = "wgsl-out")] { if targets.contains(Targets::WGSL) { write_output_wgsl(input, module, &info, ¶ms.wgsl); @@ -583,7 +203,7 @@ fn check_targets(input: &Input, module: &mut naga::Module, source_code: Option<& } } -#[cfg(spv_out)] +#[cfg(feature = "spv-out")] fn write_output_spv( input: &Input, module: &naga::Module, @@ -594,31 +214,8 @@ fn write_output_spv( pipeline_constants: &naga::back::PipelineConstants, ) { use naga::back::spv; - use rspirv::binary::Disassemble; - let mut flags = spv::WriterFlags::LABEL_VARYINGS; - flags.set(spv::WriterFlags::DEBUG, params.debug); - flags.set( - spv::WriterFlags::ADJUST_COORDINATE_SPACE, - params.adjust_coordinate_space, - ); - flags.set(spv::WriterFlags::FORCE_POINT_SIZE, params.force_point_size); - flags.set(spv::WriterFlags::CLAMP_FRAG_DEPTH, params.clamp_frag_depth); - - let options = spv::Options { - lang_version: (params.version.0, params.version.1), - flags, - capabilities: if params.capabilities.is_empty() { - None - } else { - Some(params.capabilities.clone()) - }, - bounds_check_policies, - binding_map: params.binding_map.clone(), - zero_initialize_workgroup_memory: spv::ZeroInitializeWorkgroupMemoryMode::Polyfill, - force_loop_bounding: true, - debug_info, - }; + let options = params.to_options(bounds_check_policies, debug_info); let (module, info) = naga::back::pipeline_constants::process_overrides(module, info, None, pipeline_constants) @@ -644,7 +241,7 @@ fn write_output_spv( } } -#[cfg(spv_out)] +#[cfg(feature = "spv-out")] fn write_output_spv_inner( input: &Input, module: &naga::Module, @@ -667,10 +264,10 @@ fn write_output_spv_inner( } else { dis }; - input.write_output_file("spv", extension, dis); + input.write_output_file("spv", extension, dis, DIR_OUT); } -#[cfg(msl_out)] +#[cfg(feature = "msl-out")] fn write_output_msl( input: &Input, module: &naga::Module, @@ -699,10 +296,10 @@ fn write_output_msl( } } - input.write_output_file("msl", "msl", string); + input.write_output_file("msl", "msl", string, DIR_OUT); } -#[cfg(glsl_out)] +#[cfg(feature = "glsl-out")] #[allow(clippy::too_many_arguments)] fn write_output_glsl( input: &Input, @@ -741,10 +338,10 @@ fn write_output_glsl( writer.write().expect("GLSL write failed"); let extension = format!("{ep_name}.{stage:?}.glsl"); - input.write_output_file("glsl", &extension, buffer); + input.write_output_file("glsl", &extension, buffer, DIR_OUT); } -#[cfg(hlsl_out)] +#[cfg(feature = "hlsl-out")] fn write_output_hlsl( input: &Input, module: &naga::Module, @@ -753,7 +350,6 @@ fn write_output_hlsl( pipeline_constants: &naga::back::PipelineConstants, frag_ep: Option, ) { - use core::fmt::Write as _; use naga::back::hlsl; println!("generating HLSL"); @@ -769,7 +365,7 @@ fn write_output_hlsl( .write(&module, &info, frag_ep.as_ref()) .expect("HLSL write failed"); - input.write_output_file("hlsl", "hlsl", buffer); + input.write_output_file("hlsl", "hlsl", buffer, DIR_OUT); // We need a config file for validation script // This file contains an info about profiles (shader stages) contains inside generated shader @@ -796,10 +392,12 @@ fn write_output_hlsl( }); } - config.to_file(input.output_path("hlsl", "ron")).unwrap(); + config + .to_file(input.output_path("hlsl", "ron", DIR_OUT)) + .unwrap(); } -#[cfg(wgsl_out)] +#[cfg(feature = "wgsl-out")] fn write_output_wgsl( input: &Input, module: &naga::Module, @@ -810,12 +408,9 @@ fn write_output_wgsl( println!("generating WGSL"); - let mut flags = wgsl::WriterFlags::empty(); - flags.set(wgsl::WriterFlags::EXPLICIT_TYPES, params.explicit_types); - - let string = wgsl::write_string(module, info, flags).expect("WGSL write failed"); + let string = wgsl::write_string(module, info, params.into()).expect("WGSL write failed"); - input.write_output_file("wgsl", "wgsl", string); + input.write_output_file("wgsl", "wgsl", string, DIR_OUT); } // While we _can_ run this test under miri, it is extremely slow (>5 minutes), @@ -826,21 +421,19 @@ fn write_output_wgsl( fn convert_snapshots_wgsl() { let _ = env_logger::try_init(); - for input in Input::files_in_dir("wgsl", &["wgsl"]) { - let source = input.read_source(); + for input in Input::files_in_dir("wgsl", &["wgsl"], DIR_IN) { + let source = input.read_source(DIR_IN, true); // crlf will make the large split output different on different platform let source = source.replace('\r', ""); - let params = input.read_parameters(); - let WgslInParameters { parse_doc_comments } = params.wgsl_in; + let params = input.read_parameters(DIR_IN); - let options = naga::front::wgsl::Options { parse_doc_comments }; - let mut frontend = naga::front::wgsl::Frontend::new_with_options(options); + let mut frontend = naga::front::wgsl::Frontend::new_with_options((¶ms.wgsl_in).into()); match frontend.parse(&source) { Ok(mut module) => check_targets(&input, &mut module, Some(&source)), Err(e) => panic!( "{}", - e.emit_to_string_with_path(&source, input.input_path()) + e.emit_to_string_with_path(&source, input.input_path(DIR_IN)) ), } } @@ -855,11 +448,11 @@ fn convert_snapshots_spv() { let _ = env_logger::try_init(); - for input in Input::files_in_dir("spv", &["spvasm"]) { + for input in Input::files_in_dir("spv", &["spvasm"], DIR_IN) { println!("Assembling '{}'", input.file_name.display()); let command = Command::new("spirv-as") - .arg(input.input_path()) + .arg(input.input_path(DIR_IN)) .arg("-o") .arg("-") .output() @@ -878,20 +471,10 @@ fn convert_snapshots_spv() { ); } - let params = input.read_parameters(); - let SpirvInParameters { - adjust_coordinate_space, - } = params.spv_in; - - let mut module = naga::front::spv::parse_u8_slice( - &command.stdout, - &naga::front::spv::Options { - adjust_coordinate_space, - strict_capabilities: true, - ..Default::default() - }, - ) - .unwrap(); + let params = input.read_parameters(DIR_IN); + + let mut module = + naga::front::spv::parse_u8_slice(&command.stdout, &(¶ms.spv_in).into()).unwrap(); check_targets(&input, &mut module, None); } @@ -906,7 +489,7 @@ fn convert_snapshots_spv() { fn convert_snapshots_glsl() { let _ = env_logger::try_init(); - for input in Input::files_in_dir("glsl", &["vert", "frag", "comp"]) { + for input in Input::files_in_dir("glsl", &["vert", "frag", "comp"], DIR_IN) { let input = Input { keep_input_extension: true, ..input @@ -927,7 +510,7 @@ fn convert_snapshots_glsl() { stage, defines: Default::default(), }, - &input.read_source(), + &input.read_source(DIR_IN, true), ) .unwrap(); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 95301df9488..581af3e879c 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -33,11 +33,29 @@ webgl = ["wgpu/webgl"] # allows us to force the build to have profiling code enabled so we can test that configuration. test-build-with-profiling = ["profiling/type-check"] +# Naga forwarded features +glsl-in = ["naga/glsl-in"] +glsl-out = ["naga/glsl-out"] +spv-in = ["naga/spv-in"] +spv-out = ["naga/spv-out"] +wgsl-in = ["naga/wgsl-in"] +wgsl-out = ["naga/wgsl-out"] +msl-out = ["naga/msl-out"] +dot-out = ["naga/dot-out"] +hlsl-out = ["naga/hlsl-out"] + [dependencies] wgpu = { workspace = true, features = ["noop"] } wgpu-hal = { workspace = true, features = ["validation_canary"] } wgpu-macros.workspace = true +# Naga stuff that lives here due to sharing logic with benchmarks +naga = { workspace = true, features = ["serialize", "deserialize"] } +spirv = { workspace = true, features = ["deserialize"] } +rspirv.workspace = true +ron.workspace = true +toml.workspace = true + anyhow.workspace = true arrayvec.workspace = true approx.workspace = true diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 22afd7ecf77..775a3fdfc2b 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -7,6 +7,7 @@ mod expectations; pub mod image; mod init; mod isolation; +pub mod naga; pub mod native; mod params; mod poll; diff --git a/tests/src/naga.rs b/tests/src/naga.rs new file mode 100644 index 00000000000..075b26e46c0 --- /dev/null +++ b/tests/src/naga.rs @@ -0,0 +1,454 @@ +// A lot of the code can be unused based on configuration flags, +// the corresponding warnings aren't helpful. +#![allow(dead_code, unused_imports)] + +use core::fmt::Write; + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use naga::compact::KeepUnused; +use ron::de; + +bitflags::bitflags! { + #[derive(Clone, Copy, serde::Deserialize)] + #[serde(transparent)] + #[derive(Debug, Eq, PartialEq)] + pub struct Targets: u32 { + /// A serialization of the `naga::Module`, in RON format. + const IR = 1; + + /// A serialization of the `naga::valid::ModuleInfo`, in RON format. + const ANALYSIS = 1 << 1; + + const SPIRV = 1 << 2; + const METAL = 1 << 3; + const GLSL = 1 << 4; + const DOT = 1 << 5; + const HLSL = 1 << 6; + const WGSL = 1 << 7; + const NO_VALIDATION = 1 << 8; + } +} + +impl Targets { + /// Defaults for `spv` and `glsl` snapshots. + pub fn non_wgsl_default() -> Self { + Targets::WGSL + } + + /// Defaults for `wgsl` snapshots. + pub fn wgsl_default() -> Self { + Targets::HLSL | Targets::SPIRV | Targets::GLSL | Targets::METAL | Targets::WGSL + } +} + +#[derive(serde::Deserialize)] +pub struct SpvOutVersion(pub u8, pub u8); +impl Default for SpvOutVersion { + fn default() -> Self { + SpvOutVersion(1, 1) + } +} + +#[cfg(feature = "spv-out")] +#[derive(serde::Deserialize)] +pub struct BindingMapSerialization { + pub resource_binding: naga::ResourceBinding, + pub bind_target: naga::back::spv::BindingInfo, +} + +#[cfg(feature = "spv-out")] +pub fn deserialize_binding_map<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let vec = Vec::::deserialize(deserializer)?; + let mut map = naga::back::spv::BindingMap::default(); + for item in vec { + map.insert(item.resource_binding, item.bind_target); + } + Ok(map) +} + +#[derive(Default, serde::Deserialize)] +#[serde(default)] +pub struct WgslInParameters { + pub parse_doc_comments: bool, +} +#[cfg(feature = "wgsl-in")] +impl From<&WgslInParameters> for naga::front::wgsl::Options { + fn from(value: &WgslInParameters) -> Self { + Self { + parse_doc_comments: value.parse_doc_comments, + } + } +} + +#[derive(Default, serde::Deserialize)] +#[serde(default)] +pub struct SpirvInParameters { + pub adjust_coordinate_space: bool, +} +#[cfg(feature = "spv-in")] +impl From<&SpirvInParameters> for naga::front::spv::Options { + fn from(value: &SpirvInParameters) -> Self { + Self { + adjust_coordinate_space: value.adjust_coordinate_space, + ..Default::default() + } + } +} + +#[derive(Default, serde::Deserialize)] +#[serde(default)] +pub struct SpirvOutParameters { + pub version: SpvOutVersion, + pub capabilities: naga::FastHashSet, + pub debug: bool, + pub adjust_coordinate_space: bool, + pub force_point_size: bool, + pub clamp_frag_depth: bool, + pub separate_entry_points: bool, + #[cfg(feature = "spv-out")] + #[serde(deserialize_with = "deserialize_binding_map")] + pub binding_map: naga::back::spv::BindingMap, +} +#[cfg(feature = "spv-out")] +impl SpirvOutParameters { + pub fn to_options<'a>( + &'a self, + bounds_check_policies: naga::proc::BoundsCheckPolicies, + debug_info: Option>, + ) -> naga::back::spv::Options<'a> { + use naga::back::spv; + let mut flags = spv::WriterFlags::LABEL_VARYINGS; + flags.set(spv::WriterFlags::DEBUG, self.debug); + flags.set( + spv::WriterFlags::ADJUST_COORDINATE_SPACE, + self.adjust_coordinate_space, + ); + flags.set(spv::WriterFlags::FORCE_POINT_SIZE, self.force_point_size); + flags.set(spv::WriterFlags::CLAMP_FRAG_DEPTH, self.clamp_frag_depth); + naga::back::spv::Options { + lang_version: (self.version.0, self.version.1), + flags, + capabilities: if self.capabilities.is_empty() { + None + } else { + Some(self.capabilities.clone()) + }, + bounds_check_policies, + binding_map: self.binding_map.clone(), + zero_initialize_workgroup_memory: spv::ZeroInitializeWorkgroupMemoryMode::Polyfill, + force_loop_bounding: true, + debug_info, + } + } +} + +#[derive(Default, serde::Deserialize)] +#[serde(default)] +pub struct WgslOutParameters { + pub explicit_types: bool, +} +#[cfg(feature = "wgsl-out")] +impl From<&WgslOutParameters> for naga::back::wgsl::WriterFlags { + fn from(value: &WgslOutParameters) -> Self { + let mut flags = Self::empty(); + flags.set(Self::EXPLICIT_TYPES, value.explicit_types); + flags + } +} + +#[derive(Default, serde::Deserialize)] +pub struct FragmentModule { + pub path: String, + pub entry_point: String, +} + +#[derive(Default, serde::Deserialize)] +#[serde(default)] +pub struct Parameters { + // -- GOD MODE -- + pub god_mode: bool, + + // -- wgsl-in options -- + #[serde(rename = "wgsl-in")] + pub wgsl_in: WgslInParameters, + + // -- spirv-in options -- + #[serde(rename = "spv-in")] + pub spv_in: SpirvInParameters, + + // -- SPIR-V options -- + pub spv: SpirvOutParameters, + + /// Defaults to [`Targets::non_wgsl_default()`] for `spv` and `glsl` snapshots, + /// and [`Targets::wgsl_default()`] for `wgsl` snapshots. + pub targets: Option, + + // -- MSL options -- + #[cfg(feature = "msl-out")] + pub msl: naga::back::msl::Options, + #[cfg(feature = "msl-out")] + #[serde(default)] + pub msl_pipeline: naga::back::msl::PipelineOptions, + + // -- GLSL options -- + #[cfg(feature = "glsl-out")] + pub glsl: naga::back::glsl::Options, + pub glsl_exclude_list: naga::FastHashSet, + #[cfg(feature = "glsl-out")] + pub glsl_multiview: Option, + + // -- HLSL options -- + #[cfg(feature = "hlsl-out")] + pub hlsl: naga::back::hlsl::Options, + + // -- WGSL options -- + pub wgsl: WgslOutParameters, + + // -- General options -- + + // Allow backends to be aware of the fragment module. + // Is the name of a WGSL file in the same directory as the test file. + pub fragment_module: Option, + + pub bounds_check_policies: naga::proc::BoundsCheckPolicies, + + #[cfg(any( + feature = "hlsl-out", + feature = "msl-out", + feature = "spv-out", + feature = "glsl-out" + ))] + pub pipeline_constants: naga::back::PipelineConstants, +} + +/// Information about a shader input file. +#[derive(Debug)] +pub struct Input { + /// The subdirectory of `tests/in` to which this input belongs, if any. + /// + /// If the subdirectory is omitted, we assume that the output goes + /// to "wgsl". + pub subdirectory: PathBuf, + + /// The input filename name, without a directory. + pub file_name: PathBuf, + + /// True if output filenames should add the output extension on top of + /// `file_name`'s existing extension, rather than replacing it. + /// + /// This is used by `convert_snapshots_glsl`, which wants to take input files + /// like `210-bevy-2d-shader.frag` and just add `.wgsl` to it, producing + /// `210-bevy-2d-shader.frag.wgsl`. + pub keep_input_extension: bool, +} + +impl Input { + /// Read an input file and its corresponding parameters file. + /// + /// Given `input`, the relative path of a shader input file, return + /// a `Source` value containing its path, code, and parameters. + /// + /// The `input` path is interpreted relative to the `BASE_DIR_IN` + /// subdirectory of the directory given by the `CARGO_MANIFEST_DIR` + /// environment variable. + pub fn new(subdirectory: &str, name: &str, extension: &str) -> Input { + Input { + subdirectory: PathBuf::from(subdirectory), + // Don't wipe out any extensions on `name`, as + // `with_extension` would do. + file_name: PathBuf::from(format!("{name}.{extension}")), + keep_input_extension: false, + } + } + + /// Return an iterator that produces an `Input` for each entry in `subdirectory`. + pub fn files_in_dir<'a>( + subdirectory: &'a str, + file_extensions: &'a [&'a str], + dir_in: &str, + ) -> impl Iterator + 'a { + let input_directory = Path::new(dir_in).join(subdirectory); + + let entries = match std::fs::read_dir(&input_directory) { + Ok(entries) => entries, + Err(err) => panic!( + "Error opening directory '{}': {}", + input_directory.display(), + err + ), + }; + + entries.filter_map(move |result| { + let entry = result.expect("error reading directory"); + if !entry.file_type().unwrap().is_file() { + return None; + } + + let file_name = PathBuf::from(entry.file_name()); + let extension = file_name + .extension() + .expect("all files in snapshot input directory should have extensions"); + + if !file_extensions.contains(&extension.to_str().unwrap()) { + return None; + } + + if let Ok(pat) = std::env::var("NAGA_SNAPSHOT") { + if !file_name.to_string_lossy().contains(&pat) { + return None; + } + } + + let input = Input::new( + subdirectory, + file_name.file_stem().unwrap().to_str().unwrap(), + extension.to_str().unwrap(), + ); + Some(input) + }) + } + + /// Return the path to the input directory. + pub fn input_directory(&self, dir_in: &str) -> PathBuf { + Path::new(dir_in).join(&self.subdirectory) + } + + /// Return the path to the output directory. + pub fn output_directory(subdirectory: &str, dir_out: &str) -> PathBuf { + Path::new(dir_out).join(subdirectory) + } + + /// Return the path to the input file. + pub fn input_path(&self, dir_in: &str) -> PathBuf { + let mut input = self.input_directory(dir_in); + input.push(&self.file_name); + input + } + + pub fn output_path(&self, subdirectory: &str, extension: &str, dir_out: &str) -> PathBuf { + let mut output = Self::output_directory(subdirectory, dir_out); + if self.keep_input_extension { + let file_name = format!( + "{}-{}.{}", + self.subdirectory.display(), + self.file_name.display(), + extension + ); + + output.push(&file_name); + } else { + let file_name = format!( + "{}-{}", + self.subdirectory.display(), + self.file_name.display() + ); + + output.push(&file_name); + output.set_extension(extension); + } + output + } + + /// Return the contents of the input file as a string. + pub fn read_source(&self, dir_in: &str, print: bool) -> String { + if print { + println!("Processing '{}'", self.file_name.display()); + } + let input_path = self.input_path(dir_in); + match fs::read_to_string(&input_path) { + Ok(source) => source, + Err(err) => { + panic!( + "Couldn't read shader input file `{}`: {}", + input_path.display(), + err + ); + } + } + } + + /// Return the contents of the input file as a vector of bytes. + pub fn read_bytes(&self, dir_in: &str, print: bool) -> Vec { + if print { + println!("Processing '{}'", self.file_name.display()); + } + let input_path = self.input_path(dir_in); + match fs::read(&input_path) { + Ok(bytes) => bytes, + Err(err) => { + panic!( + "Couldn't read shader input file `{}`: {}", + input_path.display(), + err + ); + } + } + } + + pub fn bytes(&self, dir_in: &str) -> u64 { + let input_path = self.input_path(dir_in); + std::fs::metadata(input_path).unwrap().len() + } + + /// Return this input's parameter file, parsed. + pub fn read_parameters(&self, dir_in: &str) -> Parameters { + let mut param_path = self.input_path(dir_in); + param_path.set_extension("toml"); + let mut params = match fs::read_to_string(¶m_path) { + Ok(string) => match toml::de::from_str(&string) { + Ok(params) => params, + Err(e) => panic!( + "Couldn't parse param file: {} due to: {e}", + param_path.display() + ), + }, + Err(_) => Parameters::default(), + }; + + if params.targets.is_none() { + match self + .input_path(dir_in) + .extension() + .unwrap() + .to_str() + .unwrap() + { + "wgsl" => params.targets = Some(Targets::wgsl_default()), + "spvasm" => params.targets = Some(Targets::non_wgsl_default()), + "vert" | "frag" | "comp" => params.targets = Some(Targets::non_wgsl_default()), + e => { + panic!("Unknown extension: {e}"); + } + } + } + + params + } + + /// Write `data` to a file corresponding to this input file in + /// `subdirectory`, with `extension`. + pub fn write_output_file( + &self, + subdirectory: &str, + extension: &str, + data: impl AsRef<[u8]>, + dir_out: &str, + ) { + let output_path = self.output_path(subdirectory, extension, dir_out); + fs::create_dir_all(output_path.parent().unwrap()).unwrap(); + if let Err(err) = fs::write(&output_path, data) { + panic!("Error writing {}: {}", output_path.display(), err); + } + } +} From 2f2cf3747ac1f56417fcf07a4f724a05f25d8b48 Mon Sep 17 00:00:00 2001 From: SupaMaggie70Incorporated Date: Fri, 15 Aug 2025 13:25:44 -0500 Subject: [PATCH 2/2] Added set_options to WGSL front and SPV back --- naga/src/back/spv/writer.rs | 15 +++++++++++++++ naga/src/front/wgsl/mod.rs | 3 +++ 2 files changed, 18 insertions(+) diff --git a/naga/src/back/spv/writer.rs b/naga/src/back/spv/writer.rs index 0688eb6c975..3f7d5d25fc8 100644 --- a/naga/src/back/spv/writer.rs +++ b/naga/src/back/spv/writer.rs @@ -95,6 +95,21 @@ impl Writer { }) } + pub fn set_options(&mut self, options: &Options) -> Result<(), Error> { + let (major, minor) = options.lang_version; + if major != 1 { + return Err(Error::UnsupportedVersion(major, minor)); + } + self.physical_layout = PhysicalLayout::new(major, minor); + self.capabilities_available = options.capabilities.clone(); + self.flags = options.flags; + self.bounds_check_policies = options.bounds_check_policies; + self.zero_initialize_workgroup_memory = options.zero_initialize_workgroup_memory; + self.force_loop_bounding = options.force_loop_bounding; + self.binding_map = options.binding_map.clone(); + Ok(()) + } + /// Returns `(major, minor)` of the SPIR-V language version. pub const fn lang_version(&self) -> (u8, u8) { self.physical_layout.lang_version() diff --git a/naga/src/front/wgsl/mod.rs b/naga/src/front/wgsl/mod.rs index 1080392cc61..dfacc7d975a 100644 --- a/naga/src/front/wgsl/mod.rs +++ b/naga/src/front/wgsl/mod.rs @@ -48,6 +48,9 @@ impl Frontend { options, } } + pub fn set_options(&mut self, options: Options) { + self.options = options; + } pub fn parse(&mut self, source: &str) -> core::result::Result { self.inner(source).map_err(|x| x.as_parse_error(source))