diff --git a/Cargo.toml b/Cargo.toml index 880f246f2a..6c85ab552d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,9 @@ spv-in = ["petgraph", "spirv"] spv-out = ["spirv"] wgsl-in = [] -#Note: it would be nice to do `cargo run` for conversion. -# Blocked on https://github.com/rust-lang/cargo/issues/4663 +[[bin]] +name = "convert" +path = "bin/convert.rs" [dev-dependencies] difference = "2.0" @@ -43,6 +44,5 @@ env_logger = "0.8" ron = "0.6" serde = { version = "1.0", features = ["derive"] } spirv = { package = "spirv_headers", version = "1.5", features = ["deserialize"] } -# `similar` dependency, introduced in 1.5.3, breaks MSRV -insta = { version = "=1.5.2", features = ["glob"] } +insta = { version = "1.6", features = ["glob"] } rspirv = "0.7" diff --git a/README.md b/README.md index bd7e5d44f2..5e04f23720 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,15 @@ DXBC | | | :white_check_mark: = Primary support — :ok: = Secondary support — :construction: = Unsupported, but support in progress +## Conversion tool + +Naga includes a default binary target "convert", which allows to test the conversion of different code paths. +```bash +cargo run --features spv-in -- my_shader.spv # dump the IR module to debug output +cargo run --features spv-in,msl-out -- my_shader.spv my_shader.metal --flow-dir flow-dir # convert the SPV to Metal, also dump the SPIR-V flow graph to `flow-dir` +cargo run --features wgsl-in,glsl-out -- my_shader.wgsl my_shader.vert --profile es310 # convert the WGSL to GLSL vertex stage under ES 3.20 profile +``` + ## Development workflow The main instrument aiding the development is the good old `cargo test --all-features`, diff --git a/bin/convert.rs b/bin/convert.rs new file mode 100644 index 0000000000..f3cf33e508 --- /dev/null +++ b/bin/convert.rs @@ -0,0 +1,233 @@ +#![allow(clippy::manual_strip)] +#[allow(unused_imports)] +use std::fs; +use std::{env, error::Error, path::Path}; + +#[derive(Default)] +struct Parameters { + spv_flow_dump_prefix: Option, + #[cfg(feature = "spv-out")] + spv: naga::back::spv::Options, + #[cfg(feature = "msl-out")] + msl: naga::back::msl::Options, + #[cfg(feature = "glsl-out")] + glsl: naga::back::glsl::Options, +} + +trait PrettyResult { + type Target; + fn unwrap_pretty(self) -> Self::Target; +} + +impl PrettyResult for Result { + type Target = T; + fn unwrap_pretty(self) -> T { + match self { + Result::Ok(value) => value, + Result::Err(error) => { + println!("{}:", error); + let mut e = error.source(); + while let Some(source) = e { + println!("\t{}", source); + e = source.source(); + } + std::process::exit(1); + } + } + } +} + +fn main() { + let mut input_path = None; + let mut output_path = None; + //TODO: read the parameters from RON? + let mut params = Parameters::default(); + + let mut args = env::args(); + let _ = args.next().unwrap(); + while let Some(arg) = args.next() { + //TODO: use `strip_prefix` when MSRV reaches 1.45.0 + if arg.starts_with("--") { + match &arg[2..] { + "flow-dir" => params.spv_flow_dump_prefix = args.next(), + #[cfg(feature = "glsl-out")] + "entry-point" => params.glsl.entry_point = args.next().unwrap(), + #[cfg(feature = "glsl-out")] + "profile" => { + use naga::back::glsl::Version; + let string = args.next().unwrap(); + //TODO: use `strip_prefix` in 1.45.0 + params.glsl.version = if string.starts_with("core") { + Version::Desktop(string[4..].parse().unwrap_or(330)) + } else if string.starts_with("es") { + Version::Embedded(string[2..].parse().unwrap_or(310)) + } else { + panic!("Unknown profile: {}", string) + }; + } + other => log::warn!("Unknown parameter: {}", other), + } + } else if input_path.is_none() { + input_path = Some(arg); + } else if output_path.is_none() { + output_path = Some(arg); + } else { + log::warn!("Extra parameter: {}", arg); + } + } + + let input_path = match input_path { + Some(ref string) => string, + None => { + println!("Call with []"); + return; + } + }; + let module = match Path::new(input_path) + .extension() + .expect("Input has no extension?") + .to_str() + .unwrap() + { + #[cfg(feature = "spv-in")] + "spv" => { + let options = naga::front::spv::Options { + flow_graph_dump_prefix: params.spv_flow_dump_prefix.map(std::path::PathBuf::from), + }; + let input = fs::read(input_path).unwrap(); + naga::front::spv::parse_u8_slice(&input, &options).unwrap() + } + #[cfg(feature = "wgsl-in")] + "wgsl" => { + let input = fs::read_to_string(input_path).unwrap(); + naga::front::wgsl::parse_str(&input).unwrap_pretty() + } + #[cfg(feature = "glsl-in")] + "vert" => { + let input = fs::read_to_string(input_path).unwrap(); + let mut entry_points = naga::FastHashMap::default(); + entry_points.insert("main".to_string(), naga::ShaderStage::Vertex); + naga::front::glsl::parse_str( + &input, + &naga::front::glsl::Options { + entry_points, + defines: Default::default(), + }, + ) + .unwrap_pretty() + } + #[cfg(feature = "glsl-in")] + "frag" => { + let input = fs::read_to_string(input_path).unwrap(); + let mut entry_points = naga::FastHashMap::default(); + entry_points.insert("main".to_string(), naga::ShaderStage::Fragment); + naga::front::glsl::parse_str( + &input, + &naga::front::glsl::Options { + entry_points, + defines: Default::default(), + }, + ) + .unwrap_pretty() + } + #[cfg(feature = "glsl-in")] + "comp" => { + let input = fs::read_to_string(input_path).unwrap(); + let mut entry_points = naga::FastHashMap::default(); + entry_points.insert("main".to_string(), naga::ShaderStage::Compute); + naga::front::glsl::parse_str( + &input, + &naga::front::glsl::Options { + entry_points, + defines: Default::default(), + }, + ) + .unwrap_pretty() + } + other => { + if true { + // prevent "unreachable_code" warnings + panic!("Unknown input extension: {}", other); + } + naga::Module::default() + } + }; + + let output_path = match output_path { + Some(ref string) => string, + None => { + println!("{:#?}", module); + return; + } + }; + + // validate the IR + #[allow(unused_variables)] + let analysis = naga::proc::Validator::new() + .validate(&module) + .unwrap_pretty(); + + match Path::new(output_path) + .extension() + .expect("Output has no extension?") + .to_str() + .unwrap() + { + #[cfg(feature = "msl-out")] + "metal" => { + use naga::back::msl; + let (msl, _) = msl::write_string(&module, &analysis, ¶ms.msl).unwrap_pretty(); + fs::write(output_path, msl).unwrap(); + } + #[cfg(feature = "spv-out")] + "spv" => { + use naga::back::spv; + + let spv = spv::write_vec(&module, &analysis, ¶ms.spv).unwrap_pretty(); + let bytes = spv + .iter() + .fold(Vec::with_capacity(spv.len() * 4), |mut v, w| { + v.extend_from_slice(&w.to_le_bytes()); + v + }); + + fs::write(output_path, bytes.as_slice()).unwrap(); + } + #[cfg(feature = "glsl-out")] + stage @ "vert" | stage @ "frag" | stage @ "comp" => { + use naga::back::glsl; + + params.glsl.shader_stage = match stage { + "vert" => naga::ShaderStage::Vertex, + "frag" => naga::ShaderStage::Fragment, + "comp" => naga::ShaderStage::Compute, + _ => unreachable!(), + }; + + let file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(output_path) + .unwrap(); + + let mut writer = + glsl::Writer::new(file, &module, &analysis, ¶ms.glsl).unwrap_pretty(); + + writer + .write() + .map_err(|e| { + fs::remove_file(output_path).unwrap(); + e + }) + .unwrap(); + } + other => { + let _ = params; + panic!( + "Unknown output extension: {}, forgot to enable a feature?", + other + ); + } + } +} diff --git a/examples/convert.rs b/examples/convert.rs deleted file mode 100644 index e148049019..0000000000 --- a/examples/convert.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::{env, error::Error, fs, path::Path}; - -#[derive(Hash, PartialEq, Eq, serde::Deserialize)] -enum Stage { - Vertex, - Fragment, - Compute, -} - -#[derive(Hash, PartialEq, Eq, serde::Deserialize)] -struct BindSource { - stage: Stage, - group: u32, - binding: u32, -} - -#[derive(serde::Deserialize)] -struct BindTarget { - #[serde(default)] - #[cfg_attr(not(feature = "msl-out"), allow(dead_code))] - buffer: Option, - #[serde(default)] - #[cfg_attr(not(feature = "msl-out"), allow(dead_code))] - texture: Option, - #[serde(default)] - #[cfg_attr(not(feature = "msl-out"), allow(dead_code))] - sampler: Option, - #[serde(default)] - #[cfg_attr(not(feature = "msl-out"), allow(dead_code))] - mutable: bool, -} - -#[derive(Default, serde::Deserialize)] -struct Parameters { - #[serde(default)] - #[cfg_attr(not(feature = "spv-in"), allow(dead_code))] - spv_flow_dump_prefix: String, - #[cfg_attr(not(feature = "spv-out"), allow(dead_code))] - spv_version: (u8, u8), - #[cfg_attr(not(feature = "spv-out"), allow(dead_code))] - spv_capabilities: naga::FastHashSet, - #[cfg_attr(not(feature = "msl-out"), allow(dead_code))] - mtl_bindings: naga::FastHashMap, -} - -trait PrettyResult { - type Target; - fn unwrap_pretty(self) -> Self::Target; -} - -impl PrettyResult for Result { - type Target = T; - fn unwrap_pretty(self) -> T { - match self { - Result::Ok(value) => value, - Result::Err(error) => { - println!("{}:", error); - let mut e = error.source(); - while let Some(source) = e { - println!("\t{}", source); - e = source.source(); - } - std::process::exit(1); - } - } - } -} - -fn main() { - env_logger::init(); - - let args = env::args().collect::>(); - if args.len() <= 1 { - println!("Call with "); - return; - } - - let param_path = std::path::PathBuf::from(&args[1]).with_extension("param.ron"); - let params = match fs::read_to_string(¶m_path) { - Ok(string) => ron::de::from_str(&string).unwrap_pretty(), - Err(_) => { - log::warn!("Param file {:?} not found, using defaults", param_path); - let mut param = Parameters::default(); - // very useful to have this by default - param.spv_capabilities.insert(spirv::Capability::Shader); - param.spv_version = (1, 0); - param - } - }; - - let module = match Path::new(&args[1]) - .extension() - .expect("Input has no extension?") - .to_str() - .unwrap() - { - #[cfg(feature = "spv-in")] - "spv" => { - let options = naga::front::spv::Options { - flow_graph_dump_prefix: if params.spv_flow_dump_prefix.is_empty() { - None - } else { - Some(params.spv_flow_dump_prefix.into()) - }, - }; - let input = fs::read(&args[1]).unwrap(); - naga::front::spv::parse_u8_slice(&input, &options).unwrap() - } - #[cfg(feature = "wgsl-in")] - "wgsl" => { - let input = fs::read_to_string(&args[1]).unwrap(); - naga::front::wgsl::parse_str(&input).unwrap_pretty() - } - #[cfg(feature = "glsl-in")] - "vert" => { - let input = fs::read_to_string(&args[1]).unwrap(); - let mut entry_points = naga::FastHashMap::default(); - entry_points.insert("main".to_string(), naga::ShaderStage::Vertex); - naga::front::glsl::parse_str( - &input, - &naga::front::glsl::Options { - entry_points, - defines: Default::default(), - }, - ) - .unwrap_pretty() - } - #[cfg(feature = "glsl-in")] - "frag" => { - let input = fs::read_to_string(&args[1]).unwrap(); - let mut entry_points = naga::FastHashMap::default(); - entry_points.insert("main".to_string(), naga::ShaderStage::Fragment); - naga::front::glsl::parse_str( - &input, - &naga::front::glsl::Options { - entry_points, - defines: Default::default(), - }, - ) - .unwrap_pretty() - } - #[cfg(feature = "glsl-in")] - "comp" => { - let input = fs::read_to_string(&args[1]).unwrap(); - let mut entry_points = naga::FastHashMap::default(); - entry_points.insert("main".to_string(), naga::ShaderStage::Compute); - naga::front::glsl::parse_str( - &input, - &naga::front::glsl::Options { - entry_points, - defines: Default::default(), - }, - ) - .unwrap_pretty() - } - #[cfg(feature = "deserialize")] - "ron" => { - let mut input = fs::File::open(&args[1]).unwrap(); - ron::de::from_reader(&mut input).unwrap_pretty() - } - other => { - if true { - // prevent "unreachable_code" warnings - panic!("Unknown input extension: {}", other); - } - naga::Module::default() - } - }; - - if args.len() <= 2 { - println!("{:#?}", module); - return; - } - - // validate the IR - #[allow(unused_variables)] - let analysis = naga::proc::Validator::new() - .validate(&module) - .unwrap_pretty(); - - match Path::new(&args[2]) - .extension() - .expect("Output has no extension?") - .to_str() - .unwrap() - { - #[cfg(feature = "msl-out")] - "metal" => { - use naga::back::msl; - let mut options = msl::Options { - lang_version: (1, 0), - binding_map: msl::BindingMap::default(), - spirv_cross_compatibility: false, - fake_missing_bindings: false, - }; - if params.mtl_bindings.is_empty() { - log::warn!("Metal binding map is missing"); - options.fake_missing_bindings = true; - } else { - for (key, value) in params.mtl_bindings { - options.binding_map.insert( - msl::BindSource { - stage: match key.stage { - Stage::Vertex => naga::ShaderStage::Vertex, - Stage::Fragment => naga::ShaderStage::Fragment, - Stage::Compute => naga::ShaderStage::Compute, - }, - group: key.group, - binding: key.binding, - }, - msl::BindTarget { - buffer: value.buffer, - texture: value.texture, - sampler: value.sampler, - mutable: value.mutable, - }, - ); - } - } - let (msl, _) = msl::write_string(&module, &analysis, &options).unwrap_pretty(); - fs::write(&args[2], msl).unwrap(); - } - #[cfg(feature = "spv-out")] - "spv" => { - use naga::back::spv; - - let options = spv::Options { - lang_version: params.spv_version, - flags: args.get(3).map_or(spv::WriterFlags::DEBUG, |arg| { - if arg.parse().unwrap() { - spv::WriterFlags::DEBUG - } else { - spv::WriterFlags::empty() - } - }), - capabilities: params.spv_capabilities, - }; - - let spv = spv::write_vec(&module, &analysis, &options).unwrap_pretty(); - - let bytes = spv - .iter() - .fold(Vec::with_capacity(spv.len() * 4), |mut v, w| { - v.extend_from_slice(&w.to_le_bytes()); - v - }); - - fs::write(&args[2], bytes.as_slice()).unwrap(); - } - #[cfg(feature = "glsl-out")] - stage @ "vert" | stage @ "frag" | stage @ "comp" => { - use naga::back::glsl; - - let version = { - let arg = args.get(3).map_or("es", |p| p.as_str()); - if arg.starts_with("core") { - glsl::Version::Desktop(arg[4..].parse().unwrap_or(330)) - } else if arg.starts_with("es") { - glsl::Version::Embedded(arg[2..].parse().unwrap_or(310)) - } else { - panic!("Unknown profile: {}", arg) - } - }; - let name = args.get(4).map_or("main", |p| p.as_str()).to_string(); - let options = glsl::Options { - version, - shader_stage: match stage { - "vert" => naga::ShaderStage::Vertex, - "frag" => naga::ShaderStage::Fragment, - "comp" => naga::ShaderStage::Compute, - _ => unreachable!(), - }, - entry_point: name, - }; - - let file = fs::OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(&args[2]) - .unwrap(); - - let mut writer = glsl::Writer::new(file, &module, &analysis, &options).unwrap_pretty(); - - writer - .write() - .map_err(|e| { - fs::remove_file(&args[2]).unwrap(); - e - }) - .unwrap(); - } - #[cfg(feature = "serialize")] - "ron" => { - let config = ron::ser::PrettyConfig::new() - .with_enumerate_arrays(true) - .with_decimal_floats(true); - - let output = ron::ser::to_string_pretty(&module, config).unwrap_pretty(); - fs::write(&args[2], output).unwrap(); - } - other => { - let _ = params; - panic!( - "Unknown output extension: {}, forgot to enable a feature?", - other - ); - } - } -} diff --git a/src/back/glsl/mod.rs b/src/back/glsl/mod.rs index 38a416ac6e..e1b03ab2de 100644 --- a/src/back/glsl/mod.rs +++ b/src/back/glsl/mod.rs @@ -135,6 +135,16 @@ pub struct Options { pub entry_point: String, } +impl Default for Options { + fn default() -> Self { + Options { + version: Version::Embedded(320), + shader_stage: ShaderStage::Compute, + entry_point: "main".to_string(), + } + } +} + /// Structure that connects a texture to a sampler or not /// /// glsl pre vulkan has no concept of separate textures and samplers instead everything is a diff --git a/src/back/msl/mod.rs b/src/back/msl/mod.rs index ecf9eee7d7..353b1e1481 100644 --- a/src/back/msl/mod.rs +++ b/src/back/msl/mod.rs @@ -106,7 +106,7 @@ impl Default for Options { lang_version: (1, 0), binding_map: BindingMap::default(), spirv_cross_compatibility: false, - fake_missing_bindings: false, + fake_missing_bindings: true, } } } diff --git a/src/back/spv/mod.rs b/src/back/spv/mod.rs index 346aca071a..4a780f2efd 100644 --- a/src/back/spv/mod.rs +++ b/src/back/spv/mod.rs @@ -57,10 +57,16 @@ pub struct Options { impl Default for Options { fn default() -> Self { + let mut flags = WriterFlags::empty(); + if cfg!(debug_assertions) { + flags |= WriterFlags::DEBUG; + } + let mut capabilities = crate::FastHashSet::default(); + capabilities.insert(Capability::Shader); Options { lang_version: (1, 0), - flags: WriterFlags::empty(), - capabilities: Default::default(), + flags, + capabilities, } } }