Move the convert from an example to a binary target

This commit is contained in:
Dzmitry Malyshau
2021-02-24 10:49:27 -05:00
committed by Dzmitry Malyshau
parent cb45cd9011
commit afbcf05006
7 changed files with 265 additions and 317 deletions

View File

@@ -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"

View File

@@ -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`,

233
bin/convert.rs Normal file
View File

@@ -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<String>,
#[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<T, E: Error> PrettyResult for Result<T, E> {
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 <input> <output> [<options>]");
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, &params.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, &params.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, &params.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
);
}
}
}

View File

@@ -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<u8>,
#[serde(default)]
#[cfg_attr(not(feature = "msl-out"), allow(dead_code))]
texture: Option<u8>,
#[serde(default)]
#[cfg_attr(not(feature = "msl-out"), allow(dead_code))]
sampler: Option<u8>,
#[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<spirv::Capability>,
#[cfg_attr(not(feature = "msl-out"), allow(dead_code))]
mtl_bindings: naga::FastHashMap<BindSource, BindTarget>,
}
trait PrettyResult {
type Target;
fn unwrap_pretty(self) -> Self::Target;
}
impl<T, E: Error> PrettyResult for Result<T, E> {
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::<Vec<_>>();
if args.len() <= 1 {
println!("Call with <input> <output>");
return;
}
let param_path = std::path::PathBuf::from(&args[1]).with_extension("param.ron");
let params = match fs::read_to_string(&param_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
);
}
}
}

View File

@@ -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

View File

@@ -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,
}
}
}

View File

@@ -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,
}
}
}