#![allow(clippy::result_large_err)]
extern crate wgpu_types as wgpu;
use std::{
collections::BTreeMap,
io::Write,
path::Path,
process::{Command, Stdio},
};
use bindgroup::{bind_groups_module, get_bind_group_data};
use consts::pipeline_overridable_constants;
use entry::{entry_point_constants, fragment_states, vertex_states, vertex_struct_methods};
use naga::{valid::ValidationFlags, WithSpan};
use proc_macro2::{Literal, Span, TokenStream};
use quote::quote;
use syn::Ident;
use thiserror::Error;
mod bindgroup;
mod consts;
mod entry;
mod structs;
mod wgsl;
pub use naga::valid::Capabilities as WgslCapabilities;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CreateModuleError {
#[error("bind groups are non-consecutive or do not start from 0")]
NonConsecutiveBindGroups,
#[error("duplicate binding found with index `{binding}`")]
DuplicateBinding { binding: u32 },
#[error("failed to parse: {error}")]
ParseError {
error: naga::front::wgsl::ParseError,
},
#[error("failed to validate: {error}")]
ValidationError {
error: WithSpan<naga::valid::ValidationError>,
},
}
impl CreateModuleError {
pub fn emit_to_stderr(&self, wgsl_source: &str) {
match self {
CreateModuleError::ParseError { error } => error.emit_to_stderr(wgsl_source),
CreateModuleError::ValidationError { error } => error.emit_to_stderr(wgsl_source),
other => {
eprintln!("{other}")
}
}
}
pub fn emit_to_stderr_with_path(&self, wgsl_source: &str, path: impl AsRef<Path>) {
let path = path.as_ref();
match self {
CreateModuleError::ParseError { error } => {
error.emit_to_stderr_with_path(wgsl_source, path)
}
CreateModuleError::ValidationError { error } => {
error.emit_to_stderr_with_path(wgsl_source, &path.to_string_lossy())
}
other => {
eprintln!("{}: {}", path.to_string_lossy(), other)
}
}
}
pub fn emit_to_string(&self, wgsl_source: &str) -> String {
match self {
CreateModuleError::ParseError { error } => error.emit_to_string(wgsl_source),
CreateModuleError::ValidationError { error } => error.emit_to_string(wgsl_source),
other => {
format!("{other}")
}
}
}
pub fn emit_to_string_with_path(&self, wgsl_source: &str, path: impl AsRef<Path>) -> String {
let path = path.as_ref();
match self {
CreateModuleError::ParseError { error } => {
error.emit_to_string_with_path(wgsl_source, path)
}
CreateModuleError::ValidationError { error } => {
error.emit_to_string_with_path(wgsl_source, &path.to_string_lossy())
}
other => {
format!("{}: {}", path.to_string_lossy(), other)
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub struct WriteOptions {
pub derive_bytemuck_vertex: bool,
pub derive_bytemuck_host_shareable: bool,
pub derive_encase_host_shareable: bool,
pub derive_serde: bool,
pub matrix_vector_types: MatrixVectorTypes,
pub rustfmt: bool,
pub validate: Option<ValidationOptions>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ValidationOptions {
pub capabilities: WgslCapabilities,
}
impl Default for ValidationOptions {
fn default() -> Self {
Self {
capabilities: WgslCapabilities::all(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatrixVectorTypes {
Rust,
Glam,
Nalgebra,
}
impl Default for MatrixVectorTypes {
fn default() -> Self {
Self::Rust
}
}
pub fn create_shader_module(
wgsl_source: &str,
wgsl_include_path: &str,
options: WriteOptions,
) -> Result<String, CreateModuleError> {
let mut root = Module::default();
root.add_shader_module(
wgsl_source,
Some(wgsl_include_path),
options,
ModulePath::default(),
demangle_identity,
)?;
Ok(root.to_generated_bindings(options))
}
pub fn create_shader_modules<F>(
wgsl_source: &str,
options: WriteOptions,
demangle: F,
) -> Result<String, CreateModuleError>
where
F: Fn(&str) -> TypePath + Clone,
{
let mut root = Module::default();
root.add_shader_module(wgsl_source, None, options, ModulePath::default(), demangle)?;
let output = root.to_generated_bindings(options);
Ok(output)
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct ModulePath {
pub components: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TypePath {
pub parent: ModulePath,
pub name: String,
}
pub fn demangle_identity(name: &str) -> TypePath {
TypePath {
parent: ModulePath::default(),
name: name.to_string(),
}
}
#[derive(Debug, Default)]
pub struct Module {
items: BTreeMap<String, TokenStream>,
submodules: BTreeMap<String, Module>,
}
impl Module {
fn to_tokens(&self) -> TokenStream {
let mut tokens = quote!();
for item in self.items.values() {
tokens = quote!(#tokens #item);
}
for (name, m) in &self.submodules {
let submodule = m.to_tokens();
let name = Ident::new(name, Span::call_site());
tokens = quote! {
#tokens
pub mod #name {
#submodule
}
}
}
tokens
}
pub fn to_generated_bindings(&self, options: WriteOptions) -> String {
let output = self.to_tokens();
if options.rustfmt {
pretty_print_rustfmt(output)
} else {
pretty_print(output)
}
}
fn add_module_items(&mut self, structs: &[(TypePath, TokenStream)], root_path: &ModulePath) {
for (item, tokens) in structs {
let components = if item.parent.components.is_empty() {
&root_path.components
} else {
&item.parent.components
};
let module = self.get_module(components);
module.items.insert(item.name.clone(), tokens.clone());
}
}
fn get_module<'a>(&'a mut self, parents: &[String]) -> &'a mut Module {
if let Some((name, remaining)) = parents.split_first() {
self.submodules
.entry(name.clone())
.or_default()
.get_module(remaining)
} else {
self
}
}
pub fn add_shader_module<F>(
&mut self,
wgsl_source: &str,
wgsl_include_path: Option<&str>,
options: WriteOptions,
root_path: ModulePath,
demangle: F,
) -> Result<(), CreateModuleError>
where
F: Fn(&str) -> TypePath + Clone,
{
let module = naga::front::wgsl::parse_str(wgsl_source)
.map_err(|error| CreateModuleError::ParseError { error })?;
if let Some(options) = options.validate.as_ref() {
naga::valid::Validator::new(ValidationFlags::all(), options.capabilities)
.validate(&module)
.map_err(|error| CreateModuleError::ValidationError { error })?;
}
let global_stages = wgsl::global_shader_stages(&module);
let bind_group_data = get_bind_group_data(&module, &global_stages, demangle.clone())?;
let entry_stages = wgsl::entry_stages(&module);
let structs = structs::structs(&module, options, demangle.clone());
let consts = consts::consts(&module, demangle.clone());
let bind_groups_module = bind_groups_module(&module, &bind_group_data);
let vertex_methods = vertex_struct_methods(&module, demangle.clone());
let compute_module = compute_module(&module, demangle.clone());
let entry_point_constants = entry_point_constants(&module, demangle.clone());
let vertex_states = vertex_states(&module, demangle.clone());
let fragment_states = fragment_states(&module, demangle.clone());
let included_source = wgsl_include_path
.map(|p| quote!(include_str!(#p)))
.unwrap_or_else(|| quote!(#wgsl_source));
let create_shader_module = quote! {
pub const SOURCE: &str = #included_source;
pub fn create_shader_module(device: &wgpu::Device) -> wgpu::ShaderModule {
let source = std::borrow::Cow::Borrowed(SOURCE);
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(source)
})
}
};
let bind_group_layouts: Vec<_> = bind_group_data
.keys()
.map(|group_no| {
let group = indexed_name_to_ident("BindGroup", *group_no);
quote!(bind_groups::#group::get_bind_group_layout(device))
})
.collect();
let (push_constant_range, push_constant_stages) =
push_constant_range_stages(&module, &global_stages, entry_stages).unzip();
let create_pipeline_layout = quote! {
pub fn create_pipeline_layout(device: &wgpu::Device) -> wgpu::PipelineLayout {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[
#(&#bind_group_layouts),*
],
push_constant_ranges: &[#push_constant_range],
})
}
};
let override_constants = pipeline_overridable_constants(&module, demangle);
let push_constant_stages = push_constant_stages.map(|stages| {
quote! {
pub const PUSH_CONSTANT_STAGES: wgpu::ShaderStages = #stages;
}
});
self.add_module_items(&consts, &root_path);
self.add_module_items(&structs, &root_path);
self.add_module_items(&vertex_methods, &root_path);
let root_items = vec![(
TypePath {
parent: root_path.clone(),
name: String::new(),
},
quote! {
#override_constants
#bind_groups_module
#compute_module
#entry_point_constants
#vertex_states
#fragment_states
#create_shader_module
#push_constant_stages
#create_pipeline_layout
},
)];
self.add_module_items(&root_items, &root_path);
Ok(())
}
}
fn push_constant_range_stages(
module: &naga::Module,
global_stages: &BTreeMap<String, wgpu::ShaderStages>,
entry_stages: wgpu::ShaderStages,
) -> Option<(TokenStream, TokenStream)> {
let (_, global) = module
.global_variables
.iter()
.find(|(_, g)| g.space == naga::AddressSpace::PushConstant)?;
let push_constant_size = module.types[global.ty].inner.size(module.to_ctx());
let shader_stages = global
.name
.as_ref()
.and_then(|n| global_stages.get(n).copied())
.unwrap_or(entry_stages);
let stages = quote_shader_stages(shader_stages);
let size = Literal::usize_unsuffixed(push_constant_size as usize);
Some((
quote! {
wgpu::PushConstantRange {
stages: PUSH_CONSTANT_STAGES,
range: 0..#size
}
},
stages,
))
}
fn pretty_print(output: TokenStream) -> String {
let file = syn::parse_file(&output.to_string()).unwrap();
prettyplease::unparse(&file)
}
fn pretty_print_rustfmt(tokens: TokenStream) -> String {
let value = tokens.to_string();
if let Ok(mut proc) = Command::new("rustfmt")
.arg("--emit=stdout")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
let stdin = proc.stdin.as_mut().unwrap();
stdin.write_all(value.as_bytes()).unwrap();
let output = proc.wait_with_output().unwrap();
if output.status.success() {
return String::from_utf8(output.stdout).unwrap().replace("\r", "");
}
}
value.to_string()
}
fn indexed_name_to_ident(name: &str, index: u32) -> Ident {
Ident::new(&format!("{name}{index}"), Span::call_site())
}
fn compute_module<F>(module: &naga::Module, demangle: F) -> TokenStream
where
F: Fn(&str) -> TypePath + Clone,
{
let entry_points: Vec<_> = module
.entry_points
.iter()
.filter_map(|e| {
if e.stage == naga::ShaderStage::Compute {
let workgroup_size_constant = workgroup_size(e, demangle.clone());
let create_pipeline = create_compute_pipeline(e, demangle.clone());
Some(quote! {
#workgroup_size_constant
#create_pipeline
})
} else {
None
}
})
.collect();
if entry_points.is_empty() {
quote!()
} else {
quote! {
pub mod compute {
#(#entry_points)*
}
}
}
}
fn create_compute_pipeline<F>(e: &naga::EntryPoint, demangle: F) -> TokenStream
where
F: Fn(&str) -> TypePath,
{
let name = &demangle(&e.name).name;
let pipeline_name = Ident::new(&format!("create_{}_pipeline", name), Span::call_site());
let entry_point = &e.name;
let label = format!("Compute Pipeline {}", name);
quote! {
pub fn #pipeline_name(device: &wgpu::Device) -> wgpu::ComputePipeline {
let module = super::create_shader_module(device);
let layout = super::create_pipeline_layout(device);
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(#label),
layout: Some(&layout),
module: &module,
entry_point: Some(#entry_point),
compilation_options: Default::default(),
cache: Default::default(),
})
}
}
}
fn workgroup_size<F>(e: &naga::EntryPoint, demangle: F) -> TokenStream
where
F: Fn(&str) -> TypePath + Clone,
{
let name = &demangle(&e.name).name;
let name = Ident::new(
&format!("{}_WORKGROUP_SIZE", name.to_uppercase()),
Span::call_site(),
);
let [x, y, z] = e
.workgroup_size
.map(|s| Literal::usize_unsuffixed(s as usize));
quote!(pub const #name: [u32; 3] = [#x, #y, #z];)
}
fn quote_shader_stages(stages: wgpu::ShaderStages) -> TokenStream {
if stages == wgpu::ShaderStages::all() {
quote!(wgpu::ShaderStages::all())
} else if stages == wgpu::ShaderStages::VERTEX_FRAGMENT {
quote!(wgpu::ShaderStages::VERTEX_FRAGMENT)
} else {
let mut components = Vec::new();
if stages.contains(wgpu::ShaderStages::VERTEX) {
components.push(quote!(wgpu::ShaderStages::VERTEX));
}
if stages.contains(wgpu::ShaderStages::FRAGMENT) {
components.push(quote!(wgpu::ShaderStages::FRAGMENT));
}
if stages.contains(wgpu::ShaderStages::COMPUTE) {
components.push(quote!(wgpu::ShaderStages::COMPUTE));
}
if let Some((first, remaining)) = components.split_first() {
quote!(#first #(.union(#remaining))*)
} else {
quote!(wgpu::ShaderStages::NONE)
}
}
}
#[cfg(test)]
#[macro_export]
macro_rules! assert_tokens_eq {
($a:expr, $b:expr) => {
pretty_assertions::assert_eq!(
crate::pretty_print_rustfmt($a),
crate::pretty_print_rustfmt($b)
)
};
}
#[cfg(test)]
#[macro_export]
macro_rules! assert_tokens_snapshot {
($output:expr) => {
let mut settings = insta::Settings::new();
settings.set_prepend_module_to_snapshot(false);
settings.set_omit_expression(true);
settings.bind(|| {
insta::assert_snapshot!(crate::pretty_print_rustfmt($output));
});
};
}
#[cfg(test)]
#[macro_export]
macro_rules! assert_rust_snapshot {
($output:expr) => {
let mut settings = insta::Settings::new();
settings.set_prepend_module_to_snapshot(false);
settings.set_omit_expression(true);
settings.bind(|| {
insta::assert_snapshot!($output);
});
};
}
#[cfg(test)]
mod test {
use super::*;
use indoc::indoc;
#[test]
fn create_shader_module_push_constants() {
let source = indoc! {r#"
var<push_constant> consts: vec4<f32>;
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return consts;
}
"#};
let actual = create_shader_module(source, "shader.wgsl", WriteOptions::default()).unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn create_shader_multiple_entries() {
let source = indoc! {r#"
@group(0) @binding(0) var<uniform> a: f32;
@group(0) @binding(1) var<uniform> b: f32;
@group(0) @binding(2) var<uniform> c: f32;
@group(0) @binding(3) var<uniform> d: u32;
@group(0) @binding(4) var<uniform> e: u32;
@group(0) @binding(5) var<uniform> f: u32;
@group(0) @binding(6) var<uniform> g: u32;
@group(0) @binding(7) var<uniform> h: u32;
fn inner() -> f32 {
return d;
}
@vertex
fn vs_main() {
{
let x = b;
let y = f;
}
let x = h;
switch e {
default: {
let y = e;
return;
}
}
}
@fragment
fn fs_main() {
let z = e;
loop {
let z = c;
}
if true {
let x = h;
let y = g;
}
}
@compute @workgroup_size(1, 1, 1)
fn main() {
let y = inner();
let z = f;
loop {
let w = g;
let x = h;
}
}
"#};
let actual = create_shader_module(source, "shader.wgsl", WriteOptions::default()).unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn create_shader_module_multiple_outputs() {
let source = indoc! {r#"
struct Output {
@location(0) col0: vec4<f32>,
@builtin(frag_depth) depth: f32,
@location(1) col1: vec4<f32>,
};
@fragment
fn fs_multiple() -> Output {}
"#};
let actual = create_shader_module(source, "shader.wgsl", WriteOptions::default()).unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn create_shader_modules_source() {
let source = "@fragment fn main() {}";
let actual =
create_shader_modules(source, WriteOptions::default(), demangle_identity).unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn create_shader_modules_source_rustfmt() {
let source = "@fragment fn main() {}";
let actual = create_shader_modules(
source,
WriteOptions {
rustfmt: true,
..Default::default()
},
demangle_identity,
)
.unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn create_shader_module_consecutive_bind_groups() {
let source = indoc! {r#"
struct A {
f: vec4<f32>
};
@group(0) @binding(0) var<uniform> a: A;
@group(1) @binding(0) var<uniform> b: f32;
@group(2) @binding(0) var<uniform> c: vec4<f32>;
@group(3) @binding(0) var<uniform> d: mat4x4<f32>;
@vertex
fn vs_main() {}
@fragment
fn fs_main() {}
"#};
create_shader_module(source, "shader.wgsl", WriteOptions::default()).unwrap();
}
#[test]
fn create_shader_module_non_consecutive_bind_groups() {
let source = indoc! {r#"
@group(0) @binding(0) var<uniform> a: vec4<f32>;
@group(1) @binding(0) var<uniform> b: vec4<f32>;
@group(3) @binding(0) var<uniform> c: vec4<f32>;
@fragment
fn main() {}
"#};
let result = create_shader_module(source, "shader.wgsl", WriteOptions::default());
assert!(matches!(
result,
Err(CreateModuleError::NonConsecutiveBindGroups)
));
}
#[test]
fn create_shader_module_repeated_bindings() {
let source = indoc! {r#"
struct A {
f: vec4<f32>
};
@group(0) @binding(2) var<uniform> a: A;
@group(0) @binding(2) var<uniform> b: A;
@fragment
fn main() {}
"#};
let result = create_shader_module(source, "shader.wgsl", WriteOptions::default());
assert!(matches!(
result,
Err(CreateModuleError::DuplicateBinding { binding: 2 })
));
}
fn items_to_tokens(items: Vec<(TypePath, TokenStream)>) -> TokenStream {
let mut root = Module::default();
root.add_module_items(&items, &ModulePath::default());
root.to_tokens()
}
#[test]
fn write_vertex_module_empty() {
let source = indoc! {r#"
@vertex
fn main() {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_eq!(quote!(), items_to_tokens(actual));
}
#[test]
fn write_vertex_module_single_input_float32() {
let source = indoc! {r#"
struct VertexInput0 {
@location(0) a: f32,
@location(1) b: vec2<f32>,
@location(2) c: vec3<f32>,
@location(3) d: vec4<f32>,
};
@vertex
fn main(in0: VertexInput0) {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_snapshot!(items_to_tokens(actual));
}
#[test]
fn write_vertex_module_single_input_float64() {
let source = indoc! {r#"
struct VertexInput0 {
@location(0) a: f64,
@location(1) b: vec2<f64>,
@location(2) c: vec3<f64>,
@location(3) d: vec4<f64>,
};
@vertex
fn main(in0: VertexInput0) {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_snapshot!(items_to_tokens(actual));
}
#[test]
fn write_vertex_module_single_input_float16() {
let source = indoc! {r#"
enable f16;
struct VertexInput0 {
@location(0) a: f16,
@location(1) b: vec2<f16>,
@location(2) c: vec4<f16>,
};
@vertex
fn main(in0: VertexInput0) {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_snapshot!(items_to_tokens(actual));
}
#[test]
fn write_vertex_module_single_input_sint32() {
let source = indoc! {r#"
struct VertexInput0 {
@location(0) a: i32,
@location(1) b: vec2<i32>,
@location(2) c: vec3<i32>,
@location(3) d: vec4<i32>,
};
@vertex
fn main(in0: VertexInput0) {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_snapshot!(items_to_tokens(actual));
}
#[test]
fn write_vertex_module_single_input_uint32() {
let source = indoc! {r#"
struct VertexInput0 {
@location(0) a: u32,
@location(1) b: vec2<u32>,
@location(2) c: vec3<u32>,
@location(3) d: vec4<u32>,
};
@vertex
fn main(in0: VertexInput0) {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = vertex_struct_methods(&module, demangle_identity);
assert_tokens_snapshot!(items_to_tokens(actual));
}
#[test]
fn write_compute_module_empty() {
let source = indoc! {r#"
@vertex
fn main() {}
"#};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = compute_module(&module, demangle_identity);
assert_tokens_eq!(quote!(), actual);
}
#[test]
fn write_compute_module_multiple_entries() {
let source = indoc! {r#"
@compute
@workgroup_size(1,2,3)
fn main1() {}
@compute
@workgroup_size(256)
fn main2() {}
"#
};
let module = naga::front::wgsl::parse_str(source).unwrap();
let actual = compute_module(&module, demangle_identity);
assert_tokens_snapshot!(actual);
}
#[test]
fn create_shader_module_parse_error() {
let source = indoc! {r#"
var<push_constant> consts: vec4<f32>;
@fragment
fn fs_main() }
"#};
let result = create_shader_module(source, "shader.wgsl", WriteOptions::default());
assert!(matches!(result, Err(CreateModuleError::ParseError { .. })));
}
#[test]
fn create_shader_module_semantic_error() {
let source = indoc! {r#"
var<push_constant> consts: vec4<f32>;
@fragment
fn fs_main() {
consts.x = 1;
}
"#};
let result = create_shader_module(
source,
"shader.wgsl",
WriteOptions {
validate: Some(Default::default()),
..Default::default()
},
);
assert!(matches!(
result,
Err(CreateModuleError::ValidationError { .. })
));
}
fn demangle_underscore(name: &str) -> TypePath {
let components: Vec<_> = name.split("_").collect();
let (name, parents) = components.split_last().unwrap();
TypePath {
parent: ModulePath {
components: parents.into_iter().map(|p| p.to_string()).collect(),
},
name: name.to_string(),
}
}
#[test]
fn single_root_module() {
let output = create_shader_modules(
include_str!("data/modules.wgsl"),
WriteOptions {
rustfmt: true,
..Default::default()
},
demangle_underscore,
)
.unwrap();
assert_rust_snapshot!(output);
}
#[test]
fn add_single_root_module() {
let mut root = Module::default();
let options = WriteOptions {
rustfmt: true,
..Default::default()
};
root.add_shader_module(
include_str!("data/modules.wgsl"),
None,
options,
ModulePath::default(),
demangle_underscore,
)
.unwrap();
let output = root.to_generated_bindings(options);
assert_rust_snapshot!(output);
}
#[test]
fn add_duplicate_module_different_paths() {
let mut root = Module::default();
let options = WriteOptions {
rustfmt: true,
..Default::default()
};
root.add_shader_module(
include_str!("data/modules.wgsl"),
None,
options,
ModulePath {
components: vec!["shader1".to_string()],
},
demangle_underscore,
)
.unwrap();
root.add_shader_module(
include_str!("data/modules.wgsl"),
None,
options,
ModulePath {
components: vec!["shaders".to_string(), "shader2".to_string()],
},
demangle_underscore,
)
.unwrap();
let output = root.to_generated_bindings(options);
assert_rust_snapshot!(output);
}
#[test]
fn vertex_entries() {
let actual = create_shader_module(
include_str!("data/vertex_entries.wgsl"),
"shader.wgsl",
WriteOptions {
rustfmt: true,
..Default::default()
},
)
.unwrap();
assert_rust_snapshot!(actual);
}
#[test]
fn shader_stage_collection() {
let actual = create_shader_module(
include_str!("data/shader_stage_collection.wgsl"),
"shader.wgsl",
WriteOptions {
rustfmt: true,
derive_encase_host_shareable: true,
..Default::default()
},
)
.unwrap();
assert_rust_snapshot!(actual);
}
}