├── .github ├── FUNDING.yml └── workflows │ └── release-plz.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── example ├── Cargo.toml ├── build.rs ├── release.toml ├── shaders │ ├── global_bindings.wgsl │ └── triangle.wgsl └── src │ ├── main.rs │ └── shader_bindings.rs └── wgsl_bindgen ├── CHANGELOG.md ├── Cargo.toml ├── release.toml ├── src ├── bevy_util │ ├── deptree.rs │ ├── mod.rs │ ├── module_path_resolver.rs │ ├── name_demangle.rs │ ├── parse_imports.rs │ └── source_file.rs ├── bindgen │ ├── bindgen.rs │ ├── errors.rs │ ├── mod.rs │ └── options │ │ ├── bindings.rs │ │ ├── mod.rs │ │ └── types.rs ├── generate │ ├── bind_group │ │ ├── mod.rs │ │ ├── raw_shader_bind_group.rs │ │ └── single_bind_group.rs │ ├── consts.rs │ ├── entry.rs │ ├── mod.rs │ ├── pipeline.rs │ ├── shader_module.rs │ └── shader_registry.rs ├── lib.rs ├── naga_util │ ├── mod.rs │ └── module_to_source.rs ├── quote_gen │ ├── constants.rs │ ├── mod.rs │ ├── rust_module_builder.rs │ ├── rust_source_item.rs │ ├── rust_struct_builder.rs │ └── rust_type_info.rs ├── structs.rs ├── types.rs ├── wgsl.rs └── wgsl_type.rs └── tests ├── bindgen_tests.rs ├── deptree_tests.rs ├── issues_tests.rs ├── lib.rs ├── output ├── .gitignore ├── bindgen_bevy.expected.rs ├── bindgen_layouts.expected.rs ├── bindgen_main.expected.rs ├── bindgen_minimal.expected.rs ├── bindgen_padding.expected.rs └── issue_35.expected.rs └── shaders ├── additional └── types.wgsl ├── basic ├── bindings.wgsl ├── main.wgsl └── path_import.wgsl ├── bevy_pbr_wgsl ├── clustered_forward.wgsl ├── depth.wgsl ├── mesh.wgsl ├── mesh_bindings.wgsl ├── mesh_functions.wgsl ├── mesh_types.wgsl ├── mesh_vertex_output.wgsl ├── mesh_view_bindings.wgsl ├── mesh_view_types.wgsl ├── output_VERTEX_UVS.wgsl ├── pbr.wgsl ├── pbr │ ├── bindings.wgsl │ ├── functions.wgsl │ ├── lighting.wgsl │ └── types.wgsl ├── shadows.wgsl ├── skinning.wgsl ├── utils.wgsl └── wireframe.wgsl ├── issue_35 ├── clear.wgsl └── vertices.wgsl ├── layouts.wgsl ├── minimal.wgsl └── padding.wgsl /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Swoorup] 4 | buy_me_a_coffee: Swoorup 5 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | environment: crates.io 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@nightly 25 | - name: Run release-plz 26 | uses: MarcoIeni/release-plz-action@v0.5 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea/ 13 | .qodo 14 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | indent_style = "Block" 2 | reorder_imports = true 3 | tab_spaces = 2 4 | max_width=90 5 | fn_call_width=80 6 | group_imports="StdExternalCrate" 7 | imports_granularity = "Module" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | wgsl_bindgen/CHANGELOG.md -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | # wgpu requires the newer resolver 3 | resolver = "2" 4 | members = ["wgsl_bindgen", "example"] 5 | 6 | [workspace.package] 7 | version = "0.18.2" 8 | authors = ["swoorup", "ScanMountGoat(Original Fork)"] 9 | edition = "2021" 10 | repository = "https://github.com/Swoorup/wgsl_bindgen" 11 | license = "MIT" 12 | documentation = "https://docs.rs/wgsl_bindgen" 13 | 14 | [workspace.dependencies] 15 | blake3 = "1.6" 16 | bytemuck = "1.13" 17 | case = "1.0" 18 | colored = "3.0" 19 | data-encoding = "2.5" 20 | derivative = "2.2" 21 | derive_builder = "0.20" 22 | derive_more = { version = "2.0", features = ["full"] } 23 | encase = "0.10" 24 | enumflags2 = "0.7" 25 | futures = "0.3" 26 | fxhash = "0.2" 27 | glam = "0.30" 28 | heck = "0.5" 29 | include_absolute_path = "0.1" 30 | indexmap = "2.7" 31 | indoc = "2.0" 32 | miette = "7.5" 33 | naga = "24.0" 34 | naga_oil = "0.17" 35 | pathdiff = "0.2" 36 | pretty_assertions = "1.4" 37 | prettyplease = "0.2" 38 | proc-macro2 = "1.0" 39 | quote = "1.0" 40 | regex = "1.11" 41 | regex-syntax = "0.8" 42 | smallvec = "1.14" 43 | smol_str = "0.3" 44 | strum = "0.27" 45 | strum_macros = "0.27" 46 | syn = "2.0" 47 | thiserror = "2.0" 48 | wgpu = "24.0" 49 | wgpu-types = "24.0" 50 | winit = "0.30" 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SMG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Rough ideas. 2 | 3 | * Treat `_pad` as padding members and automatically initiallize it in init struct. 4 | This requires moving the pad field as a member in struct member entry info to a member entry variant type? 5 | 6 | * Allow injecting dynamic shader defines at build-time (we already have a runtime mechanism from generation) 7 | 8 | * Allow generation directly from shader strings. 9 | 10 | * proc_macro as an option alongside of build.rs. We need proc_macro::tracked* feature? 11 | 12 | * Use something like derivative and use MaybeUninit for padded fields 13 | 14 | * Use rustfmt instead of prettyparse 15 | 16 | * Use struct like this instead directly using the array. 17 | * ```rust 18 | #[repr(C)] 19 | struct PaddedField { 20 | field: T, 21 | padding: [u8; N], 22 | } 23 | 24 | impl PaddedField { 25 | pub fn new(value: T) -> Self { 26 | Self { 27 | field: value, 28 | padding: [0; N], 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | - https://github.com/rust-lang/rust/issues/73557 35 | - https://www.reddit.com/r/rust/comments/16e18kp/how_to_set_alignment_of_individual_struct_members/ 36 | 37 | * Add a way to encode variant types in wgsl?. 38 | * Maybe a seperate binary that accepts rust source. 39 | * Generates accessors, setters in wgsl 40 | * Struct fields are efficiently utilised. 41 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | authors.workspace = true 4 | version.workspace = true 5 | edition.workspace = true 6 | publish = false 7 | 8 | [dependencies] 9 | winit.workspace = true 10 | wgpu.workspace = true 11 | futures.workspace = true 12 | bytemuck = { workspace = true, features = ["derive"] } 13 | encase = { workspace = true, features = ["glam"] } 14 | glam = { workspace = true, features = ["bytemuck"] } 15 | naga_oil.workspace = true 16 | include_absolute_path.workspace = true 17 | 18 | [build-dependencies] 19 | wgsl_bindgen = { path = "../wgsl_bindgen" } 20 | miette = { workspace = true, features = ["fancy"] } 21 | -------------------------------------------------------------------------------- /example/build.rs: -------------------------------------------------------------------------------- 1 | use miette::{IntoDiagnostic, Result}; 2 | use wgsl_bindgen::qs::quote; 3 | use wgsl_bindgen::{ 4 | GlamWgslTypeMap, Regex, WgslBindgenOptionBuilder, WgslShaderIrCapabilities, 5 | WgslShaderSourceType, WgslTypeSerializeStrategy, 6 | }; 7 | 8 | fn main() -> Result<()> { 9 | WgslBindgenOptionBuilder::default() 10 | .workspace_root("shaders") 11 | .add_entry_point("shaders/triangle.wgsl") 12 | .skip_hash_check(true) 13 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 14 | .type_map(GlamWgslTypeMap) 15 | .ir_capabilities(WgslShaderIrCapabilities::PUSH_CONSTANT) 16 | .override_struct_field_type( 17 | [("utils::types::VectorsU32", "a", quote!(crate::MyTwoU32))].map(Into::into), 18 | ) 19 | .add_override_struct_mapping(("utils::types::Scalars", quote!(crate::MyScalars))) 20 | .add_custom_padding_field_regexp(Regex::new("_pad.*").unwrap()) 21 | .short_constructor(2) 22 | .shader_source_type( 23 | WgslShaderSourceType::HardCodedFilePathWithNagaOilComposer 24 | | WgslShaderSourceType::EmbedWithNagaOilComposer 25 | | WgslShaderSourceType::EmbedSource, 26 | ) 27 | .derive_serde(false) 28 | .output("src/shader_bindings.rs") 29 | .build()? 30 | .generate() 31 | .into_diagnostic()?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /example/release.toml: -------------------------------------------------------------------------------- 1 | tag = false 2 | publish = false -------------------------------------------------------------------------------- /example/shaders/global_bindings.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var color_texture: texture_2d; 2 | @group(0) @binding(1) var color_sampler: sampler; 3 | 4 | /// The time since startup in seconds. 5 | /// Wraps to 0 after 1 hour. 6 | @group(0) @binding(2) var time: f32; 7 | -------------------------------------------------------------------------------- /example/shaders/triangle.wgsl: -------------------------------------------------------------------------------- 1 | #import global_bindings::{ 2 | color_texture, 3 | color_sampler, 4 | time 5 | } 6 | 7 | struct Uniforms { 8 | color_rgb: vec4, 9 | } 10 | 11 | @group(1) @binding(0) 12 | var uniforms: Uniforms; 13 | 14 | struct VertexInput { 15 | @location(0) position: vec3, 16 | }; 17 | 18 | struct VertexOutput { 19 | @builtin(position) clip_position: vec4, 20 | @location(0) tex_coords: vec2, 21 | #ifdef VERTEX_UVS 22 | @location(2) uv: vec2, 23 | #endif 24 | }; 25 | 26 | @vertex 27 | fn vs_main(in: VertexInput) -> VertexOutput { 28 | //A fullscreen triangle. 29 | var out: VertexOutput; 30 | out.clip_position = vec4(in.position.xyz, 1.0); 31 | out.tex_coords = in.position.xy * 0.5 + 0.5; 32 | return out; 33 | } 34 | 35 | struct PushConstants { 36 | color_matrix: mat4x4 37 | } 38 | 39 | var constants: PushConstants; 40 | 41 | // wgsl outputs with pipeline constants are not supported. 42 | // https://github.com/gfx-rs/wgpu/blob/abba12ae4e5488b08d9e189fc37dab5e1755b443/naga/src/back/wgsl/writer.rs#L108-L113 43 | // override force_black: bool; 44 | // override scale: f32 = 1.0; 45 | 46 | @fragment 47 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 48 | let uv = in.tex_coords; 49 | let color = textureSample(color_texture, color_sampler, uv).rgb; 50 | 51 | // Simple time variable 52 | let t = time * 0.5; 53 | 54 | // Create a simple ripple effect from the center 55 | let center = vec2(0.5, 0.5); 56 | let dist = distance(uv, center); 57 | let ripple = sin(dist * 15.0 - t * 2.0) * 0.5 + 0.5; 58 | 59 | // Simple color cycling 60 | let color_shift = vec3( 61 | 0.5 + 0.5 * sin(t), 62 | 0.5 + 0.5 * sin(t + 2.0), 63 | 0.5 + 0.5 * sin(t + 4.0) 64 | ); 65 | 66 | // Simple vignette effect 67 | let vignette = smoothstep(0.0, 0.7, 1.0 - dist * 1.5); 68 | 69 | // Combine effects 70 | let final_color = color * uniforms.color_rgb.rgb * color_shift * (0.7 + 0.3 * ripple) * vignette; 71 | 72 | // Apply the color matrix from push constants 73 | return constants.color_matrix * vec4(final_color, 1.0); 74 | } -------------------------------------------------------------------------------- /wgsl_bindgen/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.18.2](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.18.1...wgsl_bindgen-v0.18.2) - 2025-05-28 9 | 10 | ### Other 11 | 12 | - Use the fully qualified path for core::mem::size_of 13 | - Added an example to patch bindgroup entry module path. 14 | 15 | ## [0.18.1](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.18.0...wgsl_bindgen-v0.18.1) - 2025-03-21 16 | 17 | ### Other 18 | 19 | - Add support for overriding bind group entry module paths 20 | - Fixed WgpuBindGroups generation in shader entries 21 | 22 | ## [0.18.0](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.17.0...wgsl_bindgen-v0.18.0) - 2025-03-20 23 | 24 | ### Other 25 | 26 | - Added support for reusable bind groups. Bindgroups defined in shared wgsl, are generated once on the shared module and not on entrypoint 27 | 28 | ## [0.15.2](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.15.1...wgsl_bindgen-v0.15.2) - 2024-12-13 29 | 30 | ### Other 31 | 32 | - Fix compute shader generation for wgpu 23 33 | - relaxed lifetime of &self in BindGroup::set 34 | - updated to wgpu 23 35 | 36 | ## [0.15.1](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.15.0...wgsl_bindgen-v0.15.1) - 2024-08-20 37 | 38 | ### Other 39 | - Fix missing cache property 40 | 41 | ## [0.15.0](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.14.1...wgsl_bindgen-v0.15.0) - 2024-08-10 42 | 43 | ### Other 44 | - Support push constants output in naga_oil composition strategy 45 | - Upgrade to wgpu 22 46 | - Rename `RustItemKind` => `RustItemType` 47 | - Treat built-in fields as padding [#34](https://github.com/Swoorup/wgsl-bindgen/pull/34) 48 | 49 | ## [0.14.1](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.14.0...wgsl_bindgen-v0.14.1) - 2024-07-27 50 | 51 | ### Other 52 | - Fix vertex type in module [#35](https://github.com/Swoorup/wgsl-bindgen/pull/35) 53 | - Update README.md 54 | 55 | ## [0.14.0](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.13.2...wgsl_bindgen-v0.14.0) - 2024-07-06 56 | 57 | ### Other 58 | - Remove unnecessary bindgroups module 59 | - Rename EntryCollection to Entries and cleanup examples 60 | 61 | ## [0.13.2](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.13.1...wgsl_bindgen-v0.13.2) - 2024-07-05 62 | 63 | ### Other 64 | - Bind Group Entry Collections can be cloned 65 | 66 | ## [0.13.1](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.13.0...wgsl_bindgen-v0.13.1) - 2024-07-05 67 | 68 | ### Other 69 | - Fix `min_binding_size` when invoking entry module contains symbols. 70 | 71 | ## [0.13.0](https://github.com/Swoorup/wgsl-bindgen/compare/wgsl_bindgen-v0.12.0...wgsl_bindgen-v0.13.0) - 2024-07-05 72 | 73 | ### Other 74 | - Added release-plz workflow 75 | - Reference version from workspace root 76 | - For bindgroup generation, rename Layout to EntryCollection with helper structs 77 | - Added `min_binding_size` for buffer types where possible 78 | - Allow to fully qualify or use relative name 79 | - Added a way to skip generating `_root_` 80 | 81 | ## v0.12.0 (2024-06-10) 82 | 83 | 84 | 85 | ### Other 86 | 87 | - format builder error message into bindgen error 88 | 89 | ### Commit Statistics 90 | 91 | 92 | 93 | - 51 commits contributed to the release over the course of 125 calendar days. 94 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 95 | - 0 issues like '(#ID)' were seen in commit messages 96 | 97 | ### Commit Details 98 | 99 | 100 | 101 |
view details 102 | 103 | * **Uncategorized** 104 | - Added changelog ([`cd55d10`](https://github.com/Swoorup/wgsl-bindgen/commit/cd55d10c57f1e159a0c31988c67559b559a68ace)) 105 | - Release wgsl_bindgen v0.12.0 ([`d61fd9e`](https://github.com/Swoorup/wgsl-bindgen/commit/d61fd9e174877500ba86d089101ecba7c1b5886f)) 106 | - Fix typo ([`22adeec`](https://github.com/Swoorup/wgsl-bindgen/commit/22adeece762ad8835a812fc448a3281ae6ce42f9)) 107 | - Added non-working support for overridable constants ([`e1937d6`](https://github.com/Swoorup/wgsl-bindgen/commit/e1937d661f920812e3587d2cb70362cad15a613f)) 108 | - Initial upgrade to wgpu 0.20 ([`92bf827`](https://github.com/Swoorup/wgsl-bindgen/commit/92bf8274c3bdc39e4332f558a653647be61c3d95)) 109 | - Make the texture sample type filterable ([`0660ee1`](https://github.com/Swoorup/wgsl-bindgen/commit/0660ee19a21e65f6da14835fd9cd85924ae762b1)) 110 | - Consolidate specifying versions in the root manifest ([`42d2822`](https://github.com/Swoorup/wgsl-bindgen/commit/42d2822da5a85e1964b4442db090a6991a5b30c3)) 111 | - Added option to change the visibily of the export types ([`88fd877`](https://github.com/Swoorup/wgsl-bindgen/commit/88fd877fc2c75c35dee3d313d93d93e22ffcb75b)) 112 | - Fix issues with texture_2d of type i32 or u32 ([`53c0c63`](https://github.com/Swoorup/wgsl-bindgen/commit/53c0c63f6e4ea2a2569182bea2e99874ca64461e)) 113 | - Use the renamed crate include_absolute_path ([`6f485bf`](https://github.com/Swoorup/wgsl-bindgen/commit/6f485bf0beb05992d8d2a2ee1950738fd2e434fe)) 114 | - Make SHADER_STRING public ([`ce4f68b`](https://github.com/Swoorup/wgsl-bindgen/commit/ce4f68b418241c3224240bab42e9cbe0bae52905)) 115 | - Regex for all overrides ([`8ea7ffd`](https://github.com/Swoorup/wgsl-bindgen/commit/8ea7ffd65871af95aaeaff8da9d4589f20ff049c)) 116 | - Simplify also for bulk options ([`d45d6f0`](https://github.com/Swoorup/wgsl-bindgen/commit/d45d6f0898c52fa7f8ad41abb7f466e6ae2aec25)) 117 | - Adding custom padding field support ([`998f7a8`](https://github.com/Swoorup/wgsl-bindgen/commit/998f7a8f60b83424fff93e471f04adf7130a8f83)) 118 | - Adjust size if custom alignment is specified. ([`a4b61c7`](https://github.com/Swoorup/wgsl-bindgen/commit/a4b61c7d52496499b92b029a3604053d2420b147)) 119 | - Ability to override alignment for structs ([`cd26b91`](https://github.com/Swoorup/wgsl-bindgen/commit/cd26b91be29870ac629a1674a8a43ba98d46b6d6)) 120 | - Use Result type for create_shader* when using `NagaOilComposer` ([`80a7f95`](https://github.com/Swoorup/wgsl-bindgen/commit/80a7f9594330b6e982bb91bb12991df8b79cba70)) 121 | - Seperate types, assertions, impls in generated output ([`c2c4dc9`](https://github.com/Swoorup/wgsl-bindgen/commit/c2c4dc956925aedef11d706cd7024c8b25593a66)) 122 | - RustSourceItem => RustItem ([`ce2a91e`](https://github.com/Swoorup/wgsl-bindgen/commit/ce2a91eca61507ba237fd9828a84a5d00a6e2d99)) 123 | - Pass entry point name to builders ([`4fc895b`](https://github.com/Swoorup/wgsl-bindgen/commit/4fc895bef6ce8a29b32611fc363ea68a40b60405)) 124 | - Export quote, syn functions and macros ([`782f481`](https://github.com/Swoorup/wgsl-bindgen/commit/782f481c70bb5d8ae8381c0ddf83ec4ddc6a2a79)) 125 | - Added extra bindings generator as prep for targetting non-wgpu libs ([`9b6204d`](https://github.com/Swoorup/wgsl-bindgen/commit/9b6204d62b4daa5f45c7d9a0ee05d41380f37650)) 126 | - Added custom field mappings ([`4132659`](https://github.com/Swoorup/wgsl-bindgen/commit/4132659692ea4a34a7cf510829a470dc3390b269)) 127 | - Avoid HashMap for more consitent shader bindings generation ([`fd6d144`](https://github.com/Swoorup/wgsl-bindgen/commit/fd6d144dafbcc6e234d479f5c7e5c53c93f0816c)) 128 | - Rename ShaderRegistry to ShaderEntry in output ([`1461393`](https://github.com/Swoorup/wgsl-bindgen/commit/1461393b0710e23a028478f1df131191f2398c2e)) 129 | - Added mandatory workspace root option used for resolving imports ([`d20d3d5`](https://github.com/Swoorup/wgsl-bindgen/commit/d20d3d5176984f305d4a3e190500c4601671af85)) 130 | - Add shader labels ([`c8a129b`](https://github.com/Swoorup/wgsl-bindgen/commit/c8a129bc5529a468eb29687b20ce4c40e6fa647f)) 131 | - Feature shader registry and shader defines ([`187c7f4`](https://github.com/Swoorup/wgsl-bindgen/commit/187c7f417ec9be4543168c462ed6d171ba3180c6)) 132 | - Added multiple shader source option ([`db90739`](https://github.com/Swoorup/wgsl-bindgen/commit/db90739cec926b464eb6fafb8f1254c42ad91201)) 133 | - Add ability to override struct and path based source type ([`1d4ee0a`](https://github.com/Swoorup/wgsl-bindgen/commit/1d4ee0a552ffe4e6a9298f183bd3c9b617635908)) 134 | - Short const constructors and fix demangle in comments ([`a49be89`](https://github.com/Swoorup/wgsl-bindgen/commit/a49be89ca98ca65ca296717b0f98e24530ad11b0)) 135 | - Rename Capabilities to WgslShaderIRCapabilities, and update test ([`1cad0cb`](https://github.com/Swoorup/wgsl-bindgen/commit/1cad0cbe5ff581810b770c6fb95940f1472c7fd1)) 136 | - Reexport Capabilities ([`7262606`](https://github.com/Swoorup/wgsl-bindgen/commit/7262606a6d0880c9f8aa8872197a3e151a16975b)) 137 | - Allow setting capabilities ([`b6df117`](https://github.com/Swoorup/wgsl-bindgen/commit/b6df117b40909cfeb803c6a7782ab2d2dc906176)) 138 | - Release new version ([`ec3d554`](https://github.com/Swoorup/wgsl-bindgen/commit/ec3d55412002d27c48200261b8e9853e9bfe8af2)) 139 | - Make naga oil's error more useful ([`6a1bc45`](https://github.com/Swoorup/wgsl-bindgen/commit/6a1bc45524ffeb4386ff18f846588cf6c1ea0e1b)) 140 | - Format builder error message into bindgen error ([`e52a9db`](https://github.com/Swoorup/wgsl-bindgen/commit/e52a9dbe660a417afa371f480be161d58f1dd642)) 141 | - Ignore snake case warnings if struct is not camel case ([`54c563e`](https://github.com/Swoorup/wgsl-bindgen/commit/54c563eb3d89d9815d7391b599c1a86de3a14d25)) 142 | - Minor corrections ([`194b3e4`](https://github.com/Swoorup/wgsl-bindgen/commit/194b3e4a66bfaad0ebc577670b50eec372701e35)) 143 | - Added a mechanism to scan additional source directory ([`300a3d7`](https://github.com/Swoorup/wgsl-bindgen/commit/300a3d7aec20556712bd835d71a42ca375ae1da9)) 144 | - Allow to use naga_oil compose in the generated output ([`f32c279`](https://github.com/Swoorup/wgsl-bindgen/commit/f32c279c02ea7760ce901533013f6d0da51674c5)) 145 | - Fix direct item wgsl imports. ([`3e58108`](https://github.com/Swoorup/wgsl-bindgen/commit/3e581089e21b245bd85feecdc94f3f1d9310aacc)) 146 | - Added failing test for direct path import for nested type ([`e014d4b`](https://github.com/Swoorup/wgsl-bindgen/commit/e014d4b6c5326a40d59291be96e24a3fd150d746)) 147 | - Demangle bindgroup struct fields if imported from other wgsl files ([`7231f78`](https://github.com/Swoorup/wgsl-bindgen/commit/7231f78806e75a18af9f78005c3b016f16dcf1dc)) 148 | - Add support for scalar types in bindings ([`4af047a`](https://github.com/Swoorup/wgsl-bindgen/commit/4af047aa976252211f31f882db8b5006fecb1977)) 149 | - Add support for path based import. ([`d1e861d`](https://github.com/Swoorup/wgsl-bindgen/commit/d1e861dacd5cb04f1b74065448fde980cfc696b6)) 150 | - Demangle name for consts items ([`5ec2c1a`](https://github.com/Swoorup/wgsl-bindgen/commit/5ec2c1a22c2b4c1855dee3d2d88fa0b46ad88d6c)) 151 | - Updated docs, use stable features only ([`06401c5`](https://github.com/Swoorup/wgsl-bindgen/commit/06401c5eb0c5d867bee4aedf4b339f9cd373f9a5)) 152 | - Support naga oil flavour of wgsl ([`99ea17c`](https://github.com/Swoorup/wgsl-bindgen/commit/99ea17c17bf682dd1ed9990341fb1a3aa119a6f6)) 153 | - Enable Runtime Sized Array, Padding for bytemuck mode ([`9e21d1d`](https://github.com/Swoorup/wgsl-bindgen/commit/9e21d1dbe084f1588d7e03e2c93642ca3ffb2c05)) 154 | - Create a fork ([`1c99e10`](https://github.com/Swoorup/wgsl-bindgen/commit/1c99e103625154dde0e357419f064e941e156f54)) 155 |
156 | 157 | -------------------------------------------------------------------------------- /wgsl_bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgsl_bindgen" 3 | authors.workspace = true 4 | version.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | documentation.workspace = true 8 | license.workspace = true 9 | publish = true 10 | description = "Type safe Rust bindings workflow for wgsl shaders in wgpu" 11 | readme = "../README.md" 12 | 13 | [dependencies] 14 | naga = { workspace = true, features = ["wgsl-in"] } 15 | wgpu-types.workspace = true 16 | syn.workspace = true 17 | quote.workspace = true 18 | proc-macro2.workspace = true 19 | prettyplease.workspace = true 20 | thiserror.workspace = true 21 | case.workspace = true 22 | naga_oil.workspace = true 23 | regex.workspace = true 24 | data-encoding.workspace = true 25 | indexmap.workspace = true 26 | smallvec.workspace = true 27 | derive_more.workspace = true 28 | fxhash.workspace = true 29 | derivative.workspace = true 30 | smol_str.workspace = true 31 | colored.workspace = true 32 | derive_builder.workspace = true 33 | miette.workspace = true 34 | blake3.workspace = true 35 | regex-syntax.workspace = true 36 | strum.workspace = true 37 | strum_macros.workspace = true 38 | pathdiff.workspace = true 39 | enumflags2.workspace = true 40 | heck.workspace = true 41 | 42 | [dev-dependencies] 43 | indoc.workspace = true 44 | pretty_assertions.workspace = true 45 | miette = { workspace = true, features = ["fancy"] } 46 | -------------------------------------------------------------------------------- /wgsl_bindgen/release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "" 2 | tag-message = "{{version}}" -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/deptree.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use colored::*; 4 | use indexmap::map::Entry; 5 | use miette::{Diagnostic, NamedSource, SourceSpan}; 6 | use smallvec::SmallVec; 7 | use thiserror::Error; 8 | use DependencyTreeError::*; 9 | 10 | use super::parse_imports::ImportStatement; 11 | use super::source_file::SourceFile; 12 | use super::ModulePathResolver; 13 | use crate::{ 14 | AdditionalScanDirectory, FxIndexMap, FxIndexSet, ImportPathPart, SourceFilePath, 15 | SourceModuleName, 16 | }; 17 | 18 | #[derive(Debug, Error, Diagnostic)] 19 | pub enum DependencyTreeError { 20 | #[error("Source file not found: {path}")] 21 | SourceNotFound { path: SourceFilePath }, 22 | #[error("Cannot find import `{path}` in this scope")] 23 | #[diagnostic(help("Maybe a typo or a missing file."))] 24 | ImportPathNotFound { 25 | path: String, 26 | stmt: ImportStatement, 27 | 28 | #[source_code] 29 | src: NamedSource, 30 | 31 | #[label("Import statement")] 32 | import_bit: SourceSpan, 33 | }, 34 | } 35 | 36 | #[derive(Default)] 37 | struct MaxRecursionLimiter { 38 | files_visited: Vec<(String, usize, String)>, // (file_path, line_number, import_str) 39 | } 40 | 41 | impl MaxRecursionLimiter { 42 | const MAX_RECURSION_DEPTH: usize = 16; 43 | 44 | fn push(&mut self, import_stmt: &ImportStatement, source: &SourceFile) -> &mut Self { 45 | let import_str = &source.content[import_stmt.range()]; 46 | self.files_visited.push(( 47 | source.file_path.to_string(), 48 | import_stmt.source_location.line_number, 49 | import_str.to_string(), 50 | )); 51 | self 52 | } 53 | 54 | fn pop(&mut self) -> &mut Self { 55 | self.files_visited.pop(); 56 | self 57 | } 58 | 59 | fn check_depth(&self) { 60 | if self.files_visited.len() > Self::MAX_RECURSION_DEPTH { 61 | let visited_files = self 62 | .files_visited 63 | .iter() 64 | .map(|(path, line, import)| { 65 | format!( 66 | "\n{}:{}: {}", 67 | path.to_string().cyan(), 68 | line.to_string().purple(), 69 | import.to_string().yellow() 70 | ) 71 | }) 72 | .rev() 73 | .collect::(); 74 | 75 | panic!( 76 | "{}\n{}\n{}\n", 77 | "Recursion limit exceeded".red(), 78 | "This error may be due to a circular dependency. The files visited during the recursion were:".red(), 79 | visited_files 80 | ); 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug, Clone)] 86 | pub struct SourceWithFullDependenciesResult<'a> { 87 | pub source_file: &'a SourceFile, 88 | pub full_dependencies: SmallVec<[&'a SourceFile; 16]>, 89 | } 90 | 91 | #[derive(Debug)] 92 | pub struct DependencyTree { 93 | resolver: ModulePathResolver, 94 | parsed_sources: FxIndexMap, 95 | entry_points: FxIndexSet, 96 | } 97 | 98 | /// Represents a dependency tree for tracking the dependencies between source files. 99 | /// 100 | /// The `DependencyTree` struct provides methods for generating possible import paths, 101 | /// crawling import statements, crawling source files, building the dependency tree, 102 | /// and retrieving all files including dependencies and the full dependency set for a given source file. 103 | impl DependencyTree { 104 | /// Tries to build a dependency tree for the given entry points. 105 | /// 106 | /// This method takes a module prefix and a list of entry points (source file paths) and 107 | /// attempts to build a dependency tree by crawling the source files and resolving import 108 | /// statements. It returns a `Result` indicating whether the dependency tree was successfully 109 | /// built or an error occurred. 110 | /// 111 | /// # Arguments 112 | /// 113 | /// * `module_prefix` - An optional module prefix to be used when generating import paths. 114 | /// * `entry_points` - A vector of source file paths representing the entry points of the 115 | /// dependency tree. 116 | /// 117 | /// # Returns 118 | /// 119 | /// A `Result` containing the built `DependencyTree` if successful, or a `DependencyTreeError` 120 | /// if an error occurred during the build process. 121 | pub fn try_build( 122 | workspace_root: PathBuf, 123 | entry_module_prefix: Option, 124 | entry_points: Vec, // path to entry points 125 | additional_scan_dirs: Vec, 126 | ) -> Result { 127 | let resolver = 128 | ModulePathResolver::new(workspace_root, entry_module_prefix, additional_scan_dirs); 129 | 130 | let mut tree = Self { 131 | resolver, 132 | parsed_sources: Default::default(), 133 | entry_points: Default::default(), 134 | }; 135 | 136 | for entry_point in entry_points { 137 | tree.entry_points.insert(entry_point.clone()); 138 | tree.crawl_source(entry_point, None, &mut MaxRecursionLimiter::default())? 139 | } 140 | 141 | Ok(tree) 142 | } 143 | 144 | /// Crawls an import statement and resolves the import paths. 145 | fn crawl_import_module( 146 | &mut self, 147 | parent_source_path: &SourceFilePath, 148 | import_stmt: &ImportStatement, 149 | import_path_part: &ImportPathPart, 150 | limiter: &mut MaxRecursionLimiter, 151 | ) -> Result<(), DependencyTreeError> { 152 | let possible_source_path = self 153 | .resolver 154 | .generate_best_possible_paths(&import_path_part, parent_source_path) 155 | .into_iter() 156 | .find(|(_, path)| path.is_file()); // make sure this is not reimporting itself 157 | 158 | let Some(parent_source) = self.parsed_sources.get_mut(parent_source_path) else { 159 | unreachable!("{:?} source code as not parsed", parent_source_path) 160 | }; 161 | 162 | let Some((module_name, source_path)) = possible_source_path else { 163 | return Err(ImportPathNotFound { 164 | stmt: import_stmt.clone(), 165 | path: import_path_part.to_string(), 166 | import_bit: (&import_stmt.source_location).into(), 167 | src: NamedSource::new( 168 | parent_source_path.to_string(), 169 | parent_source.content.clone(), 170 | ), 171 | }); 172 | }; 173 | 174 | // add self as a dependency to the parent 175 | parent_source.add_direct_dependency(source_path.clone()); 176 | 177 | limiter.push(import_stmt, parent_source).check_depth(); 178 | 179 | // if not crawled, crawl this import file 180 | if !self.parsed_sources.contains_key(&source_path) { 181 | self 182 | .crawl_source(source_path, Some(module_name), limiter) 183 | .expect("failed to crawl import path"); 184 | } 185 | 186 | limiter.pop(); 187 | 188 | Ok(()) 189 | } 190 | 191 | /// Crawls a source file and its dependencies. 192 | fn crawl_source( 193 | &mut self, 194 | source_path: SourceFilePath, 195 | module_name: Option, 196 | limiter: &mut MaxRecursionLimiter, 197 | ) -> Result<(), DependencyTreeError> { 198 | match self.parsed_sources.entry(source_path.clone()) { 199 | Entry::Occupied(_) => {} // do nothing 200 | Entry::Vacant(entry) => { 201 | let content = entry.key().read_contents().or(Err(SourceNotFound { 202 | path: entry.key().clone(), 203 | }))?; 204 | 205 | let source_file = 206 | SourceFile::create(entry.key().clone(), module_name.clone(), content); 207 | entry.insert(source_file); 208 | } 209 | }; 210 | 211 | let source_file = self.parsed_sources.get(&source_path).unwrap(); 212 | 213 | for import_stmt in &source_file.imports.clone() { 214 | for import_path_part in import_stmt.get_import_path_parts() { 215 | self.crawl_import_module( 216 | &source_path, 217 | &import_stmt, 218 | &import_path_part, 219 | limiter, 220 | )? 221 | } 222 | } 223 | 224 | Ok(()) 225 | } 226 | 227 | /// Returns all the source files including their dependencies in the dependency tree. 228 | pub fn all_files_including_dependencies(&self) -> FxIndexSet { 229 | self.parsed_sources.keys().cloned().collect() 230 | } 231 | 232 | pub fn parsed_files(&self) -> Vec<&SourceFile> { 233 | self.parsed_sources.values().collect() 234 | } 235 | 236 | /// Returns the full set of dependencies for a given source file. 237 | pub fn get_full_dependency_for( 238 | &self, 239 | source_path: &SourceFilePath, 240 | ) -> FxIndexSet { 241 | self 242 | .parsed_sources 243 | .get(source_path) 244 | .iter() 245 | .flat_map(|source| { 246 | source 247 | .direct_dependencies 248 | .iter() 249 | .flat_map(|dep| { 250 | let mut deps = FxIndexSet::default(); 251 | let sub_deps = self.get_full_dependency_for(dep); 252 | // insert the imported deps first 253 | deps.extend(sub_deps); 254 | 255 | // insert the current dep last 256 | deps.insert(dep.clone()); 257 | 258 | deps 259 | }) 260 | .collect::>() 261 | }) 262 | .collect() 263 | } 264 | 265 | /// Returns the source files with their full dependencies in the dependency tree. 266 | /// 267 | /// This method returns a vector of `SourceWithFullDependenciesResult` structs, each containing 268 | /// a source file and its full set of dependencies. The full set of dependencies includes both 269 | /// direct and transitive dependencies. 270 | /// 271 | /// # Returns 272 | /// 273 | /// A vector of `SourceWithFullDependenciesResult` structs, each representing a source file 274 | /// along with its full set of dependencies. 275 | pub fn get_source_files_with_full_dependencies( 276 | &self, 277 | ) -> Vec> { 278 | self 279 | .entry_points 280 | .iter() 281 | .map(|entry_point| { 282 | let source_file = self.parsed_sources.get(entry_point).unwrap(); 283 | let full_dependencies = self 284 | .get_full_dependency_for(entry_point) 285 | .iter() 286 | .map(|dep| self.parsed_sources.get(dep).unwrap()) 287 | .collect(); 288 | 289 | SourceWithFullDependenciesResult { 290 | source_file, 291 | full_dependencies, 292 | } 293 | }) 294 | .collect() 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod deptree; 2 | mod module_path_resolver; 3 | mod name_demangle; 4 | pub mod parse_imports; 5 | pub mod source_file; 6 | 7 | pub use deptree::*; 8 | use module_path_resolver::*; 9 | pub use name_demangle::*; 10 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/module_path_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use derive_more::Constructor; 4 | use smallvec::SmallVec; 5 | 6 | use super::escape_os_path; 7 | use crate::{ 8 | AdditionalScanDirectory, FxIndexSet, ImportPathPart, SourceFilePath, SourceModuleName, 9 | }; 10 | 11 | #[derive(Debug, Constructor, Clone, Default)] 12 | pub(crate) struct ModulePathResolver { 13 | workspace_root: PathBuf, 14 | entry_module_prefix: Option, 15 | additional_scan_dirs: Vec, 16 | } 17 | 18 | impl ModulePathResolver { 19 | fn create_path( 20 | module_prefix: &Option, 21 | root_dir: &Path, 22 | path_fragments: &[&str], 23 | ) -> Option<(SourceModuleName, SourceFilePath)> { 24 | let mut path = PathBuf::from(root_dir); 25 | let mut module_name_builder = Vec::new(); 26 | 27 | for fragment in path_fragments { 28 | // Allow to use paths directly 29 | let normalized_fragment = escape_os_path(fragment); 30 | 31 | // avoid duplicates repeated patterns 32 | if !path.ends_with(&normalized_fragment) { 33 | path.push(&normalized_fragment); 34 | module_name_builder.push(*fragment); 35 | } 36 | } 37 | 38 | if path.extension().is_none() { 39 | path.set_extension("wgsl"); 40 | } 41 | 42 | if module_name_builder.is_empty() { 43 | None 44 | } else { 45 | let module_name = module_prefix 46 | .as_slice() 47 | .iter() 48 | .map(|s| s.as_str()) 49 | .chain(module_name_builder.into_iter()) 50 | .collect::>() 51 | .join("::"); 52 | 53 | let module_name = SourceModuleName::new(module_name); 54 | let source_path = SourceFilePath::new(path); 55 | Some((module_name, source_path)) 56 | } 57 | } 58 | 59 | fn generate_paths_for_dir<'a>( 60 | module_prefix: &'a Option, 61 | import_parts: SmallVec<[&'a str; 10]>, 62 | from_dir: &'a Path, 63 | current_source_path: &'a SourceFilePath, 64 | ) -> impl Iterator + 'a { 65 | (0..import_parts.len()) 66 | .filter_map(move |i| { 67 | Self::create_path(module_prefix, &from_dir, &import_parts[0..=i]) 68 | }) 69 | .filter(|(_, path)| path.as_ref() != current_source_path.as_path()) 70 | .rev() 71 | } 72 | 73 | /// Generates possible import paths for a given import path fragment. 74 | pub fn generate_best_possible_paths( 75 | &self, 76 | import_path_part: &ImportPathPart, 77 | source_path: &SourceFilePath, 78 | ) -> FxIndexSet<(SourceModuleName, SourceFilePath)> { 79 | let import_parts: SmallVec<[&str; 10]> = import_path_part 80 | .split("::") 81 | .enumerate() 82 | .skip_while(|(i, part)| { 83 | *i == 0 && self.entry_module_prefix == Some(part.to_string()) // skip the first part 84 | }) 85 | .map(|(_, part)| part) 86 | .filter(|part| !part.is_empty()) 87 | .collect(); 88 | 89 | if import_parts.is_empty() { 90 | panic!("import module is empty") 91 | } 92 | 93 | let source_dir = source_path.parent().unwrap_or(Path::new("")); 94 | 95 | let mut paths = Self::generate_paths_for_dir( 96 | &self.entry_module_prefix, 97 | import_parts.clone(), 98 | &self.workspace_root, 99 | source_path, 100 | ) 101 | .chain(Self::generate_paths_for_dir( 102 | &self.entry_module_prefix, 103 | import_parts.clone(), 104 | &source_dir, 105 | source_path, 106 | )) 107 | .collect::>(); 108 | 109 | for scan_dir in &self.additional_scan_dirs { 110 | let scan_path = Path::new(&scan_dir.directory); 111 | paths.extend(Self::generate_paths_for_dir( 112 | &scan_dir.module_import_root, 113 | import_parts.clone(), 114 | scan_path, 115 | source_path, 116 | )) 117 | } 118 | 119 | paths 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use indexmap::indexset; 126 | use pretty_assertions::assert_eq; 127 | 128 | use crate::bevy_util::ModulePathResolver; 129 | use crate::{ImportPathPart, SourceFilePath, SourceModuleName}; 130 | 131 | #[test] 132 | fn should_generate_single_import_path() { 133 | let module_prefix = None; 134 | let source_path = SourceFilePath::new("mydir/source.wgsl"); 135 | let import_path_part = ImportPathPart::new("Fragment"); 136 | 137 | let result = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 138 | .generate_best_possible_paths(&import_path_part, &source_path); 139 | 140 | let expected = indexset![( 141 | SourceModuleName::new("Fragment"), 142 | SourceFilePath::new("mydir/Fragment.wgsl") 143 | )]; 144 | 145 | assert_eq!(expected, result); 146 | } 147 | 148 | #[test] 149 | fn should_generate_single_import_path_when_module_prefix_match() { 150 | let module_prefix = Some("mymod".to_string()); 151 | let source_path = SourceFilePath::new("mydir/source.wgsl"); 152 | let import_path_part = ImportPathPart::new("mymod::Fragment"); 153 | 154 | let result = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 155 | .generate_best_possible_paths(&import_path_part, &source_path); 156 | 157 | let expected = indexset![( 158 | SourceModuleName::new("mymod::Fragment"), 159 | SourceFilePath::new("mydir/Fragment.wgsl") 160 | )]; 161 | 162 | assert_eq!(expected, result); 163 | } 164 | 165 | // Should generate import paths with correct extensions 166 | #[test] 167 | fn should_generate_import_paths_with_correct_extensions() { 168 | let module_prefix = Some("prefix".to_string()); 169 | let source_path = SourceFilePath::new("mydir/source"); 170 | let import_path_part = ImportPathPart::new("Module::Submodule::Fragment"); 171 | 172 | let actual = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 173 | .generate_best_possible_paths(&import_path_part, &source_path); 174 | 175 | let expected = indexset![ 176 | ( 177 | SourceModuleName::new("prefix::Module::Submodule::Fragment"), 178 | SourceFilePath::new("mydir/Module/Submodule/Fragment.wgsl") 179 | ), 180 | ( 181 | SourceModuleName::new("prefix::Module::Submodule"), 182 | SourceFilePath::new("mydir/Module/Submodule.wgsl") 183 | ), 184 | ( 185 | SourceModuleName::new("prefix::Module"), 186 | SourceFilePath::new("mydir/Module.wgsl") 187 | ), 188 | ]; 189 | assert_eq!(expected, actual); 190 | } 191 | 192 | #[test] 193 | #[should_panic] 194 | fn should_panic_when_import_module_is_empty() { 195 | let module_prefix = None; 196 | let source_path = SourceFilePath::new("mydir/source.wgsl"); 197 | let import_path_part = ImportPathPart::new(""); 198 | 199 | let result = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 200 | .generate_best_possible_paths(&import_path_part, &source_path); 201 | 202 | let expected = indexset![]; 203 | 204 | assert_eq!(expected, result); 205 | } 206 | 207 | // Should return an empty SmallVec when import_module has only the module prefix 208 | #[test] 209 | #[should_panic] 210 | fn should_return_empty_smallvec_when_import_module_has_only_module_prefix() { 211 | let module_prefix = Some("prefix".to_string()); 212 | let source_path = SourceFilePath::new("mydir/source.wgsl"); 213 | let import_path_part = ImportPathPart::new("prefix"); 214 | 215 | let result = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 216 | .generate_best_possible_paths(&import_path_part, &source_path); 217 | 218 | let expected = indexset![]; 219 | 220 | assert_eq!(expected, result); 221 | } 222 | 223 | #[test] 224 | fn should_return_smallvec_when_import_module() { 225 | let module_prefix = Some("prefix".to_string()); 226 | let source_path = SourceFilePath::new("mydir/source.wgsl"); 227 | let import_path_part = ImportPathPart::new("Fragment"); 228 | 229 | let result = ModulePathResolver::new("mydir".into(), module_prefix, vec![]) 230 | .generate_best_possible_paths(&import_path_part, &source_path); 231 | 232 | let expected = indexset![( 233 | SourceModuleName::new("prefix::Fragment"), 234 | SourceFilePath::new("mydir/Fragment.wgsl") 235 | )]; 236 | 237 | assert_eq!(expected, result); 238 | } 239 | 240 | #[test] 241 | fn should_return_valid_pbr_paths_from_repeated_part() { 242 | let module_prefix = Some("bevy_pbr".to_string()); 243 | let source_path = SourceFilePath::new("tests/bevy_pbr_wgsl/pbr/functions.wgsl"); 244 | let import_path_part = ImportPathPart::new("bevy_pbr::pbr::types"); 245 | 246 | let result = 247 | ModulePathResolver::new("tests/bevy_pbr_wgsl/pbr".into(), module_prefix, vec![]) 248 | .generate_best_possible_paths(&import_path_part, &source_path); 249 | 250 | let expected = indexset![( 251 | SourceModuleName::new("bevy_pbr::types"), 252 | SourceFilePath::new("tests/bevy_pbr_wgsl/pbr/types.wgsl") 253 | )]; 254 | 255 | assert_eq!(expected, result); 256 | } 257 | 258 | #[test] 259 | fn should_return_valid_pbr_paths_back_to_current_dir() { 260 | let module_prefix = Some("bevy_pbr".to_string()); 261 | let source_path = SourceFilePath::new("tests/bevy_pbr_wgsl/pbr/functions.wgsl"); 262 | let import_path_part = ImportPathPart::new("bevy_pbr::mesh_types"); 263 | 264 | let result = 265 | ModulePathResolver::new("tests/bevy_pbr_wgsl".into(), module_prefix, vec![]) 266 | .generate_best_possible_paths(&import_path_part, &source_path); 267 | 268 | let expected = indexset![ 269 | ( 270 | SourceModuleName::new("bevy_pbr::mesh_types"), 271 | SourceFilePath::new("tests/bevy_pbr_wgsl/mesh_types.wgsl"), 272 | ), 273 | ( 274 | SourceModuleName::new("bevy_pbr::mesh_types"), 275 | SourceFilePath::new("tests/bevy_pbr_wgsl/pbr/mesh_types.wgsl") 276 | ) 277 | ]; 278 | 279 | assert_eq!(expected, result); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/name_demangle.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::sync::OnceLock; 3 | 4 | use regex::Regex; 5 | use smallvec::SmallVec; 6 | 7 | use crate::quote_gen::RustSourceItemPath; 8 | 9 | const DECORATION_PRE: &str = "X_naga_oil_mod_X"; 10 | const DECORATION_POST: &str = "X"; 11 | 12 | impl RustSourceItemPath { 13 | /// Demangles a string representing a module path and item name, splitting them into separate parts. 14 | pub fn from_mangled(string: &str, default_module_path: &str) -> Self { 15 | let demangled = demangle_str(string); 16 | let mut parts = demangled 17 | .as_ref() 18 | .split("::") 19 | .collect::>(); 20 | 21 | let (mod_path, item) = if parts.len() == 1 { 22 | (default_module_path.into(), parts[0]) 23 | } else { 24 | let item = parts.pop().unwrap(); 25 | let mod_path = parts.join("::"); 26 | (mod_path.into(), item) 27 | }; 28 | 29 | Self { 30 | module: mod_path, 31 | name: item.into(), 32 | } 33 | } 34 | } 35 | 36 | fn undecorate_regex() -> &'static Regex { 37 | static MEM: OnceLock = OnceLock::new(); 38 | 39 | MEM.get_or_init(|| { 40 | // https://github.com/bevyengine/naga_oil/blob/master/src/compose/mod.rs#L355-L363 41 | Regex::new( 42 | format!( 43 | r"(\x1B\[\d+\w)?([\w\d_]+){}([A-Z0-9]*){}", 44 | regex_syntax::escape(DECORATION_PRE), 45 | regex_syntax::escape(DECORATION_POST) 46 | ) 47 | .as_str(), 48 | ) 49 | .unwrap() 50 | }) 51 | } 52 | 53 | // https://github.com/bevyengine/naga_oil/blob/master/src/compose/mod.rs#L417-L419 54 | fn decode(from: &str) -> String { 55 | String::from_utf8(data_encoding::BASE32_NOPAD.decode(from.as_bytes()).unwrap()).unwrap() 56 | } 57 | 58 | pub fn escape_os_path(path: &str) -> String { 59 | path.replace("\"", "") 60 | } 61 | 62 | /// Converts 63 | /// * "\"../types\"::RtsStruct" => "types::RtsStruct" 64 | /// * "../more-shader-files/reachme" => "reachme" 65 | pub fn make_valid_rust_import(value: &str) -> String { 66 | let v = value.replace("\"../", "").replace("\"", ""); 67 | std::path::Path::new(&v) 68 | .file_stem() 69 | .and_then(|name| name.to_str()) 70 | .unwrap_or(&v) 71 | .to_string() 72 | } 73 | 74 | // https://github.com/bevyengine/naga_oil/blob/master/src/compose/mod.rs#L421-L431 75 | pub fn demangle_str(string: &str) -> Cow { 76 | undecorate_regex().replace_all(string, |caps: ®ex::Captures| { 77 | format!( 78 | "{}{}::{}", 79 | caps.get(1).map(|cc| cc.as_str()).unwrap_or(""), 80 | make_valid_rust_import(&decode(caps.get(3).unwrap().as_str())), 81 | caps.get(2).unwrap().as_str() 82 | ) 83 | }) 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use pretty_assertions::assert_eq; 89 | 90 | use crate::bevy_util::make_valid_rust_import; 91 | use crate::quote_gen::RustSourceItemPath; 92 | 93 | #[test] 94 | fn test_make_valid_rust_import() { 95 | assert_eq!(make_valid_rust_import("\"../types\"::RtsStruct"), "types::RtsStruct"); 96 | assert_eq!(make_valid_rust_import("../more-shader-files/reachme"), "reachme"); 97 | } 98 | 99 | #[test] 100 | fn test_demangle_mod_names() { 101 | assert_eq!( 102 | RustSourceItemPath::from_mangled("SnehaDataX_naga_oil_mod_XOM5DU5DZOBSXGX", ""), 103 | RustSourceItemPath { 104 | module: "s::types".into(), 105 | name: "SnehaData".into() 106 | } 107 | ); 108 | 109 | assert_eq!( 110 | RustSourceItemPath::from_mangled("UniformsX_naga_oil_mod_XOR4XAZLTX", ""), 111 | RustSourceItemPath { 112 | module: "types".into(), 113 | name: "Uniforms".into() 114 | } 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/parse_imports.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use std::sync::OnceLock; 3 | 4 | use indexmap::IndexMap; 5 | use regex::Regex; 6 | 7 | use crate::{FxIndexSet, ImportPathPart, SourceLocation}; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub struct ImportStatement { 11 | pub source_location: SourceLocation, 12 | pub item_to_import_paths: IndexMap>, 13 | } 14 | 15 | impl ImportStatement { 16 | pub fn range(&self) -> Range { 17 | let start = self.source_location.offset; 18 | let end = start + self.source_location.length; 19 | start..end 20 | } 21 | 22 | pub fn get_import_path_parts(&self) -> FxIndexSet { 23 | self 24 | .item_to_import_paths 25 | .values() 26 | .flatten() 27 | .map(ImportPathPart::new) 28 | .collect() 29 | } 30 | } 31 | 32 | fn import_prefix_regex() -> &'static Regex { 33 | static MEM: OnceLock = OnceLock::new(); 34 | MEM.get_or_init(|| Regex::new(r"(?m)^\s*(#import)").expect("Failed to compile regex")) 35 | } 36 | 37 | fn parse_import_stmt(input: &str) -> IndexMap> { 38 | let mut declared_imports = IndexMap::default(); 39 | naga_oil::compose::parse_imports::parse_imports(input, &mut declared_imports) 40 | .expect(format!("failed to parse imports: '{}'", input).as_str()); 41 | declared_imports 42 | } 43 | 44 | fn build_newline_offsets(content: &str) -> Vec { 45 | let mut line_starts = vec![]; 46 | for (offset, c) in content.char_indices() { 47 | if c == '\n' { 48 | line_starts.push(offset + 1) 49 | } 50 | } 51 | line_starts 52 | } 53 | 54 | fn get_line_and_column(offset: usize, newline_offsets: &[usize]) -> (usize, usize) { 55 | let line_idx = newline_offsets.partition_point(|&x| x <= offset); 56 | let line_start = if line_idx == 0 { 57 | 0 58 | } else { 59 | newline_offsets[line_idx - 1] 60 | }; 61 | (line_idx, offset - line_start + 1) 62 | } 63 | 64 | pub(crate) fn parse_import_statements_iter( 65 | wgsl_content: &str, 66 | ) -> impl Iterator + '_ { 67 | let mut start = 0; 68 | let line_offsets = build_newline_offsets(wgsl_content); 69 | 70 | std::iter::from_fn(move || { 71 | if let Some(c) = import_prefix_regex().captures(&wgsl_content[start..]) { 72 | let m = c.get(1).unwrap(); 73 | let pos = m.start(); 74 | let mut end = start + m.end(); 75 | 76 | let mut brace_level = 0; 77 | let mut in_quotes = false; 78 | let mut prev_char = '\0'; 79 | 80 | while let Some((i, c)) = wgsl_content[end..].char_indices().next() { 81 | match c { 82 | '{' if !in_quotes => brace_level += 1, 83 | '}' if !in_quotes => brace_level -= 1, 84 | '"' if prev_char != '\\' => in_quotes = !in_quotes, 85 | '\n' if !in_quotes && brace_level == 0 => { 86 | end += i; 87 | break; 88 | } 89 | _ => {} 90 | } 91 | prev_char = c; 92 | end += c.len_utf8(); 93 | } 94 | let range = start + pos..end; 95 | let (line_number, line_position) = get_line_and_column(start + pos, &line_offsets); 96 | 97 | // advance the cursor 98 | start = end; 99 | 100 | let source_location = SourceLocation { 101 | line_number, 102 | line_position, 103 | length: range.len(), 104 | offset: range.start, 105 | }; 106 | 107 | let item_to_module_paths = parse_import_stmt(&wgsl_content[range.clone()]); 108 | 109 | let import_stmt = ImportStatement { 110 | source_location, 111 | item_to_import_paths: item_to_module_paths, 112 | }; 113 | 114 | Some(import_stmt) 115 | } else { 116 | None 117 | } 118 | }) 119 | } 120 | 121 | pub fn get_import_statements>(content: &str) -> B { 122 | parse_import_statements_iter(content).collect::() 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use pretty_assertions::{assert_eq, assert_str_eq}; 128 | use smallvec::{smallvec, SmallVec}; 129 | 130 | use super::*; 131 | 132 | const TEST_IMPORTS: &'static str = r#" 133 | #import a::b::{c::{d, e}, f, g::{h as i, j}} 134 | #import a::b c, d 135 | #import a, b 136 | #import "path//with\ all sorts of .stuff"::{a, b} 137 | #import a::b::{ 138 | c::{d, e}, 139 | f, 140 | g::{ 141 | h as i, 142 | j::k::l as m, 143 | } 144 | } 145 | "#; 146 | 147 | fn create_index_map(values: Vec<(&str, Vec<&str>)>) -> IndexMap> { 148 | let mut m = IndexMap::default(); 149 | for (k, v) in values { 150 | let _ = m.insert(k.to_string(), v.into_iter().map(String::from).collect()); 151 | } 152 | m 153 | } 154 | 155 | #[test] 156 | fn test_parsing_from_contents() { 157 | let test_imports = TEST_IMPORTS.replace("\r\n", "\n").replace("\r", "\n"); 158 | let actual = parse_import_statements_iter(&test_imports) 159 | .collect::>(); 160 | 161 | let expected: SmallVec<[ImportStatement; 4]> = smallvec![ 162 | ImportStatement { 163 | source_location: SourceLocation { 164 | line_number: 1, 165 | line_position: 1, 166 | offset: 1, 167 | length: 44, 168 | }, 169 | item_to_import_paths: create_index_map(vec![ 170 | ("d", vec!["a::b::c::d"]), 171 | ("e", vec!["a::b::c::e"]), 172 | ("f", vec!["a::b::f"]), 173 | ("i", vec!["a::b::g::h"]), 174 | ("j", vec!["a::b::g::j",]), 175 | ]), 176 | }, 177 | ImportStatement { 178 | source_location: SourceLocation { 179 | line_number: 2, 180 | line_position: 1, 181 | offset: 46, 182 | length: 17, 183 | }, 184 | item_to_import_paths: create_index_map(vec![ 185 | ("c", vec!["a::b::c"]), 186 | ("d", vec!["a::b::d"]), 187 | ]), 188 | }, 189 | ImportStatement { 190 | source_location: SourceLocation { 191 | line_number: 3, 192 | line_position: 1, 193 | offset: 64, 194 | length: 12, 195 | }, 196 | item_to_import_paths: create_index_map(vec![("a", vec!["a"]), ("b", vec!["b"]),]), 197 | }, 198 | ImportStatement { 199 | source_location: SourceLocation { 200 | line_number: 4, 201 | line_position: 1, 202 | offset: 77, 203 | length: 49, 204 | }, 205 | item_to_import_paths: create_index_map(vec![ 206 | ("a", vec!["\"path//with\\ all sorts of .stuff\"::a"]), 207 | ("b", vec!["\"path//with\\ all sorts of .stuff\"::b"]), 208 | ]), 209 | }, 210 | ImportStatement { 211 | source_location: SourceLocation { 212 | line_number: 5, 213 | line_position: 1, 214 | offset: 127, 215 | length: 95, 216 | }, 217 | item_to_import_paths: create_index_map(vec![ 218 | ("d", vec!["a::b::c::d"]), 219 | ("e", vec!["a::b::c::e"]), 220 | ("f", vec!["a::b::f"]), 221 | ("i", vec!["a::b::g::h"]), 222 | ("m", vec!["a::b::g::j::k::l"]), 223 | ]), 224 | } 225 | ]; 226 | 227 | assert_eq!(expected, actual); 228 | 229 | assert_str_eq!("#import a::b c, d", &test_imports[actual[1].range()]); 230 | } 231 | 232 | #[test] 233 | fn test_parsing_imports_from_bevy_mesh_view_bindings() { 234 | let contents = 235 | include_str!("../../tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl"); 236 | let actual = parse_import_statements_iter(contents) 237 | .flat_map(|x| x.get_import_path_parts()) 238 | .collect::>(); 239 | 240 | assert_eq!(vec![ImportPathPart::new("bevy_pbr::mesh_view_types")], actual); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bevy_util/source_file.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | 3 | use super::parse_imports; 4 | use super::parse_imports::ImportStatement; 5 | use crate::types::{FxIndexSet, SourceFilePath}; 6 | use crate::{ImportPathPart, SourceModuleName}; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub struct SourceFile { 10 | pub file_path: SourceFilePath, 11 | pub module_name: Option, 12 | pub content: String, 13 | pub imports: SmallVec<[ImportStatement; 4]>, 14 | pub direct_dependencies: FxIndexSet, 15 | } 16 | 17 | impl SourceFile { 18 | pub fn create( 19 | file_path: SourceFilePath, 20 | module_name: Option, 21 | content: String, 22 | ) -> Self { 23 | let normalized_content = content.replace("\r\n", "\n").replace("\r", "\n"); 24 | let mut source = Self { 25 | file_path, 26 | module_name, 27 | content: normalized_content, 28 | imports: SmallVec::default(), 29 | direct_dependencies: FxIndexSet::default(), 30 | }; 31 | 32 | source.imports = 33 | parse_imports::get_import_statements::>(&source.content.as_ref()); 34 | source 35 | } 36 | 37 | pub fn add_direct_dependency(&mut self, dependency: SourceFilePath) { 38 | self.direct_dependencies.insert(dependency); 39 | } 40 | 41 | pub fn get_import_path_parts(&self) -> FxIndexSet { 42 | self 43 | .imports 44 | .iter() 45 | .flat_map(|import_stmt| import_stmt.get_import_path_parts()) 46 | .collect() 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use indexmap::indexset; 53 | use pretty_assertions::assert_eq; 54 | 55 | use super::*; 56 | 57 | #[test] 58 | fn test_parsing_imports_from_bevy_mesh_view_bindings() { 59 | let module_name = Some(SourceModuleName::new("bevy_pbr::mesh_view_bindings")); 60 | let source_path = SourceFilePath::new("mesh_view_bindings.wgsl"); 61 | let source = SourceFile::create( 62 | source_path, 63 | module_name, 64 | include_str!("../../tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl") 65 | .to_owned(), 66 | ); 67 | let actual = source.get_import_path_parts(); 68 | 69 | assert_eq!( 70 | actual, 71 | indexset! { 72 | ImportPathPart::new("bevy_pbr::mesh_view_types") 73 | } 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bindgen/bindgen.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use naga_oil::compose::{ 4 | ComposableModuleDescriptor, Composer, ComposerError, NagaModuleDescriptor, 5 | ShaderLanguage, 6 | }; 7 | 8 | use crate::bevy_util::source_file::SourceFile; 9 | use crate::bevy_util::DependencyTree; 10 | use crate::{ 11 | create_rust_bindings, SourceFilePath, SourceWithFullDependenciesResult, 12 | WgslBindgenError, WgslBindgenOption, WgslEntryResult, WgslShaderIrCapabilities, 13 | }; 14 | 15 | const PKG_VER: &str = env!("CARGO_PKG_VERSION"); 16 | const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 17 | 18 | pub struct WGSLBindgen { 19 | dependency_tree: DependencyTree, 20 | options: WgslBindgenOption, 21 | content_hash: String, 22 | } 23 | 24 | impl WGSLBindgen { 25 | pub(crate) fn new(options: WgslBindgenOption) -> Result { 26 | let entry_points = options 27 | .entry_points 28 | .iter() 29 | .cloned() 30 | .map(SourceFilePath::new) 31 | .collect(); 32 | 33 | let dependency_tree = DependencyTree::try_build( 34 | options.workspace_root.clone(), 35 | options.module_import_root.clone(), 36 | entry_points, 37 | options.additional_scan_dirs.clone(), 38 | )?; 39 | 40 | let content_hash = Self::get_contents_hash(&options, &dependency_tree); 41 | 42 | if options.emit_rerun_if_change { 43 | for file in Self::iter_files_to_watch(&dependency_tree) { 44 | println!("cargo:rerun-if-changed={}", file); 45 | } 46 | } 47 | 48 | Ok(Self { 49 | dependency_tree, 50 | options, 51 | content_hash, 52 | }) 53 | } 54 | 55 | fn iter_files_to_watch(dep_tree: &DependencyTree) -> impl Iterator { 56 | dep_tree 57 | .all_files_including_dependencies() 58 | .into_iter() 59 | .map(|path| path.to_string()) 60 | } 61 | 62 | fn get_contents_hash(options: &WgslBindgenOption, dep_tree: &DependencyTree) -> String { 63 | let mut hasher = blake3::Hasher::new(); 64 | 65 | hasher.update(format!("{:?}", options).as_bytes()); 66 | hasher.update(PKG_VER.as_bytes()); 67 | 68 | for SourceFile { content, .. } in dep_tree.parsed_files() { 69 | hasher.update(content.as_bytes()); 70 | } 71 | 72 | hasher.finalize().to_string() 73 | } 74 | 75 | fn generate_naga_module_for_entry( 76 | ir_capabilities: Option, 77 | entry: SourceWithFullDependenciesResult<'_>, 78 | ) -> Result { 79 | let map_err = |composer: &Composer, err: ComposerError| { 80 | let msg = err.emit_to_string(composer); 81 | WgslBindgenError::NagaModuleComposeError { 82 | entry: entry.source_file.file_path.to_string(), 83 | inner: err.inner, 84 | msg, 85 | } 86 | }; 87 | 88 | let mut composer = match ir_capabilities { 89 | Some(capabilities) => Composer::default().with_capabilities(capabilities), 90 | _ => Composer::default(), 91 | }; 92 | let source = entry.source_file; 93 | 94 | for dependency in entry.full_dependencies.iter() { 95 | composer 96 | .add_composable_module(ComposableModuleDescriptor { 97 | source: &dependency.content, 98 | file_path: &dependency.file_path.to_string(), 99 | language: ShaderLanguage::Wgsl, 100 | as_name: dependency.module_name.as_ref().map(|name| name.to_string()), 101 | ..Default::default() 102 | }) 103 | .map(|_| ()) 104 | .map_err(|err| map_err(&composer, err))?; 105 | } 106 | 107 | let module = composer 108 | .make_naga_module(NagaModuleDescriptor { 109 | source: &source.content, 110 | file_path: &source.file_path.to_string(), 111 | ..Default::default() 112 | }) 113 | .map_err(|err| map_err(&composer, err))?; 114 | 115 | Ok(WgslEntryResult { 116 | mod_name: source.file_path.file_prefix(), 117 | naga_module: module, 118 | source_including_deps: entry, 119 | }) 120 | } 121 | 122 | pub fn header_texts(&self) -> String { 123 | use std::fmt::Write; 124 | let mut text = String::new(); 125 | if !self.options.skip_header_comments { 126 | writeln!(text, "// File automatically generated by {PKG_NAME}^").unwrap(); 127 | writeln!(text, "//").unwrap(); 128 | writeln!(text, "// ^ {PKG_NAME} version {PKG_VER}",).unwrap(); 129 | writeln!(text, "// Changes made to this file will not be saved.").unwrap(); 130 | writeln!(text, "// SourceHash: {}", self.content_hash).unwrap(); 131 | writeln!(text).unwrap(); 132 | } 133 | text 134 | } 135 | 136 | fn generate_output(&self) -> Result { 137 | let ir_capabilities = self.options.ir_capabilities; 138 | let entry_results = self 139 | .dependency_tree 140 | .get_source_files_with_full_dependencies() 141 | .into_iter() 142 | .map(|it| Self::generate_naga_module_for_entry(ir_capabilities, it)) 143 | .collect::, _>>()?; 144 | 145 | Ok(create_rust_bindings(entry_results, &self.options)?) 146 | } 147 | 148 | pub fn generate_string(&self) -> Result { 149 | let mut text = self.header_texts(); 150 | text += &self.generate_output()?; 151 | Ok(text) 152 | } 153 | 154 | pub fn generate(&self) -> Result<(), WgslBindgenError> { 155 | let out = self 156 | .options 157 | .output 158 | .as_ref() 159 | .ok_or(WgslBindgenError::OutputFileNotSpecified)?; 160 | 161 | let old_content = std::fs::read_to_string(out).unwrap_or_else(|_| String::new()); 162 | 163 | let old_hashstr_comment = old_content 164 | .lines() 165 | .find(|line| line.starts_with("// SourceHash:")) 166 | .unwrap_or(""); 167 | 168 | let is_hash_changed = 169 | || old_hashstr_comment != format!("// SourceHash: {}", &self.content_hash); 170 | 171 | if self.options.skip_hash_check || is_hash_changed() { 172 | let content = self.generate_string()?; 173 | std::fs::File::create(out)?.write_all(content.as_bytes())? 174 | } 175 | 176 | Ok(()) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bindgen/errors.rs: -------------------------------------------------------------------------------- 1 | use miette::Diagnostic; 2 | use thiserror::Error; 3 | 4 | use crate::bevy_util::DependencyTreeError; 5 | use crate::{CreateModuleError, WgslBindgenOptionBuilderError}; 6 | 7 | /// Enum representing the possible errors that can occur in the `wgsl_bindgen` process. 8 | /// 9 | /// This enum is used to represent all the different kinds of errors that can occur 10 | /// when parsing WGSL shaders, generating Rust bindings, or performing other operations 11 | /// in `wgsl_bindgen`. 12 | #[derive(Debug, Error, Diagnostic)] 13 | pub enum WgslBindgenError { 14 | #[error("All required fields need to be set upfront: {0}")] 15 | OptionBuilderError(#[from] WgslBindgenOptionBuilderError), 16 | 17 | #[error(transparent)] 18 | #[diagnostic(transparent)] 19 | DependencyTreeError(#[from] DependencyTreeError), 20 | 21 | #[error("Failed to compose modules with entry `{entry}`\n{msg}")] 22 | NagaModuleComposeError { 23 | entry: String, 24 | msg: String, 25 | inner: naga_oil::compose::ComposerErrorInner, 26 | }, 27 | 28 | #[error(transparent)] 29 | ModuleCreationError(#[from] CreateModuleError), 30 | 31 | #[error(transparent)] 32 | WriteOutputError(#[from] std::io::Error), 33 | 34 | #[error("Output file is not specified. Maybe use `generate_string` instead")] 35 | OutputFileNotSpecified, 36 | } 37 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bindgen/mod.rs: -------------------------------------------------------------------------------- 1 | mod bindgen; 2 | mod errors; 3 | mod options; 4 | 5 | pub use bindgen::*; 6 | pub use errors::*; 7 | pub use options::*; 8 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bindgen/options/bindings.rs: -------------------------------------------------------------------------------- 1 | use quote::format_ident; 2 | use syn::Ident; 3 | 4 | use crate::qs::{quote, Index, TokenStream}; 5 | use crate::FastIndexMap; 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] 8 | pub enum BindResourceType { 9 | Buffer, 10 | Sampler, 11 | Texture, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub struct BindingGenerator { 16 | pub bind_group_layout: BindGroupLayoutGenerator, 17 | pub pipeline_layout: PipelineLayoutGenerator, 18 | } 19 | 20 | impl std::fmt::Debug for BindingGenerator { 21 | fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | // skip the debug generation for this, 23 | // as the output changes on every build due to fns 24 | Ok(()) 25 | } 26 | } 27 | 28 | /// Represents a generator for creating WGSL bind group layout structures. 29 | /// 30 | /// This struct is used to generate the code for creating a bind group layout in WGSL. 31 | /// It contains the necessary information for generating the code, such as the prefix name for the layout, 32 | /// whether the generated code uses lifetimes, the type of the entry struct, a function for constructing entries, 33 | /// and a map of binding resource types to their corresponding token streams. 34 | #[derive(Clone, Debug)] 35 | pub struct BindGroupLayoutGenerator { 36 | /// The prefix for the bind group layout. 37 | pub name_prefix: String, 38 | 39 | /// Indicates whether the generated code uses lifetimes. 40 | /// 41 | /// If this is `true`, the generated code will include lifetimes. If it's `false`, the generated code will not include lifetimes. 42 | pub uses_lifetime: bool, 43 | 44 | /// The type of the entry struct in the generated code. 45 | /// 46 | /// This is represented as a `TokenStream` that contains the code for the type of the entry struct. 47 | pub entry_struct_type: TokenStream, 48 | 49 | /// A function for constructing entries in the generated code. 50 | /// 51 | /// This function takes a binding index, a `TokenStream` for the binding variable, and a `WgslBindResourceType` for the resource type, 52 | /// and returns a `TokenStream` that contains the code for constructing an entry. 53 | pub entry_constructor: fn(usize, TokenStream, BindResourceType) -> TokenStream, 54 | 55 | /// A map of binding resource types to their corresponding token streams. 56 | /// 57 | /// This map is used to generate the code for the binding resources in the bind group layout. 58 | pub binding_type_map: FastIndexMap, 59 | } 60 | 61 | impl BindGroupLayoutGenerator { 62 | pub(crate) fn bind_group_name_ident(&self, group_index: u32) -> Ident { 63 | format_ident!("{}{}", self.name_prefix, group_index) 64 | } 65 | 66 | pub(crate) fn bind_group_entries_struct_name_ident(&self, group_index: u32) -> Ident { 67 | format_ident!("{}{}{}", self.name_prefix, group_index, "Entries") 68 | } 69 | } 70 | 71 | #[derive(Clone, Debug)] 72 | pub struct PipelineLayoutGenerator { 73 | pub layout_name: String, 74 | pub bind_group_layout_type: TokenStream, 75 | } 76 | 77 | pub trait GetBindingsGeneratorConfig { 78 | fn get_generator_config(self) -> BindingGenerator; 79 | } 80 | impl GetBindingsGeneratorConfig for BindingGenerator { 81 | fn get_generator_config(self) -> BindingGenerator { 82 | self 83 | } 84 | } 85 | 86 | impl Default for BindingGenerator { 87 | fn default() -> BindingGenerator { 88 | WgpuGetBindingsGeneratorConfig.get_generator_config() 89 | } 90 | } 91 | 92 | pub struct WgpuGetBindingsGeneratorConfig; 93 | impl WgpuGetBindingsGeneratorConfig { 94 | fn get_bind_group_layout_generator_config() -> BindGroupLayoutGenerator { 95 | let binding_type_map = vec![ 96 | (BindResourceType::Buffer, quote! { wgpu::BufferBinding<'a> }), 97 | (BindResourceType::Sampler, quote! { &'a wgpu::Sampler }), 98 | (BindResourceType::Texture, quote! { &'a wgpu::TextureView }), 99 | ] 100 | .into_iter() 101 | .collect::>(); 102 | 103 | fn entry_constructor( 104 | binding: usize, 105 | binding_var: TokenStream, 106 | resource_type: BindResourceType, 107 | ) -> TokenStream { 108 | let resource = match resource_type { 109 | BindResourceType::Buffer => { 110 | quote!(wgpu::BindingResource::Buffer(#binding_var)) 111 | } 112 | BindResourceType::Sampler => { 113 | quote!(wgpu::BindingResource::Sampler(#binding_var)) 114 | } 115 | BindResourceType::Texture => { 116 | quote!(wgpu::BindingResource::TextureView(#binding_var)) 117 | } 118 | }; 119 | 120 | let binding = Index::from(binding); 121 | quote! { 122 | wgpu::BindGroupEntry { 123 | binding: #binding, 124 | resource: #resource, 125 | } 126 | } 127 | } 128 | 129 | BindGroupLayoutGenerator { 130 | name_prefix: "WgpuBindGroup".into(), 131 | uses_lifetime: true, 132 | entry_struct_type: quote!(wgpu::BindGroupEntry<'a>), 133 | entry_constructor, 134 | binding_type_map, 135 | } 136 | } 137 | 138 | fn get_pipeline_layout_generator() -> PipelineLayoutGenerator { 139 | PipelineLayoutGenerator { 140 | layout_name: "WgpuPipelineLayout".into(), 141 | bind_group_layout_type: quote!(wgpu::BindGroupLayout), 142 | } 143 | } 144 | } 145 | 146 | impl GetBindingsGeneratorConfig for WgpuGetBindingsGeneratorConfig { 147 | fn get_generator_config(self) -> BindingGenerator { 148 | let bind_group_layout = Self::get_bind_group_layout_generator_config(); 149 | let pipeline_layout = Self::get_pipeline_layout_generator(); 150 | BindingGenerator { 151 | bind_group_layout, 152 | pipeline_layout, 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/bindgen/options/types.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | 3 | use super::{WgslTypeMap, WgslTypeMapBuild, WgslTypeSerializeStrategy}; 4 | 5 | /// Rust types like `[f32; 4]` or `[[f32; 4]; 4]`. 6 | #[derive(Clone)] 7 | pub struct RustWgslTypeMap; 8 | 9 | impl WgslTypeMapBuild for RustWgslTypeMap { 10 | fn build(&self, _: WgslTypeSerializeStrategy) -> WgslTypeMap { 11 | WgslTypeMap::default() 12 | } 13 | } 14 | 15 | /// `glam` types like `glam::Vec4` or `glam::Mat4`. 16 | /// Types not representable by `glam` like `mat2x3` will use the output from [RustWgslTypeMap]. 17 | #[derive(Clone)] 18 | pub struct GlamWgslTypeMap; 19 | 20 | impl WgslTypeMapBuild for GlamWgslTypeMap { 21 | fn build(&self, serialize_strategy: WgslTypeSerializeStrategy) -> WgslTypeMap { 22 | use crate::WgslMatType::*; 23 | use crate::WgslType::*; 24 | use crate::WgslVecType::*; 25 | let is_encase = serialize_strategy.is_encase(); 26 | let types = if is_encase { 27 | vec![ 28 | (Vector(Vec2i), quote!(glam::IVec2)), 29 | (Vector(Vec3i), quote!(glam::IVec3)), 30 | (Vector(Vec4i), quote!(glam::IVec4)), 31 | (Vector(Vec2u), quote!(glam::UVec2)), 32 | (Vector(Vec3u), quote!(glam::UVec3)), 33 | (Vector(Vec4u), quote!(glam::UVec4)), 34 | (Vector(Vec2f), quote!(glam::Vec2)), 35 | (Vector(Vec3f), quote!(glam::Vec3A)), 36 | (Vector(Vec4f), quote!(glam::Vec4)), 37 | (Matrix(Mat2x2f), quote!(glam::Mat2)), 38 | (Matrix(Mat3x3f), quote!(glam::Mat3A)), 39 | (Matrix(Mat4x4f), quote!(glam::Mat4)), 40 | ] 41 | } else { 42 | vec![ 43 | (Vector(Vec3f), quote!(glam::Vec3A)), 44 | (Vector(Vec4f), quote!(glam::Vec4)), 45 | (Matrix(Mat3x3f), quote!(glam::Mat3A)), 46 | (Matrix(Mat4x4f), quote!(glam::Mat4)), 47 | ] 48 | }; 49 | 50 | types.into_iter().collect() 51 | } 52 | } 53 | 54 | /// `nalgebra` types like `nalgebra::SVector` or `nalgebra::SMatrix`. 55 | #[derive(Clone)] 56 | pub struct NalgebraWgslTypeMap; 57 | 58 | impl WgslTypeMapBuild for NalgebraWgslTypeMap { 59 | fn build(&self, _: WgslTypeSerializeStrategy) -> WgslTypeMap { 60 | use crate::WgslMatType::*; 61 | use crate::WgslType::*; 62 | use crate::WgslVecType::*; 63 | 64 | vec![ 65 | (Vector(Vec2i), quote!(nalgebra::SVector)), 66 | (Vector(Vec3i), quote!(nalgebra::SVector)), 67 | (Vector(Vec4i), quote!(nalgebra::SVector)), 68 | (Vector(Vec2u), quote!(nalgebra::SVector)), 69 | (Vector(Vec3u), quote!(nalgebra::SVector)), 70 | (Vector(Vec4u), quote!(nalgebra::SVector)), 71 | (Vector(Vec2f), quote!(nalgebra::SVector)), 72 | (Vector(Vec3f), quote!(nalgebra::SVector)), 73 | (Vector(Vec4f), quote!(nalgebra::SVector)), 74 | (Matrix(Mat2x2f), quote!(nalgebra::SMatrix)), 75 | (Matrix(Mat2x3f), quote!(nalgebra::SMatrix)), 76 | (Matrix(Mat2x4f), quote!(nalgebra::SMatrix)), 77 | (Matrix(Mat3x2f), quote!(nalgebra::SMatrix)), 78 | (Matrix(Mat3x3f), quote!(nalgebra::SMatrix)), 79 | (Matrix(Mat3x4f), quote!(nalgebra::SMatrix)), 80 | (Matrix(Mat4x2f), quote!(nalgebra::SMatrix)), 81 | (Matrix(Mat4x3f), quote!(nalgebra::SMatrix)), 82 | (Matrix(Mat4x4f), quote!(nalgebra::SMatrix)), 83 | ] 84 | .into_iter() 85 | .collect() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/generate/bind_group/raw_shader_bind_group.rs: -------------------------------------------------------------------------------- 1 | use std::collections::btree_map::Entry; 2 | use std::collections::BTreeMap; 3 | 4 | use naga::FastIndexMap; 5 | use smol_str::SmolStr; 6 | 7 | use super::single_bind_group::SingleBindGroupEntry; 8 | use crate::bind_group::{ 9 | CommonShaderBindGroups, ReusableShaderBindGroups, ShaderBindGroupRef, 10 | ShaderBindGroupRefKind, ShaderEntryBindGroups, SingleBindGroupData, 11 | }; 12 | use crate::{CreateModuleError, WgslBindgenOption}; 13 | 14 | pub struct RawShaderEntryBindGroups<'a> { 15 | pub containing_module: SmolStr, 16 | pub shader_stages: wgpu::ShaderStages, 17 | pub bind_group_data: BTreeMap>, 18 | } 19 | 20 | pub struct RawShadersBindGroups<'a> { 21 | entrypoint_bindgroups: FastIndexMap>, 22 | options: &'a WgslBindgenOption, 23 | } 24 | 25 | impl<'a> RawShadersBindGroups<'a> { 26 | pub fn new(options: &'a WgslBindgenOption) -> Self { 27 | Self { 28 | entrypoint_bindgroups: FastIndexMap::default(), 29 | options, 30 | } 31 | } 32 | 33 | pub fn add(&mut self, mut shader: RawShaderEntryBindGroups<'a>) { 34 | // patch up the module name for bind group entrys 35 | for entry in shader 36 | .bind_group_data 37 | .values_mut() 38 | .flat_map(|v| v.bindings.iter_mut()) 39 | { 40 | let target_patch_module = self 41 | .options 42 | .override_bind_group_entry_module_path 43 | .iter() 44 | .find_map(|o| { 45 | let matched = o 46 | .bind_group_entry_regex 47 | .is_match(&entry.item_path.get_fully_qualified_name()); 48 | matched.then_some(SmolStr::new(o.target_path.clone())) 49 | }); 50 | 51 | if let Some(target_patch_module) = target_patch_module { 52 | entry.item_path.module = target_patch_module; 53 | } 54 | } 55 | 56 | self 57 | .entrypoint_bindgroups 58 | .insert(shader.containing_module.clone(), shader); 59 | } 60 | 61 | pub fn create_reusable_shader_bind_groups(self) -> ReusableShaderBindGroups<'a> { 62 | fn merge_bind_groups<'a>( 63 | existing_group: &SingleBindGroupData<'a>, 64 | new_group: &SingleBindGroupData<'a>, 65 | ) -> SingleBindGroupData<'a> { 66 | let mut merged_bindings = existing_group.bindings.clone(); 67 | for binding in new_group.bindings.iter() { 68 | merged_bindings.push(binding.clone()); 69 | } 70 | merged_bindings.sort_by(|a, b| a.binding_index.cmp(&b.binding_index)); 71 | merged_bindings.dedup_by(|a, b| { 72 | a.binding_index == b.binding_index 73 | && a.item_path == b.item_path 74 | && a.name == b.name 75 | }); 76 | SingleBindGroupData { 77 | bindings: merged_bindings, 78 | } 79 | } 80 | 81 | // Create a common binding group for all shaders. 82 | let mut common_bind_groups = BTreeMap::new(); 83 | for shader in self.entrypoint_bindgroups.values() { 84 | for (&group_no, group) in &shader.bind_group_data { 85 | // Check if all entry have the same module. 86 | let first_module = group.first_module(); 87 | let all_same_module = group.are_all_same_module(); 88 | 89 | // if all the bindings are in the same module, and of this shader, skip it. 90 | if all_same_module && first_module == shader.containing_module { 91 | continue; 92 | } 93 | 94 | match common_bind_groups.entry(group_no) { 95 | Entry::Vacant(vacant_entry) => { 96 | vacant_entry.insert((shader.shader_stages, group.clone())); 97 | } 98 | Entry::Occupied(mut occupied_entry) => { 99 | let merged_group = merge_bind_groups(&occupied_entry.get().1, group); 100 | let merged_stages = occupied_entry.get().0 | shader.shader_stages; 101 | occupied_entry.insert((merged_stages, merged_group)); 102 | } 103 | }; 104 | } 105 | } 106 | 107 | // Remove all the bind groups that are not reusable. 108 | common_bind_groups.retain(|_, (_, group)| group.are_all_same_module()); 109 | 110 | // Create the reusable shader bind groups 111 | let mut reusable_shader_bind_groups = ReusableShaderBindGroups::new(); 112 | for (&group_no, (_, group)) in &common_bind_groups { 113 | let common_module = group.first_module(); 114 | 115 | reusable_shader_bind_groups.common_bind_groups.insert( 116 | common_module.clone(), 117 | CommonShaderBindGroups { 118 | containing_module: common_module, 119 | bind_group_data: BTreeMap::from([(group_no, group.clone())]), 120 | }, 121 | ); 122 | } 123 | 124 | // Add the shader entries to the reusable shader bind groups 125 | for (_, shader) in &self.entrypoint_bindgroups { 126 | // force create an entry even though bind groups might be empty. 127 | // this is required for other parts of the pipeline to work 128 | let shader_entry_bindgroups = reusable_shader_bind_groups 129 | .entrypoint_bindgroups 130 | .entry(shader.containing_module.clone()) 131 | .or_insert_with(|| ShaderEntryBindGroups { 132 | containing_module: shader.containing_module.clone(), 133 | shader_stages: shader.shader_stages, 134 | bind_group_ref: BTreeMap::new(), 135 | original_bind_group: shader.bind_group_data.clone(), 136 | }); 137 | 138 | for (group_no, group) in &shader.bind_group_data { 139 | let common_bindgroup = common_bind_groups.get(group_no).map(|(_, group)| group); 140 | let is_common = Some(group.first_module()) 141 | == common_bindgroup.map(|group| group.first_module()); 142 | let reusable_bindgroup = is_common.then(|| common_bindgroup).flatten(); 143 | 144 | if let Some(reusable_bindgroup) = reusable_bindgroup { 145 | shader_entry_bindgroups.bind_group_ref.insert( 146 | *group_no, 147 | ShaderBindGroupRef { 148 | kind: ShaderBindGroupRefKind::Common, 149 | data: reusable_bindgroup.clone(), 150 | }, 151 | ); 152 | } else { 153 | shader_entry_bindgroups.bind_group_ref.insert( 154 | *group_no, 155 | ShaderBindGroupRef { 156 | kind: ShaderBindGroupRefKind::Entrypoint, 157 | data: group.clone(), 158 | }, 159 | ); 160 | } 161 | } 162 | } 163 | 164 | reusable_shader_bind_groups 165 | } 166 | } 167 | 168 | pub fn get_bind_group_data_for_entry<'a>( 169 | module: &'a naga::Module, 170 | shader_stages: wgpu::ShaderStages, 171 | options: &WgslBindgenOption, 172 | module_name: &'a str, 173 | ) -> Result, CreateModuleError> { 174 | // Use a BTree to sort type and field names by group index. 175 | // This isn't strictly necessary but makes the generated code cleaner. 176 | let mut bind_group_data = BTreeMap::new(); 177 | 178 | for global_handle in module.global_variables.iter() { 179 | let global = &module.global_variables[global_handle.0]; 180 | if let Some(binding) = &global.binding { 181 | let group = bind_group_data 182 | .entry(binding.group) 183 | .or_insert(SingleBindGroupData { 184 | bindings: Vec::new(), 185 | }); 186 | let binding_type = &module.types[module.global_variables[global_handle.0].ty]; 187 | 188 | let group_binding = SingleBindGroupEntry::new( 189 | global.name.clone(), 190 | module_name, 191 | options, 192 | module, 193 | shader_stages, 194 | binding.binding, 195 | binding_type, 196 | global.space, 197 | ); 198 | 199 | // Repeated bindings will probably cause a compile error. 200 | // We'll still check for it here just in case. 201 | if group 202 | .bindings 203 | .iter() 204 | .any(|g| g.binding_index == binding.binding) 205 | { 206 | return Err(CreateModuleError::DuplicateBinding { 207 | binding: binding.binding, 208 | }); 209 | } 210 | group.bindings.push(group_binding); 211 | } 212 | } 213 | 214 | // wgpu expects bind groups to be consecutive starting from 0. 215 | if bind_group_data 216 | .keys() 217 | .map(|i| *i as usize) 218 | .eq(0..bind_group_data.len()) 219 | { 220 | Ok(RawShaderEntryBindGroups { 221 | containing_module: module_name.into(), 222 | shader_stages, 223 | bind_group_data: bind_group_data.clone(), 224 | }) 225 | } else { 226 | Err(CreateModuleError::NonConsecutiveBindGroups) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/generate/consts.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::Ident; 4 | 5 | use crate::quote_gen::{ 6 | rust_type, RustSourceItem, RustSourceItemCategory, RustSourceItemPath, 7 | }; 8 | use crate::WgslBindgenOption; 9 | 10 | pub fn consts_items( 11 | invoking_entry_module: &str, 12 | module: &naga::Module, 13 | ) -> Vec { 14 | // Create matching Rust constants for WGSl constants. 15 | module 16 | .constants 17 | .iter() 18 | .filter_map(|(_, t)| -> Option { 19 | let name_str = t.name.as_ref()?; 20 | 21 | // we don't need full qualification here 22 | let rust_item_path = 23 | RustSourceItemPath::from_mangled(name_str, invoking_entry_module); 24 | let name = Ident::new(&rust_item_path.name, Span::call_site()); 25 | 26 | // TODO: Add support for f64 and f16 once naga supports them. 27 | let type_and_value = match &module.global_expressions[t.init] { 28 | naga::Expression::Literal(literal) => match literal { 29 | naga::Literal::F64(v) => Some(quote!(f32 = #v)), 30 | naga::Literal::F32(v) => Some(quote!(f32 = #v)), 31 | naga::Literal::U32(v) => Some(quote!(u32 = #v)), 32 | naga::Literal::U64(v) => Some(quote!(u64 = #v)), 33 | naga::Literal::I32(v) => Some(quote!(i32 = #v)), 34 | naga::Literal::Bool(v) => Some(quote!(bool = #v)), 35 | naga::Literal::I64(v) => Some(quote!(i64 = #v)), 36 | naga::Literal::AbstractInt(v) => Some(quote!(i64 = #v)), 37 | naga::Literal::AbstractFloat(v) => Some(quote!(f64 = #v)), 38 | }, 39 | _ => None, 40 | }?; 41 | 42 | Some(RustSourceItem::new( 43 | RustSourceItemCategory::ConstVarDecls.into(), 44 | rust_item_path, 45 | quote! { pub const #name: #type_and_value;}, 46 | )) 47 | }) 48 | .collect() 49 | } 50 | 51 | pub fn pipeline_overridable_constants( 52 | module: &naga::Module, 53 | options: &WgslBindgenOption, 54 | ) -> TokenStream { 55 | let overrides: Vec<_> = module.overrides.iter().map(|(_, o)| o).collect(); 56 | 57 | let fields: Vec<_> = overrides 58 | .iter() 59 | .map(|o| { 60 | let name = Ident::new(o.name.as_ref().unwrap(), Span::call_site()); 61 | // TODO: Do we only need to handle scalar types here? 62 | let ty = rust_type(None, module, &module.types[o.ty], options); 63 | 64 | if o.init.is_some() { 65 | quote!(pub #name: Option<#ty>) 66 | } else { 67 | quote!(pub #name: #ty) 68 | } 69 | }) 70 | .collect(); 71 | 72 | let required_entries: Vec<_> = overrides 73 | .iter() 74 | .filter_map(|o| { 75 | if o.init.is_some() { 76 | None 77 | } else { 78 | let key = override_key(o); 79 | 80 | let name = Ident::new(o.name.as_ref().unwrap(), Span::call_site()); 81 | 82 | // TODO: Do we only need to handle scalar types here? 83 | let ty = &module.types[o.ty]; 84 | let value = if matches!(ty.inner, naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Bool) { 85 | quote!(if self.#name { 1.0 } else { 0.0}) 86 | } else { 87 | quote!(self.#name as f64) 88 | }; 89 | 90 | Some(quote!((#key.to_owned(), #value))) 91 | } 92 | }) 93 | .collect(); 94 | 95 | // Add code for optionally inserting the constants with defaults. 96 | // Omitted constants will be initialized using the values defined in WGSL. 97 | let insert_optional_entries: Vec<_> = overrides 98 | .iter() 99 | .filter_map(|o| { 100 | if o.init.is_some() { 101 | let key = override_key(o); 102 | 103 | // TODO: Do we only need to handle scalar types here? 104 | let ty = &module.types[o.ty]; 105 | let value = if matches!(ty.inner, naga::TypeInner::Scalar(s) if s.kind == naga::ScalarKind::Bool) { 106 | quote!(if value { 1.0 } else { 0.0}) 107 | } else { 108 | quote!(value as f64) 109 | }; 110 | 111 | let name = Ident::new(o.name.as_ref().unwrap(), Span::call_site()); 112 | 113 | Some(quote! { 114 | if let Some(value) = self.#name { 115 | entries.insert(#key.to_owned(), #value); 116 | } 117 | }) 118 | } else { 119 | None 120 | } 121 | }) 122 | .collect(); 123 | 124 | let init_entries = if insert_optional_entries.is_empty() { 125 | quote!(let entries = std::collections::HashMap::from([#(#required_entries),*]);) 126 | } else { 127 | quote!(let mut entries = std::collections::HashMap::from([#(#required_entries),*]);) 128 | }; 129 | 130 | if !fields.is_empty() { 131 | // Create a Rust struct that can initialize the constants dictionary. 132 | quote! { 133 | pub struct OverrideConstants { 134 | #(#fields),* 135 | } 136 | 137 | // TODO: Only start with the required ones. 138 | impl OverrideConstants { 139 | pub fn constants(&self) -> std::collections::HashMap { 140 | #init_entries 141 | #(#insert_optional_entries);* 142 | entries 143 | } 144 | } 145 | } 146 | } else { 147 | quote!() 148 | } 149 | } 150 | 151 | fn override_key(o: &naga::Override) -> String { 152 | // The @id(id) should be the name if present. 153 | o.id 154 | .map(|i| i.to_string()) 155 | .unwrap_or(o.name.clone().unwrap()) 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use indoc::indoc; 161 | use proc_macro2::TokenStream; 162 | 163 | use super::*; 164 | use crate::assert_tokens_eq; 165 | 166 | fn consts(module: &naga::Module) -> Vec { 167 | consts_items("", module) 168 | .into_iter() 169 | .map(|i| i.tokenstream) 170 | .collect() 171 | } 172 | #[test] 173 | fn write_global_constants() { 174 | let source = indoc! {r#" 175 | const INT_CONST = 12; 176 | const UNSIGNED_CONST = 34u; 177 | const FLOAT_CONST = 0.1; 178 | // TODO: Naga doesn't implement f16, even though it's in the WGSL spec 179 | // const SMALL_FLOAT_CONST:f16 = 0.1h; 180 | const BOOL_CONST = true; 181 | 182 | @fragment 183 | fn main() { 184 | // TODO: This is valid WGSL syntax, but naga doesn't support it apparently. 185 | // const C_INNER = 456; 186 | } 187 | "#}; 188 | 189 | let module = naga::front::wgsl::parse_str(source).unwrap(); 190 | 191 | let consts = consts(&module); 192 | let actual = quote!(#(#consts)*); 193 | eprintln!("{actual}"); 194 | 195 | assert_tokens_eq!( 196 | quote! { 197 | pub const INT_CONST: i32 = 12i32; 198 | pub const UNSIGNED_CONST: u32 = 34u32; 199 | pub const FLOAT_CONST: f32 = 0.1f32; 200 | pub const BOOL_CONST: bool = true; 201 | }, 202 | actual 203 | ); 204 | } 205 | 206 | #[test] 207 | fn write_pipeline_overrideable_constants() { 208 | let source = indoc! {r#" 209 | override b1: bool = true; 210 | override b2: bool = false; 211 | override b3: bool; 212 | override f1: f32 = 0.5; 213 | override f2: f32; 214 | // override f3: f64 = 0.6; 215 | // override f4: f64; 216 | override i1: i32 = 0; 217 | override i2: i32; 218 | override i3: i32 = i1 * i2; 219 | @id(0) override a: f32 = 1.0; 220 | @id(35) override b: f32 = 2.0; 221 | @fragment 222 | fn main() {} 223 | "#}; 224 | 225 | let module = naga::front::wgsl::parse_str(source).unwrap(); 226 | 227 | let actual = pipeline_overridable_constants(&module, &WgslBindgenOption::default()); 228 | 229 | assert_tokens_eq!( 230 | quote! { 231 | pub struct OverrideConstants { 232 | pub b1: Option, 233 | pub b2: Option, 234 | pub b3: bool, 235 | pub f1: Option, 236 | pub f2: f32, 237 | pub i1: Option, 238 | pub i2: i32, 239 | pub i3: Option, 240 | pub a: Option, 241 | pub b: Option, 242 | } 243 | 244 | impl OverrideConstants { 245 | pub fn constants(&self) -> std::collections::HashMap { 246 | let mut entries = std::collections::HashMap::from([ 247 | ("b3".to_owned(), if self.b3 { 1.0 } else { 0.0 }), 248 | ("f2".to_owned(), self.f2 as f64), 249 | ("i2".to_owned(), self.i2 as f64) 250 | ]); 251 | if let Some(value) = self.b1 { 252 | entries.insert("b1".to_owned(), if value { 1.0 } else { 0.0 }); 253 | } 254 | if let Some(value) = self.b2 { 255 | entries.insert("b2".to_owned(), if value { 1.0 } else { 0.0 }); 256 | } 257 | if let Some(value) = self.f1 { 258 | entries.insert("f1".to_owned(), value as f64); 259 | } 260 | if let Some(value) = self.i1 { 261 | entries.insert("i1".to_owned(), value as f64); 262 | } 263 | if let Some(value) = self.i3 { 264 | entries.insert("i3".to_owned(), value as f64); 265 | } 266 | if let Some(value) = self.a { 267 | entries.insert("0".to_owned(), value as f64); 268 | } 269 | if let Some(value) = self.b { 270 | entries.insert("35".to_owned(), value as f64); 271 | } 272 | entries 273 | } 274 | } 275 | }, 276 | actual 277 | ); 278 | } 279 | 280 | #[test] 281 | fn write_pipeline_overrideable_constants_empty() { 282 | let source = indoc! {r#" 283 | @fragment 284 | fn main() {} 285 | "#}; 286 | 287 | let module = naga::front::wgsl::parse_str(source).unwrap(); 288 | let actual = pipeline_overridable_constants(&module, &WgslBindgenOption::default()); 289 | assert_tokens_eq!(quote!(), actual); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/generate/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub(crate) mod bind_group; 5 | pub(crate) mod consts; 6 | pub(crate) mod entry; 7 | pub(crate) mod pipeline; 8 | pub(crate) mod shader_module; 9 | pub(crate) mod shader_registry; 10 | 11 | pub(crate) fn quote_shader_stages(shader_stages: wgpu::ShaderStages) -> TokenStream { 12 | match shader_stages { 13 | wgpu::ShaderStages::VERTEX_FRAGMENT => quote!(wgpu::ShaderStages::VERTEX_FRAGMENT), 14 | wgpu::ShaderStages::COMPUTE => quote!(wgpu::ShaderStages::COMPUTE), 15 | wgpu::ShaderStages::VERTEX => quote!(wgpu::ShaderStages::VERTEX), 16 | wgpu::ShaderStages::FRAGMENT => quote!(wgpu::ShaderStages::FRAGMENT), 17 | _ => { 18 | let mut stage_tokens = vec![]; 19 | if shader_stages.contains(wgpu::ShaderStages::VERTEX) { 20 | stage_tokens.push(quote!(wgpu::ShaderStages::VERTEX)); 21 | } 22 | if shader_stages.contains(wgpu::ShaderStages::FRAGMENT) { 23 | stage_tokens.push(quote!(wgpu::ShaderStages::FRAGMENT)); 24 | } 25 | if shader_stages.contains(wgpu::ShaderStages::COMPUTE) { 26 | stage_tokens.push(quote!(wgpu::ShaderStages::COMPUTE)); 27 | } 28 | quote!(#(#stage_tokens)|*) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/generate/pipeline.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use derive_more::Constructor; 4 | use generate::quote_shader_stages; 5 | use smol_str::ToSmolStr; 6 | 7 | use super::bind_group::{ShaderEntryBindGroups, SingleBindGroupData}; 8 | use crate::bind_group::ShaderBindGroupRefKind; 9 | use crate::quote_gen::RustSourceItemPath; 10 | use crate::*; 11 | 12 | #[derive(Constructor)] 13 | pub struct PipelineLayoutDataEntriesBuilder<'a> { 14 | generator: &'a PipelineLayoutGenerator, 15 | bind_group_data: &'a BTreeMap>, 16 | } 17 | 18 | impl<'a> PipelineLayoutDataEntriesBuilder<'a> { 19 | fn bind_group_layout_entries_fn(&self) -> TokenStream { 20 | let entry_type = self.generator.bind_group_layout_type.clone(); 21 | let len = Index::from(self.bind_group_data.len()); 22 | 23 | quote! { 24 | pub fn bind_group_layout_entries(entries: [#entry_type; #len]) -> [#entry_type; #len] { 25 | entries 26 | } 27 | } 28 | } 29 | 30 | fn build(&self) -> TokenStream { 31 | let name = format_ident!("{}", self.generator.layout_name); 32 | let bind_group_layout_entries_fn = self.bind_group_layout_entries_fn(); 33 | 34 | quote! { 35 | #[derive(Debug)] 36 | pub struct #name; 37 | 38 | impl #name { 39 | #bind_group_layout_entries_fn 40 | } 41 | } 42 | } 43 | } 44 | 45 | fn push_constant_range( 46 | module: &naga::Module, 47 | shader_stages: wgpu::ShaderStages, 48 | ) -> Option { 49 | // Assume only one variable is used with var in WGSL. 50 | let push_constant_size = module.global_variables.iter().find_map(|g| { 51 | if g.1.space == naga::AddressSpace::PushConstant { 52 | Some(module.types[g.1.ty].inner.size(module.to_ctx())) 53 | } else { 54 | None 55 | } 56 | }); 57 | 58 | let stages = quote_shader_stages(shader_stages); 59 | 60 | // Use a single push constant range for all shader stages. 61 | // This allows easily setting push constants in a single call with offset 0. 62 | let push_constant_range = push_constant_size.map(|size| { 63 | let size = Index::from(size as usize); 64 | quote! { 65 | wgpu::PushConstantRange { 66 | stages: #stages, 67 | range: 0..#size 68 | } 69 | } 70 | }); 71 | push_constant_range 72 | } 73 | 74 | pub fn create_pipeline_layout_fn( 75 | entry_name: &str, 76 | naga_module: &naga::Module, 77 | shader_entry_bind_groups: &ShaderEntryBindGroups, 78 | options: &WgslBindgenOption, 79 | ) -> TokenStream { 80 | let bind_group_layouts: Vec<_> = shader_entry_bind_groups 81 | .bind_group_ref 82 | .iter() 83 | .map(|(&group_no, group_ref)| { 84 | let group_name = options 85 | .wgpu_binding_generator 86 | .bind_group_layout 87 | .bind_group_name_ident(group_no); 88 | 89 | // if all entries have a common module, reference that module instead 90 | let group_name = match group_ref.kind { 91 | ShaderBindGroupRefKind::Common => { 92 | let containing_module = group_ref.data.first_module(); 93 | let path = RustSourceItemPath::new(containing_module, group_name.to_smolstr()); 94 | quote!(#path) 95 | } 96 | ShaderBindGroupRefKind::Entrypoint => quote!(#group_name), 97 | }; 98 | 99 | quote!(#group_name::get_bind_group_layout(device)) 100 | }) 101 | .collect(); 102 | 103 | let wgpu_pipeline_gen = &options.wgpu_binding_generator.pipeline_layout; 104 | let wgpu_pipeline_entries_struct = PipelineLayoutDataEntriesBuilder::new( 105 | wgpu_pipeline_gen, 106 | &shader_entry_bind_groups.original_bind_group, 107 | ) 108 | .build(); 109 | 110 | let additional_pipeline_entries_struct = 111 | if let Some(a) = options.extra_binding_generator.as_ref() { 112 | PipelineLayoutDataEntriesBuilder::new( 113 | &a.pipeline_layout, 114 | &shader_entry_bind_groups.original_bind_group, 115 | ) 116 | .build() 117 | } else { 118 | quote!() 119 | }; 120 | 121 | let push_constant_range = 122 | push_constant_range(&naga_module, shader_entry_bind_groups.shader_stages); 123 | 124 | let pipeline_layout_name = format!("{}::PipelineLayout", entry_name); 125 | 126 | quote! { 127 | #additional_pipeline_entries_struct 128 | #wgpu_pipeline_entries_struct 129 | pub fn create_pipeline_layout(device: &wgpu::Device) -> wgpu::PipelineLayout { 130 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 131 | label: Some(#pipeline_layout_name), 132 | bind_group_layouts: &[ 133 | #(&#bind_group_layouts),* 134 | ], 135 | push_constant_ranges: &[#push_constant_range], 136 | }) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/generate/shader_registry.rs: -------------------------------------------------------------------------------- 1 | //! This module provides functionality for building a shader registry. 2 | //! 3 | //! This will create a `ShaderEntry` enum with a variant for each entry in `entries`, 4 | //! and functions for creating the pipeline layout and shader module for each variant. 5 | 6 | use derive_more::Constructor; 7 | use enumflags2::BitFlags; 8 | use proc_macro2::TokenStream; 9 | use quote::{format_ident, quote}; 10 | 11 | use crate::{sanitize_and_pascal_case, WgslEntryResult, WgslShaderSourceType}; 12 | 13 | #[derive(Constructor)] 14 | struct ShaderEntryBuilder<'a, 'b> { 15 | entries: &'a [WgslEntryResult<'b>], 16 | source_type: BitFlags, 17 | } 18 | 19 | impl<'a, 'b> ShaderEntryBuilder<'a, 'b> { 20 | fn build_registry_enum(&self) -> TokenStream { 21 | let variants = self 22 | .entries 23 | .iter() 24 | .map(|entry| format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name))); 25 | 26 | quote! { 27 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 28 | pub enum ShaderEntry { 29 | #( #variants, )* 30 | } 31 | } 32 | } 33 | 34 | fn build_create_pipeline_layout_fn(&self) -> TokenStream { 35 | let match_arms = self.entries.iter().map(|entry| { 36 | let mod_path = format_ident!("{}", entry.mod_name); 37 | let enum_variant = format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name)); 38 | 39 | quote! { 40 | Self::#enum_variant => #mod_path::create_pipeline_layout(device) 41 | } 42 | }); 43 | 44 | quote! { 45 | pub fn create_pipeline_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { 46 | match self { 47 | #( #match_arms, )* 48 | } 49 | } 50 | } 51 | } 52 | 53 | fn build_create_shader_module(&self, source_type: WgslShaderSourceType) -> TokenStream { 54 | let fn_name = format_ident!("{}", source_type.create_shader_module_fn_name()); 55 | let (param_defs, params) = source_type.shader_module_params_defs_and_params(); 56 | 57 | let match_arms = self.entries.iter().map(|entry| { 58 | let mod_path = format_ident!("{}", entry.mod_name); 59 | let enum_variant = format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name)); 60 | 61 | quote! { 62 | Self::#enum_variant => { 63 | #mod_path::#fn_name(#params) 64 | } 65 | } 66 | }); 67 | 68 | let return_type = source_type.get_return_type(quote!(wgpu::ShaderModule)); 69 | 70 | quote! { 71 | pub fn #fn_name(&self, #param_defs) -> #return_type { 72 | match self { 73 | #( #match_arms, )* 74 | } 75 | } 76 | } 77 | } 78 | 79 | fn build_load_shader_to_composer_module( 80 | &self, 81 | source_type: WgslShaderSourceType, 82 | ) -> TokenStream { 83 | match source_type { 84 | WgslShaderSourceType::EmbedSource => { 85 | return quote!(); 86 | } 87 | _ => { 88 | let fn_name = format_ident!("{}", source_type.load_shader_module_fn_name()); 89 | 90 | let match_arms = self.entries.iter().map(|entry| { 91 | let mod_path = format_ident!("{}", entry.mod_name); 92 | let enum_variant = 93 | format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name)); 94 | 95 | quote! { 96 | Self::#enum_variant => { 97 | #mod_path::#fn_name(composer, shader_defs) 98 | } 99 | } 100 | }); 101 | 102 | let return_type = source_type.get_return_type(quote!(wgpu::naga::Module)); 103 | 104 | quote! { 105 | pub fn #fn_name(&self, 106 | composer: &mut naga_oil::compose::Composer, 107 | shader_defs: std::collections::HashMap 108 | ) -> #return_type { 109 | match self { 110 | #( #match_arms, )* 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | fn build_shader_entry_filename_fn(&self) -> TokenStream { 119 | if !self 120 | .source_type 121 | .contains(WgslShaderSourceType::HardCodedFilePathWithNagaOilComposer) 122 | { 123 | return quote!(); 124 | } 125 | 126 | let match_arms = self.entries.iter().map(|entry| { 127 | let filename = entry 128 | .source_including_deps 129 | .source_file 130 | .file_path 131 | .file_name() 132 | .unwrap() 133 | .to_str() 134 | .unwrap(); 135 | let enum_variant = format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name)); 136 | 137 | quote! { 138 | Self::#enum_variant => #filename 139 | } 140 | }); 141 | 142 | quote! { 143 | pub fn shader_entry_filename(&self) -> &'static str { 144 | match self { 145 | #( #match_arms, )* 146 | } 147 | } 148 | } 149 | } 150 | 151 | fn build_shader_paths_fn(&self) -> TokenStream { 152 | if !self 153 | .source_type 154 | .contains(WgslShaderSourceType::HardCodedFilePathWithNagaOilComposer) 155 | { 156 | return quote!(); 157 | } 158 | 159 | let match_arms = self.entries.iter().map(|entry| { 160 | let mod_path = format_ident!("{}", entry.mod_name); 161 | let enum_variant = format_ident!("{}", sanitize_and_pascal_case(&entry.mod_name)); 162 | 163 | quote! { 164 | Self::#enum_variant => #mod_path::SHADER_PATHS 165 | } 166 | }); 167 | 168 | quote! { 169 | pub fn shader_paths(&self) -> &[&str] { 170 | match self { 171 | #( #match_arms, )* 172 | } 173 | } 174 | } 175 | } 176 | 177 | fn build_enum_impl(&self) -> TokenStream { 178 | let create_shader_module_fns = self 179 | .source_type 180 | .iter() 181 | .map(|source_ty| self.build_create_shader_module(source_ty)) 182 | .collect::>(); 183 | 184 | let create_pipeline_layout_fn = self.build_create_pipeline_layout_fn(); 185 | let load_shader_to_composer_module_fns = self 186 | .source_type 187 | .iter() 188 | .map(|source_ty| self.build_load_shader_to_composer_module(source_ty)) 189 | .collect::>(); 190 | 191 | let shader_paths_fn = self.build_shader_paths_fn(); 192 | let shader_entry_filename_fn = self.build_shader_entry_filename_fn(); 193 | 194 | quote! { 195 | impl ShaderEntry { 196 | #create_pipeline_layout_fn 197 | #(#create_shader_module_fns)* 198 | #(#load_shader_to_composer_module_fns)* 199 | #shader_entry_filename_fn 200 | #shader_paths_fn 201 | } 202 | } 203 | } 204 | 205 | pub fn build(&self) -> TokenStream { 206 | let enum_def = self.build_registry_enum(); 207 | let enum_impl = self.build_enum_impl(); 208 | quote! { 209 | #enum_def 210 | #enum_impl 211 | } 212 | } 213 | } 214 | 215 | pub(crate) fn build_shader_registry( 216 | entries: &[WgslEntryResult<'_>], 217 | source_type: BitFlags, 218 | ) -> TokenStream { 219 | ShaderEntryBuilder::new(entries, source_type).build() 220 | } 221 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/naga_util/mod.rs: -------------------------------------------------------------------------------- 1 | mod module_to_source; 2 | pub use module_to_source::*; 3 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/naga_util/module_to_source.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/LucentFlux/naga-to-tokenstream/blob/main/src/lib.rs#L26 2 | pub fn module_to_source( 3 | module: &naga::Module, 4 | ) -> Result { 5 | // Clone since we sometimes modify things 6 | #[allow(unused_mut)] 7 | let mut module = module.clone(); 8 | 9 | // If we minify, do the first pass before writing out 10 | #[cfg(feature = "minify")] 11 | { 12 | wgsl_minifier::minify_module(&mut module); 13 | } 14 | 15 | // Mini validation to get module info 16 | let info = naga::valid::Validator::new( 17 | naga::valid::ValidationFlags::all(), 18 | naga::valid::Capabilities::all(), 19 | ) 20 | .validate(&module); 21 | 22 | // Write to wgsl 23 | let info = info.unwrap(); 24 | let src = naga::back::wgsl::write_string( 25 | &module, 26 | &info, 27 | naga::back::wgsl::WriterFlags::empty(), 28 | )?; 29 | 30 | // Remove whitespace if minifying 31 | #[cfg(feature = "minify")] 32 | let src = wgsl_minifier::minify_wgsl_source(&src); 33 | 34 | return Ok(src); 35 | } 36 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/quote_gen/constants.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | 3 | /// This mod is used such that all the mods in the out can reference this from anywhere 4 | pub(crate) const MOD_REFERENCE_ROOT: &str = "_root"; 5 | pub(crate) const MOD_STRUCT_ASSERTIONS: &str = "layout_asserts"; 6 | pub(crate) const MOD_BYTEMUCK_IMPLS: &str = "bytemuck_impls"; 7 | 8 | pub(crate) fn mod_reference_root() -> Ident { 9 | unsafe { syn::parse_str(MOD_REFERENCE_ROOT).unwrap_unchecked() } 10 | } 11 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/quote_gen/mod.rs: -------------------------------------------------------------------------------- 1 | mod constants; 2 | mod rust_module_builder; 3 | mod rust_source_item; 4 | mod rust_struct_builder; 5 | mod rust_type_info; 6 | 7 | use core::panic; 8 | 9 | pub(crate) use constants::*; 10 | use proc_macro2::TokenStream; 11 | pub(crate) use rust_module_builder::*; 12 | pub(crate) use rust_source_item::*; 13 | pub(crate) use rust_struct_builder::*; 14 | pub(crate) use rust_type_info::*; 15 | 16 | use crate::bevy_util::demangle_str; 17 | 18 | /// Creates a raw string literal from the given shader content. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `shader_content` - The content of the shader as a string. 23 | /// 24 | /// # Returns 25 | /// 26 | /// The token stream representing the raw string literal. 27 | pub(crate) fn create_shader_raw_string_literal(shader_content: &str) -> TokenStream { 28 | syn::parse_str::(&format!("r#\"\n{}\"#", &shader_content)).unwrap() 29 | } 30 | 31 | /// Demangles the given string and qualifies it with the qualification root. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `string` - The string to demangle and qualify. 36 | /// * `default_mod_path` - The default module path to use if the string does not contain a module path. 37 | /// 38 | /// # Returns 39 | /// 40 | /// The demangled and qualified token stream. 41 | pub(crate) fn demangle_and_fully_qualify_str( 42 | string: &str, 43 | default_mod_path: Option<&str>, 44 | ) -> String { 45 | let demangled = demangle_str(string); 46 | 47 | match (demangled.contains("::"), default_mod_path) { 48 | (true, _) => { 49 | format!("{}::{}", MOD_REFERENCE_ROOT, demangled) 50 | } 51 | (false, None) => demangled.to_string(), 52 | (false, Some(default_mod_path)) => { 53 | if default_mod_path.is_empty() { 54 | panic!("default_mod_path cannot be empty"); 55 | } 56 | 57 | let default_mod_path = default_mod_path.to_lowercase(); 58 | format!("{MOD_REFERENCE_ROOT}::{default_mod_path}::{demangled}") 59 | } 60 | } 61 | } 62 | 63 | pub(crate) fn demangle_and_fully_qualify( 64 | string: &str, 65 | default_mod_path: Option<&str>, 66 | ) -> TokenStream { 67 | let raw_path = demangle_and_fully_qualify_str(string, default_mod_path); 68 | syn::parse_str(&raw_path).unwrap() 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use pretty_assertions::assert_eq; 74 | 75 | use super::demangle_and_fully_qualify; 76 | 77 | #[test] 78 | fn should_fully_qualify_mangled_string() { 79 | let string = "UniformsX_naga_oil_mod_XOR4XAZLTX"; 80 | let actual = demangle_and_fully_qualify(string, None); 81 | assert_eq!(actual.to_string(), "_root :: types :: Uniforms"); 82 | } 83 | 84 | #[test] 85 | fn should_not_fully_qualify_non_mangled_string() { 86 | let string = "MatricesF64"; 87 | let actual = demangle_and_fully_qualify(string, None); 88 | assert_eq!(actual.to_string(), "MatricesF64"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/quote_gen/rust_source_item.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use derive_more::Constructor; 3 | use enumflags2::{bitflags, BitFlags}; 4 | use proc_macro2::{Span, TokenStream}; 5 | use quote::ToTokens; 6 | use smol_str::SmolStr; 7 | 8 | /// `RustItemPath` represents the path to a Rust item within a module. 9 | #[derive(Constructor, Debug, Clone, PartialEq, Eq, Hash)] 10 | pub(crate) struct RustSourceItemPath { 11 | /// The path to the parent module. 12 | pub module: SmolStr, 13 | /// name of the item, without the module path. 14 | pub name: SmolStr, 15 | } 16 | 17 | impl ToTokens for RustSourceItemPath { 18 | fn to_tokens(&self, tokens: &mut TokenStream) { 19 | let fq_name = self.get_fully_qualified_name(); 20 | let current = syn::parse_str::(&fq_name).unwrap(); 21 | tokens.extend(current) 22 | } 23 | } 24 | 25 | impl RustSourceItemPath { 26 | pub fn get_fully_qualified_name(&self) -> SmolStr { 27 | if self.module.is_empty() { 28 | SmolStr::new(self.name.as_str()) 29 | } else { 30 | SmolStr::new(format!("{}::{}", self.module, self.name).as_str()) 31 | } 32 | } 33 | 34 | /// Returns a shortened `TokenStream`, 35 | /// If the module of the item is the same as given `target_module`, it will return just the `name` part of the path. 36 | /// Otherwise, it will return the full path. 37 | pub fn short_token_stream(&self, target_module: &str) -> TokenStream { 38 | if self.module == target_module { 39 | let ident = syn::Ident::new(&self.name, Span::call_site()); 40 | quote::quote!(#ident) 41 | } else { 42 | self.to_token_stream() 43 | } 44 | } 45 | } 46 | 47 | #[bitflags] 48 | #[repr(u8)] 49 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 50 | pub(crate) enum RustSourceItemCategory { 51 | /// like `const VAR_NAME: Type = value;` 52 | ConstVarDecls, 53 | 54 | /// like `impl Trait for Struct {}` 55 | TraitImpls, 56 | 57 | /// like `impl Struct {}` 58 | TypeImpls, 59 | 60 | /// like `struct Struct {}` 61 | TypeDefs, 62 | } 63 | 64 | /// Represents a Rust source item, that is either a ConstVar, TraitImpls or others. 65 | #[derive(Constructor, Debug)] 66 | pub(crate) struct RustSourceItem { 67 | pub catagories: BitFlags, 68 | pub path: RustSourceItemPath, 69 | pub tokenstream: TokenStream, 70 | } 71 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use derivative::Derivative; 4 | use derive_more::{AsRef, Deref, Display, From, Into}; 5 | use fxhash::FxBuildHasher; 6 | use indexmap::{IndexMap, IndexSet}; 7 | use smol_str::SmolStr; 8 | 9 | pub type FxIndexMap = IndexMap; 10 | pub type FxIndexSet = IndexSet; 11 | 12 | #[derive(AsRef, Hash, From, Into, Clone, PartialEq, Eq, Derivative, Deref, Display)] 13 | #[display("{}", _0.to_str().unwrap())] 14 | #[derivative(Debug = "transparent")] 15 | pub struct SourceFilePath(PathBuf); 16 | 17 | impl SourceFilePath { 18 | pub fn new(value: impl Into) -> Self { 19 | Self(value.into()) 20 | } 21 | 22 | pub fn read_contents(&self) -> Result { 23 | std::fs::read_to_string(self.as_path()) 24 | } 25 | 26 | pub fn dir(&self) -> SourceFileDir { 27 | SourceFileDir(self.parent().unwrap().into()) 28 | } 29 | 30 | pub fn file_prefix(&self) -> String { 31 | // file_prefix is only available in nightly 32 | let file_name = self.0.file_stem().unwrap().to_str().unwrap(); 33 | let prefix = file_name.split('.').next().unwrap_or(""); 34 | prefix.to_string() 35 | } 36 | } 37 | 38 | #[derive(AsRef, Hash, From, Into, Clone, PartialEq, Eq, Derivative, Deref, Display)] 39 | #[display("{}", _0.to_str().unwrap())] 40 | #[derivative(Debug = "transparent")] 41 | pub struct SourceFileDir(PathBuf); 42 | 43 | impl SourceFileDir { 44 | pub fn new(value: impl Into) -> Self { 45 | Self(value.into()) 46 | } 47 | 48 | pub fn read_contents(&self) -> Result { 49 | std::fs::read_to_string(self.as_path()) 50 | } 51 | } 52 | 53 | impl From<&SourceFilePath> for SourceFileDir { 54 | fn from(value: &SourceFilePath) -> Self { 55 | value.dir() 56 | } 57 | } 58 | 59 | /// Import part path used in the import statement 60 | #[derive(AsRef, Hash, From, Into, Clone, PartialEq, Eq, Derivative, Deref, Display)] 61 | #[display("{}", _0)] 62 | #[derivative(Debug = "transparent")] 63 | pub struct ImportPathPart(SmolStr); 64 | 65 | impl ImportPathPart { 66 | pub fn new(value: impl Into) -> Self { 67 | Self(value.into()) 68 | } 69 | } 70 | 71 | #[derive(AsRef, Hash, From, Into, Clone, PartialEq, Eq, Derivative, Deref, Display)] 72 | #[display("{}", _0)] 73 | #[derivative(Debug = "transparent")] 74 | pub struct SourceModuleName(SmolStr); 75 | 76 | impl SourceModuleName { 77 | pub fn new(value: impl Into) -> Self { 78 | Self(value.into()) 79 | } 80 | } 81 | 82 | #[derive(Debug, Clone, PartialEq, Eq)] 83 | pub struct SourceLocation { 84 | /// 1-based line number. 85 | pub line_number: usize, 86 | /// 1-based column of the start of this span 87 | pub line_position: usize, 88 | /// 0-based Offset in code units (in bytes) of the start of the span. 89 | pub offset: usize, 90 | /// Length in code units (in bytes) of the span. 91 | pub length: usize, 92 | } 93 | 94 | impl From<&SourceLocation> for miette::SourceSpan { 95 | fn from(value: &SourceLocation) -> miette::SourceSpan { 96 | miette::SourceSpan::new(value.offset.into(), value.length) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/wgsl.rs: -------------------------------------------------------------------------------- 1 | use naga::StructMember; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | 5 | use crate::quote_gen::RustSourceItemPath; 6 | 7 | pub fn shader_stages(module: &naga::Module) -> wgpu::ShaderStages { 8 | module 9 | .entry_points 10 | .iter() 11 | .map(|entry| match entry.stage { 12 | naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX, 13 | naga::ShaderStage::Fragment => wgpu::ShaderStages::FRAGMENT, 14 | naga::ShaderStage::Compute => wgpu::ShaderStages::COMPUTE, 15 | }) 16 | .collect() 17 | } 18 | 19 | pub fn buffer_binding_type(storage: naga::AddressSpace) -> TokenStream { 20 | match storage { 21 | naga::AddressSpace::Uniform => quote!(wgpu::BufferBindingType::Uniform), 22 | naga::AddressSpace::Storage { access } => { 23 | let _is_read = access.contains(naga::StorageAccess::LOAD); 24 | let is_write = access.contains(naga::StorageAccess::STORE); 25 | 26 | // TODO: Is this correct? 27 | if is_write { 28 | quote!(wgpu::BufferBindingType::Storage { read_only: false }) 29 | } else { 30 | quote!(wgpu::BufferBindingType::Storage { read_only: true }) 31 | } 32 | } 33 | _ => todo!(), 34 | } 35 | } 36 | 37 | pub fn vertex_format(ty: &naga::Type) -> wgpu::VertexFormat { 38 | // Not all wgsl types work as vertex attributes in wgpu. 39 | match &ty.inner { 40 | naga::TypeInner::Scalar(scalar) => match (scalar.kind, scalar.width) { 41 | (naga::ScalarKind::Sint, 4) => wgpu::VertexFormat::Sint32, 42 | (naga::ScalarKind::Uint, 4) => wgpu::VertexFormat::Uint32, 43 | (naga::ScalarKind::Float, 4) => wgpu::VertexFormat::Float32, 44 | (naga::ScalarKind::Float, 8) => wgpu::VertexFormat::Float64, 45 | _ => todo!(), 46 | }, 47 | naga::TypeInner::Vector { size, scalar } => match size { 48 | naga::VectorSize::Bi => match (scalar.kind, scalar.width) { 49 | (naga::ScalarKind::Sint, 1) => wgpu::VertexFormat::Sint8x2, 50 | (naga::ScalarKind::Uint, 1) => wgpu::VertexFormat::Uint8x2, 51 | (naga::ScalarKind::Sint, 2) => wgpu::VertexFormat::Sint16x2, 52 | (naga::ScalarKind::Uint, 2) => wgpu::VertexFormat::Uint16x2, 53 | (naga::ScalarKind::Uint, 4) => wgpu::VertexFormat::Uint32x2, 54 | (naga::ScalarKind::Sint, 4) => wgpu::VertexFormat::Sint32x2, 55 | (naga::ScalarKind::Float, 4) => wgpu::VertexFormat::Float32x2, 56 | (naga::ScalarKind::Float, 8) => wgpu::VertexFormat::Float64x2, 57 | _ => todo!(), 58 | }, 59 | naga::VectorSize::Tri => match (scalar.kind, scalar.width) { 60 | (naga::ScalarKind::Uint, 4) => wgpu::VertexFormat::Uint32x3, 61 | (naga::ScalarKind::Sint, 4) => wgpu::VertexFormat::Sint32x3, 62 | (naga::ScalarKind::Float, 4) => wgpu::VertexFormat::Float32x3, 63 | (naga::ScalarKind::Float, 8) => wgpu::VertexFormat::Float64x3, 64 | _ => todo!(), 65 | }, 66 | naga::VectorSize::Quad => match (scalar.kind, scalar.width) { 67 | (naga::ScalarKind::Sint, 1) => wgpu::VertexFormat::Sint8x4, 68 | (naga::ScalarKind::Uint, 1) => wgpu::VertexFormat::Uint8x4, 69 | (naga::ScalarKind::Sint, 2) => wgpu::VertexFormat::Sint16x4, 70 | (naga::ScalarKind::Uint, 2) => wgpu::VertexFormat::Uint16x4, 71 | (naga::ScalarKind::Uint, 4) => wgpu::VertexFormat::Uint32x4, 72 | (naga::ScalarKind::Sint, 4) => wgpu::VertexFormat::Sint32x4, 73 | (naga::ScalarKind::Float, 4) => wgpu::VertexFormat::Float32x4, 74 | (naga::ScalarKind::Float, 8) => wgpu::VertexFormat::Float64x4, 75 | _ => todo!(), 76 | }, 77 | }, 78 | _ => todo!(), // are these types even valid as attributes? 79 | } 80 | } 81 | 82 | pub struct VertexInput { 83 | pub item_path: RustSourceItemPath, 84 | pub fields: Vec<(u32, StructMember)>, 85 | } 86 | 87 | // TODO: Handle errors. 88 | // Collect the necessary data to generate an equivalent Rust struct. 89 | pub fn get_vertex_input_structs( 90 | invoking_entry_module: &str, 91 | module: &naga::Module, 92 | ) -> Vec { 93 | // TODO: Handle multiple entries? 94 | module 95 | .entry_points 96 | .iter() 97 | .find(|e| e.stage == naga::ShaderStage::Vertex) 98 | .map(|vertex_entry| { 99 | vertex_entry 100 | .function 101 | .arguments 102 | .iter() 103 | .filter(|a| a.binding.is_none()) 104 | .filter_map(|argument| { 105 | let arg_type = &module.types[argument.ty]; 106 | match &arg_type.inner { 107 | naga::TypeInner::Struct { members, span: _ } => { 108 | let item_path = RustSourceItemPath::from_mangled( 109 | arg_type.name.as_ref().unwrap(), 110 | invoking_entry_module, 111 | ); 112 | 113 | let input = VertexInput { 114 | item_path, 115 | fields: members 116 | .iter() 117 | .filter_map(|member| { 118 | // Skip builtins since they have no location binding. 119 | let location = match member.binding.as_ref().unwrap() { 120 | naga::Binding::BuiltIn(_) => None, 121 | naga::Binding::Location { location, .. } => Some(*location), 122 | }?; 123 | 124 | Some((location, member.clone())) 125 | }) 126 | .collect(), 127 | }; 128 | 129 | Some(input) 130 | } 131 | // An argument has to have a binding unless it is a structure. 132 | _ => None, 133 | } 134 | }) 135 | .collect() 136 | }) 137 | .unwrap_or_default() 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use indoc::indoc; 143 | use pretty_assertions::assert_eq; 144 | 145 | use super::*; 146 | 147 | #[test] 148 | fn shader_stages_none() { 149 | let source = indoc! {r#" 150 | 151 | "#}; 152 | 153 | let module = naga::front::wgsl::parse_str(source).unwrap(); 154 | assert_eq!(wgpu::ShaderStages::NONE, shader_stages(&module)); 155 | } 156 | 157 | #[test] 158 | fn shader_stages_vertex() { 159 | let source = indoc! {r#" 160 | @vertex 161 | fn main() {} 162 | "#}; 163 | 164 | let module = naga::front::wgsl::parse_str(source).unwrap(); 165 | assert_eq!(wgpu::ShaderStages::VERTEX, shader_stages(&module)); 166 | } 167 | 168 | #[test] 169 | fn shader_stages_fragment() { 170 | let source = indoc! {r#" 171 | @fragment 172 | fn main() {} 173 | "#}; 174 | 175 | let module = naga::front::wgsl::parse_str(source).unwrap(); 176 | assert_eq!(wgpu::ShaderStages::FRAGMENT, shader_stages(&module)); 177 | } 178 | 179 | #[test] 180 | fn shader_stages_vertex_fragment() { 181 | let source = indoc! {r#" 182 | @vertex 183 | fn vs_main() {} 184 | 185 | @fragment 186 | fn fs_main() {} 187 | "#}; 188 | 189 | let module = naga::front::wgsl::parse_str(source).unwrap(); 190 | assert_eq!(wgpu::ShaderStages::VERTEX_FRAGMENT, shader_stages(&module)); 191 | } 192 | 193 | #[test] 194 | fn shader_stages_compute() { 195 | let source = indoc! {r#" 196 | @compute 197 | @workgroup_size(64) 198 | fn main() {} 199 | "#}; 200 | 201 | let module = naga::front::wgsl::parse_str(source).unwrap(); 202 | assert_eq!(wgpu::ShaderStages::COMPUTE, shader_stages(&module)); 203 | } 204 | 205 | #[test] 206 | fn shader_stages_all() { 207 | let source = indoc! {r#" 208 | @vertex 209 | fn vs_main() {} 210 | 211 | @fragment 212 | fn fs_main() {} 213 | 214 | @compute 215 | @workgroup_size(64) 216 | fn cs_main() {} 217 | "#}; 218 | 219 | let module = naga::front::wgsl::parse_str(source).unwrap(); 220 | assert_eq!(wgpu::ShaderStages::all(), shader_stages(&module)); 221 | } 222 | 223 | #[test] 224 | fn vertex_input_structs_two_structs() { 225 | let source = indoc! {r#" 226 | struct VertexInput0 { 227 | @location(0) in0: vec4, 228 | @location(1) in1: vec4, 229 | @location(2) in2: vec4, 230 | }; 231 | 232 | struct VertexInput1 { 233 | @location(3) in3: vec4, 234 | @location(4) in4: vec4, 235 | @builtin(vertex_index) index: u32, 236 | @location(5) in5: vec4, 237 | @location(6) in6: vec4, 238 | }; 239 | 240 | @vertex 241 | fn main( 242 | in0: VertexInput0, 243 | in1: VertexInput1, 244 | @builtin(front_facing) in2: bool, 245 | @location(7) in3: vec4, 246 | ) -> @builtin(position) vec4 { 247 | return vec4(0.0); 248 | } 249 | "#}; 250 | 251 | let module = naga::front::wgsl::parse_str(source).unwrap(); 252 | 253 | let vertex_inputs = get_vertex_input_structs("", &module); 254 | // Only structures should be included. 255 | assert_eq!(2, vertex_inputs.len()); 256 | 257 | assert_eq!("VertexInput0", vertex_inputs[0].item_path.name); 258 | assert_eq!(3, vertex_inputs[0].fields.len()); 259 | assert_eq!("in1", vertex_inputs[0].fields[1].1.name.as_ref().unwrap()); 260 | assert_eq!(1, vertex_inputs[0].fields[1].0); 261 | 262 | assert_eq!("VertexInput1", vertex_inputs[1].item_path.name); 263 | assert_eq!(4, vertex_inputs[1].fields.len()); 264 | assert_eq!("in5", vertex_inputs[1].fields[2].1.name.as_ref().unwrap()); 265 | assert_eq!(5, vertex_inputs[1].fields[2].0); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /wgsl_bindgen/src/wgsl_type.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use derive_more::{From, IsVariant}; 4 | use strum_macros::EnumIter; 5 | 6 | use crate::quote_gen::RustTypeInfo; 7 | use crate::WgslTypeMap; 8 | 9 | /// The `WgslType` enum represents various WGSL vectors. 10 | /// See [spec](https://www.w3.org/TR/WGSL/#alignment-and-size) 11 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumIter)] 12 | pub enum WgslVecType { 13 | Vec2i, 14 | Vec3i, 15 | Vec4i, 16 | Vec2u, 17 | Vec3u, 18 | Vec4u, 19 | Vec2f, 20 | Vec3f, 21 | Vec4f, 22 | Vec2h, 23 | Vec3h, 24 | Vec4h, 25 | } 26 | 27 | /// The `WgslType` enum represents various Wgsl matrices. 28 | /// See [spec](https://www.w3.org/TR/WGSL/#alignment-and-size) 29 | #[derive(Debug, From, Clone, Copy, Hash, PartialEq, Eq, EnumIter)] 30 | pub enum WgslMatType { 31 | Mat2x2f, 32 | Mat2x3f, 33 | Mat2x4f, 34 | Mat3x2f, 35 | Mat3x3f, 36 | Mat3x4f, 37 | Mat4x2f, 38 | Mat4x3f, 39 | Mat4x4f, 40 | Mat2x2h, 41 | Mat2x3h, 42 | Mat2x4h, 43 | Mat3x2h, 44 | Mat3x3h, 45 | Mat3x4h, 46 | Mat4x2h, 47 | Mat4x3h, 48 | Mat4x4h, 49 | } 50 | 51 | pub(crate) trait WgslTypeAlignmentAndSize { 52 | fn alignment_and_size(&self) -> (u8, usize); 53 | } 54 | 55 | impl WgslTypeAlignmentAndSize for WgslVecType { 56 | fn alignment_and_size(&self) -> (u8, usize) { 57 | use WgslVecType::*; 58 | match self { 59 | Vec2i | Vec2u | Vec2f => (8, 8), 60 | Vec2h => (4, 4), 61 | Vec3i | Vec3u | Vec3f => (16, 12), 62 | Vec3h => (8, 6), 63 | Vec4i | Vec4u | Vec4f => (16, 16), 64 | Vec4h => (8, 8), 65 | } 66 | } 67 | } 68 | 69 | impl WgslTypeAlignmentAndSize for WgslMatType { 70 | fn alignment_and_size(&self) -> (u8, usize) { 71 | use WgslMatType::*; 72 | match self { 73 | // AlignOf(vecR), SizeOf(array) 74 | Mat2x2f => (8, 16), 75 | Mat2x2h => (4, 8), 76 | Mat3x2f => (8, 24), 77 | Mat3x2h => (4, 12), 78 | Mat4x2f => (8, 32), 79 | Mat4x2h => (4, 16), 80 | Mat2x3f => (16, 32), 81 | Mat2x3h => (8, 16), 82 | Mat3x3f => (16, 48), 83 | Mat3x3h => (8, 24), 84 | Mat4x3f => (16, 64), 85 | Mat4x3h => (8, 32), 86 | Mat2x4f => (16, 32), 87 | Mat2x4h => (8, 16), 88 | Mat3x4f => (16, 48), 89 | Mat3x4h => (8, 24), 90 | Mat4x4f => (16, 64), 91 | Mat4x4h => (8, 32), 92 | } 93 | } 94 | } 95 | 96 | pub(crate) trait WgslBuiltInMappedType { 97 | fn get_mapped_type(&self, type_map: &WgslTypeMap) -> Option; 98 | } 99 | 100 | impl WgslBuiltInMappedType for T 101 | where 102 | T: WgslTypeAlignmentAndSize + Copy, 103 | WgslType: From, 104 | { 105 | fn get_mapped_type(&self, type_map: &WgslTypeMap) -> Option { 106 | let (alignment_width, size) = self.alignment_and_size(); 107 | let wgsl_ty = WgslType::from(*self); 108 | let ty = type_map.get(&wgsl_ty)?.clone(); 109 | let alignment = naga::proc::Alignment::from_width(alignment_width); 110 | Some(RustTypeInfo(ty, size, alignment)) 111 | } 112 | } 113 | 114 | /// The `WgslType` enum represents various WGSL types, such as vectors and matrices. 115 | /// See [spec](https://www.w3.org/TR/WGSL/#alignment-and-size) 116 | #[derive(Debug, From, Clone, Hash, PartialEq, Eq, IsVariant)] 117 | pub enum WgslType { 118 | Vector(WgslVecType), 119 | Matrix(WgslMatType), 120 | Struct { fully_qualified_name: String }, 121 | } 122 | 123 | impl WgslType { 124 | pub(crate) fn get_mapped_type( 125 | &self, 126 | type_map: &WgslTypeMap, 127 | size: usize, 128 | alignment: naga::proc::Alignment, 129 | ) -> Option { 130 | match self { 131 | WgslType::Vector(vec_ty) => vec_ty.get_mapped_type(type_map), 132 | WgslType::Matrix(mat_ty) => mat_ty.get_mapped_type(type_map), 133 | WgslType::Struct { .. } => { 134 | let ty = type_map.get(self)?.clone(); 135 | Some(RustTypeInfo(ty, size, alignment)) 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/bindgen_tests.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_to_string; 2 | 3 | use miette::{IntoDiagnostic, Result}; 4 | use pretty_assertions::assert_eq; 5 | use wgsl_bindgen::*; 6 | 7 | #[test] 8 | fn test_bevy_bindgen() -> Result<()> { 9 | WgslBindgenOptionBuilder::default() 10 | .module_import_root("bevy_pbr") 11 | .workspace_root("tests/shaders/bevy_pbr_wgsl") 12 | .add_entry_point("tests/shaders/bevy_pbr_wgsl/pbr.wgsl") 13 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 14 | .type_map(GlamWgslTypeMap) 15 | .emit_rerun_if_change(false) 16 | .skip_header_comments(true) 17 | .output("tests/output/bindgen_bevy.actual.rs".to_string()) 18 | .build()? 19 | .generate() 20 | .into_diagnostic()?; 21 | 22 | let actual = read_to_string("tests/output/bindgen_bevy.actual.rs").unwrap(); 23 | let expected = read_to_string("tests/output/bindgen_bevy.expected.rs").unwrap(); 24 | 25 | assert_eq!(expected, actual); 26 | Ok(()) 27 | } 28 | 29 | #[test] 30 | fn test_main_bindgen() -> Result<()> { 31 | WgslBindgenOptionBuilder::default() 32 | .add_entry_point("tests/shaders/basic/main.wgsl") 33 | .workspace_root("tests/shaders/additional") 34 | .additional_scan_dir((None, "tests/shaders/additional")) 35 | .override_struct_alignment([("main::Style", 256)].map(Into::into)) 36 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 37 | .type_map(GlamWgslTypeMap) 38 | .emit_rerun_if_change(false) 39 | .skip_header_comments(true) 40 | .ir_capabilities(naga::valid::Capabilities::PUSH_CONSTANT) 41 | .shader_source_type( 42 | WgslShaderSourceType::EmbedSource 43 | | WgslShaderSourceType::HardCodedFilePathWithNagaOilComposer, 44 | ) 45 | .output("tests/output/bindgen_main.actual.rs".to_string()) 46 | .build()? 47 | .generate() 48 | .into_diagnostic()?; 49 | 50 | let actual = read_to_string("tests/output/bindgen_main.actual.rs").unwrap(); 51 | let expected = read_to_string("tests/output/bindgen_main.expected.rs").unwrap(); 52 | 53 | assert_eq!(expected, actual); 54 | Ok(()) 55 | } 56 | 57 | #[test] 58 | fn test_struct_alignment_minimal() -> Result<()> { 59 | WgslBindgenOptionBuilder::default() 60 | .add_entry_point("tests/shaders/minimal.wgsl") 61 | .workspace_root("tests/shaders") 62 | .override_struct_alignment([(".*::Uniforms", 256)].map(Into::into)) 63 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 64 | .type_map(GlamWgslTypeMap) 65 | .emit_rerun_if_change(false) 66 | .skip_header_comments(true) 67 | .output("tests/output/bindgen_minimal.actual.rs".to_string()) 68 | .build()? 69 | .generate() 70 | .into_diagnostic()?; 71 | 72 | let actual = read_to_string("tests/output/bindgen_minimal.actual.rs").unwrap(); 73 | let expected = read_to_string("tests/output/bindgen_minimal.expected.rs").unwrap(); 74 | 75 | assert_eq!(expected, actual); 76 | Ok(()) 77 | } 78 | 79 | #[test] 80 | fn test_struct_alignment_padding() -> Result<()> { 81 | WgslBindgenOptionBuilder::default() 82 | .add_entry_point("tests/shaders/padding.wgsl") 83 | .workspace_root("tests/shaders") 84 | .add_custom_padding_field_regexp(Regex::new("_padding").unwrap()) 85 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 86 | .type_map(GlamWgslTypeMap) 87 | .emit_rerun_if_change(false) 88 | .skip_header_comments(true) 89 | .output("tests/output/bindgen_padding.actual.rs".to_string()) 90 | .build()? 91 | .generate() 92 | .into_diagnostic()?; 93 | 94 | let actual = read_to_string("tests/output/bindgen_padding.actual.rs").unwrap(); 95 | let expected = read_to_string("tests/output/bindgen_padding.expected.rs").unwrap(); 96 | 97 | assert_eq!(expected, actual); 98 | Ok(()) 99 | } 100 | 101 | #[test] 102 | fn test_struct_layouts() -> Result<()> { 103 | WgslBindgenOptionBuilder::default() 104 | .add_entry_point("tests/shaders/layouts.wgsl") 105 | .workspace_root("tests/shaders") 106 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 107 | .type_map(GlamWgslTypeMap) 108 | .emit_rerun_if_change(false) 109 | .skip_header_comments(true) 110 | .override_bind_group_entry_module_path( 111 | [("color_texture", "bindings"), ("color_sampler", "bindings")].map(Into::into), 112 | ) 113 | .output("tests/output/bindgen_layouts.actual.rs".to_string()) 114 | .build()? 115 | .generate() 116 | .into_diagnostic()?; 117 | 118 | let actual = read_to_string("tests/output/bindgen_layouts.actual.rs").unwrap(); 119 | let expected = read_to_string("tests/output/bindgen_layouts.expected.rs").unwrap(); 120 | 121 | assert_eq!(expected, actual); 122 | Ok(()) 123 | } 124 | 125 | #[test] 126 | #[ignore = "It doesn't like path symbols inside a nested type like array."] 127 | fn test_path_import() -> Result<()> { 128 | let _ = WgslBindgenOptionBuilder::default() 129 | .add_entry_point("tests/shaders/basic/path_import.wgsl") 130 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 131 | .type_map(GlamWgslTypeMap) 132 | .emit_rerun_if_change(false) 133 | .skip_header_comments(true) 134 | .build()? 135 | .generate_string() 136 | .into_diagnostic()?; 137 | 138 | Ok(()) 139 | } 140 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/deptree_tests.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use indexmap::{indexmap, indexset, IndexMap}; 4 | use miette::IntoDiagnostic; 5 | use pretty_assertions::assert_eq; 6 | use wgsl_bindgen::bevy_util::DependencyTree; 7 | use wgsl_bindgen::SourceFilePath; 8 | 9 | pub type SourceDependencyMap = 10 | IndexMap>; 11 | 12 | pub fn bevy_dependency_map() -> &'static SourceDependencyMap { 13 | static MEM: OnceLock = OnceLock::new(); 14 | MEM.get_or_init(|| { 15 | indexmap![ 16 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh.wgsl") => indexmap![ 17 | "bevy_pbr::mesh_view_types" => "tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl", 18 | "bevy_pbr::mesh_view_bindings" => "tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl", 19 | "bevy_pbr::mesh_types" => "tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl", 20 | "bevy_pbr::mesh_bindings" => "tests/shaders/bevy_pbr_wgsl/mesh_bindings.wgsl", 21 | "bevy_pbr::mesh_functions" => "tests/shaders/bevy_pbr_wgsl/mesh_functions.wgsl", 22 | "bevy_pbr::mesh_vertex_output" => "tests/shaders/bevy_pbr_wgsl/mesh_vertex_output.wgsl", 23 | ], 24 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/output_VERTEX_UVS.wgsl") => indexmap![], 25 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr.wgsl") => indexmap![ 26 | "bevy_pbr::mesh_vertex_output" => "tests/shaders/bevy_pbr_wgsl/mesh_vertex_output.wgsl", 27 | "bevy_pbr::pbr::types" => "tests/shaders/bevy_pbr_wgsl/pbr/types.wgsl", 28 | "bevy_pbr::mesh_types" => "tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl", 29 | "bevy_pbr::mesh_bindings" => "tests/shaders/bevy_pbr_wgsl/mesh_bindings.wgsl", 30 | "bevy_pbr::mesh_view_types" => "tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl", 31 | "bevy_pbr::mesh_view_bindings" => "tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl", 32 | "bevy_pbr::utils" => "tests/shaders/bevy_pbr_wgsl/utils.wgsl", 33 | "bevy_pbr::pbr::lighting" => "tests/shaders/bevy_pbr_wgsl/pbr/lighting.wgsl", 34 | "bevy_pbr::clustered_forward" => "tests/shaders/bevy_pbr_wgsl/clustered_forward.wgsl", 35 | "bevy_pbr::shadows" => "tests/shaders/bevy_pbr_wgsl/shadows.wgsl", 36 | "bevy_pbr::pbr::functions" => "tests/shaders/bevy_pbr_wgsl/pbr/functions.wgsl", 37 | "bevy_pbr::pbr::bindings" => "tests/shaders/bevy_pbr_wgsl/pbr/bindings.wgsl", 38 | ], 39 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/wireframe.wgsl") => indexmap![ 40 | "bevy_pbr::mesh_types" => "tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl", 41 | "bevy_pbr::mesh_view_types" => "tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl", 42 | "bevy_pbr::mesh_view_bindings" => "tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl", 43 | "bevy_pbr::skinning" => "tests/shaders/bevy_pbr_wgsl/skinning.wgsl", 44 | "bevy_pbr::mesh_functions" => "tests/shaders/bevy_pbr_wgsl/mesh_functions.wgsl", 45 | ], 46 | ] 47 | }) 48 | } 49 | 50 | fn build_bevy_deptree() -> DependencyTree { 51 | DependencyTree::try_build( 52 | "tests/shaders/bevy_pbr_wgsl".into(), 53 | Some("bevy_pbr".into()), 54 | vec![ 55 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh.wgsl"), 56 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/output_VERTEX_UVS.wgsl"), 57 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr.wgsl"), 58 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/wireframe.wgsl"), 59 | ], 60 | vec![], 61 | ) 62 | .into_diagnostic() 63 | .expect("build_bevy_deptree error") 64 | } 65 | 66 | #[test] 67 | fn test_bevy_all_files_enumeration() { 68 | let deptree = build_bevy_deptree(); 69 | 70 | assert_eq!( 71 | indexset![ 72 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh.wgsl"), 73 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl"), 74 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl"), 75 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_bindings.wgsl"), 76 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl"), 77 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_functions.wgsl"), 78 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_vertex_output.wgsl"), 79 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/output_VERTEX_UVS.wgsl"), 80 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr.wgsl"), 81 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr/functions.wgsl"), 82 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr/types.wgsl"), 83 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr/lighting.wgsl"), 84 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/utils.wgsl"), 85 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/clustered_forward.wgsl"), 86 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/shadows.wgsl"), 87 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/pbr/bindings.wgsl"), 88 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/wireframe.wgsl"), 89 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/skinning.wgsl"), 90 | ], 91 | deptree.all_files_including_dependencies(), 92 | ) 93 | } 94 | 95 | #[test] 96 | fn test_bevy_full_dependencies() { 97 | let expected = bevy_dependency_map(); 98 | 99 | let deptree = build_bevy_deptree(); 100 | let actual = deptree 101 | .get_source_files_with_full_dependencies() 102 | .into_iter() 103 | .map(|source| { 104 | let source_path = source.source_file.file_path.clone(); 105 | let dependencies = source 106 | .full_dependencies 107 | .into_iter() 108 | .map(|dep| { 109 | let module_name = dep.module_name.as_ref().unwrap().as_str(); 110 | let module_path = dep.file_path.to_str().unwrap(); 111 | (module_name, module_path) 112 | }) 113 | .collect::>(); 114 | (source_path, dependencies) 115 | }) 116 | .collect::>(); 117 | 118 | assert_eq!(expected, &actual); 119 | } 120 | 121 | #[test] 122 | fn test_bevy_mesh_wgsl_dependency_order() { 123 | let deptree = build_bevy_deptree(); 124 | let deps = deptree 125 | .get_full_dependency_for(&SourceFilePath::new( 126 | "tests/shaders/bevy_pbr_wgsl/mesh.wgsl", 127 | )) 128 | .into_iter() 129 | .collect::>(); 130 | 131 | assert_eq!( 132 | vec![ 133 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl"), 134 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl"), 135 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl"), 136 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_bindings.wgsl"), 137 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_functions.wgsl"), 138 | SourceFilePath::new("tests/shaders/bevy_pbr_wgsl/mesh_vertex_output.wgsl") 139 | ], 140 | deps 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/issues_tests.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_to_string; 2 | 3 | use miette::{IntoDiagnostic, Result}; 4 | use pretty_assertions::assert_eq; 5 | use wgsl_bindgen::*; 6 | 7 | #[test] 8 | fn test_issue_35() -> Result<()> { 9 | WgslBindgenOptionBuilder::default() 10 | .workspace_root("test/shaders/issue_35") 11 | .add_entry_point("tests/shaders/issue_35/clear.wgsl") 12 | .skip_hash_check(true) 13 | .serialization_strategy(WgslTypeSerializeStrategy::Bytemuck) 14 | .type_map(GlamWgslTypeMap) 15 | .short_constructor(2) 16 | .shader_source_type(WgslShaderSourceType::EmbedWithNagaOilComposer) 17 | .derive_serde(false) 18 | .emit_rerun_if_change(false) 19 | .skip_header_comments(true) 20 | .output("tests/output/issue_35.actual.rs") 21 | .build()? 22 | .generate() 23 | .into_diagnostic()?; 24 | 25 | let actual = read_to_string("tests/output/issue_35.actual.rs").unwrap(); 26 | let expected = read_to_string("tests/output/issue_35.expected.rs").unwrap(); 27 | 28 | assert_eq!(expected, actual); 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/lib.rs: -------------------------------------------------------------------------------- 1 | mod bindgen_tests; 2 | mod deptree_tests; 3 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/output/.gitignore: -------------------------------------------------------------------------------- 1 | *.actual.rs -------------------------------------------------------------------------------- /wgsl_bindgen/tests/output/bindgen_minimal.expected.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused, non_snake_case, non_camel_case_types, non_upper_case_globals)] 2 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 3 | pub enum ShaderEntry { 4 | Minimal, 5 | } 6 | impl ShaderEntry { 7 | pub fn create_pipeline_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { 8 | match self { 9 | Self::Minimal => minimal::create_pipeline_layout(device), 10 | } 11 | } 12 | pub fn create_shader_module_embed_source( 13 | &self, 14 | device: &wgpu::Device, 15 | ) -> wgpu::ShaderModule { 16 | match self { 17 | Self::Minimal => minimal::create_shader_module_embed_source(device), 18 | } 19 | } 20 | } 21 | mod _root { 22 | pub use super::*; 23 | pub trait SetBindGroup { 24 | fn set_bind_group( 25 | &mut self, 26 | index: u32, 27 | bind_group: &wgpu::BindGroup, 28 | offsets: &[wgpu::DynamicOffset], 29 | ); 30 | } 31 | impl SetBindGroup for wgpu::ComputePass<'_> { 32 | fn set_bind_group( 33 | &mut self, 34 | index: u32, 35 | bind_group: &wgpu::BindGroup, 36 | offsets: &[wgpu::DynamicOffset], 37 | ) { 38 | self.set_bind_group(index, bind_group, offsets); 39 | } 40 | } 41 | } 42 | pub mod layout_asserts { 43 | use super::{_root, _root::*}; 44 | const WGSL_BASE_TYPE_ASSERTS: () = { 45 | assert!(std::mem::size_of:: < glam::Vec3A > () == 16); 46 | assert!(std::mem::align_of:: < glam::Vec3A > () == 16); 47 | assert!(std::mem::size_of:: < glam::Vec4 > () == 16); 48 | assert!(std::mem::align_of:: < glam::Vec4 > () == 16); 49 | assert!(std::mem::size_of:: < glam::Mat3A > () == 48); 50 | assert!(std::mem::align_of:: < glam::Mat3A > () == 16); 51 | assert!(std::mem::size_of:: < glam::Mat4 > () == 64); 52 | assert!(std::mem::align_of:: < glam::Mat4 > () == 16); 53 | }; 54 | const MINIMAL_UNIFORMS_ASSERTS: () = { 55 | assert!(std::mem::offset_of!(minimal::Uniforms, color) == 0); 56 | assert!(std::mem::offset_of!(minimal::Uniforms, width) == 16); 57 | assert!(std::mem::size_of:: < minimal::Uniforms > () == 256); 58 | }; 59 | } 60 | pub mod minimal { 61 | use super::{_root, _root::*}; 62 | #[repr(C, align(256))] 63 | #[derive(Debug, PartialEq, Clone, Copy)] 64 | pub struct Uniforms { 65 | /// size: 16, offset: 0x0, type: `vec4` 66 | pub color: glam::Vec4, 67 | /// size: 4, offset: 0x10, type: `f32` 68 | pub width: f32, 69 | pub _pad_width: [u8; 0x10 - ::core::mem::size_of::()], 70 | } 71 | impl Uniforms { 72 | pub const fn new(color: glam::Vec4, width: f32) -> Self { 73 | Self { 74 | color, 75 | width, 76 | _pad_width: [0; 0x10 - ::core::mem::size_of::()], 77 | } 78 | } 79 | } 80 | #[repr(C)] 81 | #[derive(Debug, PartialEq, Clone, Copy)] 82 | pub struct UniformsInit { 83 | pub color: glam::Vec4, 84 | pub width: f32, 85 | } 86 | impl UniformsInit { 87 | pub const fn build(&self) -> Uniforms { 88 | Uniforms { 89 | color: self.color, 90 | width: self.width, 91 | _pad_width: [0; 0x10 - ::core::mem::size_of::()], 92 | } 93 | } 94 | } 95 | impl From for Uniforms { 96 | fn from(data: UniformsInit) -> Self { 97 | data.build() 98 | } 99 | } 100 | pub mod compute { 101 | pub const MAIN_WORKGROUP_SIZE: [u32; 3] = [1, 1, 1]; 102 | pub fn create_main_pipeline_embed_source( 103 | device: &wgpu::Device, 104 | ) -> wgpu::ComputePipeline { 105 | let module = super::create_shader_module_embed_source(device); 106 | let layout = super::create_pipeline_layout(device); 107 | device 108 | .create_compute_pipeline( 109 | &wgpu::ComputePipelineDescriptor { 110 | label: Some("Compute Pipeline main"), 111 | layout: Some(&layout), 112 | module: &module, 113 | entry_point: Some("main"), 114 | compilation_options: Default::default(), 115 | cache: None, 116 | }, 117 | ) 118 | } 119 | } 120 | pub const ENTRY_MAIN: &str = "main"; 121 | #[derive(Debug)] 122 | pub struct WgpuBindGroup0EntriesParams<'a> { 123 | pub uniform_buf: wgpu::BufferBinding<'a>, 124 | } 125 | #[derive(Clone, Debug)] 126 | pub struct WgpuBindGroup0Entries<'a> { 127 | pub uniform_buf: wgpu::BindGroupEntry<'a>, 128 | } 129 | impl<'a> WgpuBindGroup0Entries<'a> { 130 | pub fn new(params: WgpuBindGroup0EntriesParams<'a>) -> Self { 131 | Self { 132 | uniform_buf: wgpu::BindGroupEntry { 133 | binding: 0, 134 | resource: wgpu::BindingResource::Buffer(params.uniform_buf), 135 | }, 136 | } 137 | } 138 | pub fn as_array(self) -> [wgpu::BindGroupEntry<'a>; 1] { 139 | [self.uniform_buf] 140 | } 141 | pub fn collect>>(self) -> B { 142 | self.as_array().into_iter().collect() 143 | } 144 | } 145 | #[derive(Debug)] 146 | pub struct WgpuBindGroup0(wgpu::BindGroup); 147 | impl WgpuBindGroup0 { 148 | pub const LAYOUT_DESCRIPTOR: wgpu::BindGroupLayoutDescriptor<'static> = wgpu::BindGroupLayoutDescriptor { 149 | label: Some("Minimal::BindGroup0::LayoutDescriptor"), 150 | entries: &[ 151 | /// @binding(0): "uniform_buf" 152 | wgpu::BindGroupLayoutEntry { 153 | binding: 0, 154 | visibility: wgpu::ShaderStages::COMPUTE, 155 | ty: wgpu::BindingType::Buffer { 156 | ty: wgpu::BufferBindingType::Uniform, 157 | has_dynamic_offset: false, 158 | min_binding_size: std::num::NonZeroU64::new( 159 | std::mem::size_of::<_root::minimal::Uniforms>() as _, 160 | ), 161 | }, 162 | count: None, 163 | }, 164 | ], 165 | }; 166 | pub fn get_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { 167 | device.create_bind_group_layout(&Self::LAYOUT_DESCRIPTOR) 168 | } 169 | pub fn from_bindings( 170 | device: &wgpu::Device, 171 | bindings: WgpuBindGroup0Entries, 172 | ) -> Self { 173 | let bind_group_layout = Self::get_bind_group_layout(&device); 174 | let entries = bindings.as_array(); 175 | let bind_group = device 176 | .create_bind_group( 177 | &wgpu::BindGroupDescriptor { 178 | label: Some("Minimal::BindGroup0"), 179 | layout: &bind_group_layout, 180 | entries: &entries, 181 | }, 182 | ); 183 | Self(bind_group) 184 | } 185 | pub fn set(&self, pass: &mut impl SetBindGroup) { 186 | pass.set_bind_group(0, &self.0, &[]); 187 | } 188 | } 189 | /// Bind groups can be set individually using their set(render_pass) method, or all at once using `WgpuBindGroups::set`. 190 | /// For optimal performance with many draw calls, it's recommended to organize bindings into bind groups based on update frequency: 191 | /// - Bind group 0: Least frequent updates (e.g. per frame resources) 192 | /// - Bind group 1: More frequent updates 193 | /// - Bind group 2: More frequent updates 194 | /// - Bind group 3: Most frequent updates (e.g. per draw resources) 195 | #[derive(Debug, Copy, Clone)] 196 | pub struct WgpuBindGroups<'a> { 197 | pub bind_group0: &'a WgpuBindGroup0, 198 | } 199 | impl<'a> WgpuBindGroups<'a> { 200 | pub fn set(&self, pass: &mut impl SetBindGroup) { 201 | self.bind_group0.set(pass); 202 | } 203 | } 204 | #[derive(Debug)] 205 | pub struct WgpuPipelineLayout; 206 | impl WgpuPipelineLayout { 207 | pub fn bind_group_layout_entries( 208 | entries: [wgpu::BindGroupLayout; 1], 209 | ) -> [wgpu::BindGroupLayout; 1] { 210 | entries 211 | } 212 | } 213 | pub fn create_pipeline_layout(device: &wgpu::Device) -> wgpu::PipelineLayout { 214 | device 215 | .create_pipeline_layout( 216 | &wgpu::PipelineLayoutDescriptor { 217 | label: Some("Minimal::PipelineLayout"), 218 | bind_group_layouts: &[ 219 | &WgpuBindGroup0::get_bind_group_layout(device), 220 | ], 221 | push_constant_ranges: &[], 222 | }, 223 | ) 224 | } 225 | pub fn create_shader_module_embed_source( 226 | device: &wgpu::Device, 227 | ) -> wgpu::ShaderModule { 228 | let source = std::borrow::Cow::Borrowed(SHADER_STRING); 229 | device 230 | .create_shader_module(wgpu::ShaderModuleDescriptor { 231 | label: Some("minimal.wgsl"), 232 | source: wgpu::ShaderSource::Wgsl(source), 233 | }) 234 | } 235 | pub const SHADER_STRING: &'static str = r#" 236 | struct Uniforms { 237 | color: vec4, 238 | width: f32, 239 | } 240 | 241 | @group(0) @binding(0) 242 | var uniform_buf: Uniforms; 243 | 244 | @compute @workgroup_size(1, 1, 1) 245 | fn main(@builtin(global_invocation_id) id: vec3) { 246 | return; 247 | } 248 | "#; 249 | } 250 | pub mod bytemuck_impls { 251 | use super::{_root, _root::*}; 252 | unsafe impl bytemuck::Zeroable for minimal::Uniforms {} 253 | unsafe impl bytemuck::Pod for minimal::Uniforms {} 254 | } 255 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/output/bindgen_padding.expected.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused, non_snake_case, non_camel_case_types, non_upper_case_globals)] 2 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 3 | pub enum ShaderEntry { 4 | Padding, 5 | } 6 | impl ShaderEntry { 7 | pub fn create_pipeline_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { 8 | match self { 9 | Self::Padding => padding::create_pipeline_layout(device), 10 | } 11 | } 12 | pub fn create_shader_module_embed_source( 13 | &self, 14 | device: &wgpu::Device, 15 | ) -> wgpu::ShaderModule { 16 | match self { 17 | Self::Padding => padding::create_shader_module_embed_source(device), 18 | } 19 | } 20 | } 21 | mod _root { 22 | pub use super::*; 23 | pub trait SetBindGroup { 24 | fn set_bind_group( 25 | &mut self, 26 | index: u32, 27 | bind_group: &wgpu::BindGroup, 28 | offsets: &[wgpu::DynamicOffset], 29 | ); 30 | } 31 | impl SetBindGroup for wgpu::ComputePass<'_> { 32 | fn set_bind_group( 33 | &mut self, 34 | index: u32, 35 | bind_group: &wgpu::BindGroup, 36 | offsets: &[wgpu::DynamicOffset], 37 | ) { 38 | self.set_bind_group(index, bind_group, offsets); 39 | } 40 | } 41 | } 42 | pub mod layout_asserts { 43 | use super::{_root, _root::*}; 44 | const WGSL_BASE_TYPE_ASSERTS: () = { 45 | assert!(std::mem::size_of:: < glam::Vec3A > () == 16); 46 | assert!(std::mem::align_of:: < glam::Vec3A > () == 16); 47 | assert!(std::mem::size_of:: < glam::Vec4 > () == 16); 48 | assert!(std::mem::align_of:: < glam::Vec4 > () == 16); 49 | assert!(std::mem::size_of:: < glam::Mat3A > () == 48); 50 | assert!(std::mem::align_of:: < glam::Mat3A > () == 16); 51 | assert!(std::mem::size_of:: < glam::Mat4 > () == 64); 52 | assert!(std::mem::align_of:: < glam::Mat4 > () == 16); 53 | }; 54 | const PADDING_STYLE_ASSERTS: () = { 55 | assert!(std::mem::offset_of!(padding::Style, color) == 0); 56 | assert!(std::mem::offset_of!(padding::Style, width) == 16); 57 | assert!(std::mem::size_of:: < padding::Style > () == 32); 58 | }; 59 | } 60 | pub mod padding { 61 | use super::{_root, _root::*}; 62 | #[repr(C, align(16))] 63 | #[derive(Debug, PartialEq, Clone, Copy)] 64 | pub struct Style { 65 | /// size: 16, offset: 0x0, type: `vec4` 66 | pub color: glam::Vec4, 67 | /// size: 4, offset: 0x10, type: `f32` 68 | pub width: f32, 69 | pub _pad_width: [u8; 0x8 - ::core::mem::size_of::()], 70 | pub _padding: [u8; 0x8], 71 | } 72 | impl Style { 73 | pub const fn new(color: glam::Vec4, width: f32) -> Self { 74 | Self { 75 | color, 76 | width, 77 | _pad_width: [0; 0x8 - ::core::mem::size_of::()], 78 | _padding: [0; 0x8], 79 | } 80 | } 81 | } 82 | #[repr(C)] 83 | #[derive(Debug, PartialEq, Clone, Copy)] 84 | pub struct StyleInit { 85 | pub color: glam::Vec4, 86 | pub width: f32, 87 | } 88 | impl StyleInit { 89 | pub const fn build(&self) -> Style { 90 | Style { 91 | color: self.color, 92 | width: self.width, 93 | _pad_width: [0; 0x8 - ::core::mem::size_of::()], 94 | _padding: [0; 0x8], 95 | } 96 | } 97 | } 98 | impl From for Style { 99 | fn from(data: StyleInit) -> Self { 100 | data.build() 101 | } 102 | } 103 | pub mod compute { 104 | pub const MAIN_WORKGROUP_SIZE: [u32; 3] = [1, 1, 1]; 105 | pub fn create_main_pipeline_embed_source( 106 | device: &wgpu::Device, 107 | ) -> wgpu::ComputePipeline { 108 | let module = super::create_shader_module_embed_source(device); 109 | let layout = super::create_pipeline_layout(device); 110 | device 111 | .create_compute_pipeline( 112 | &wgpu::ComputePipelineDescriptor { 113 | label: Some("Compute Pipeline main"), 114 | layout: Some(&layout), 115 | module: &module, 116 | entry_point: Some("main"), 117 | compilation_options: Default::default(), 118 | cache: None, 119 | }, 120 | ) 121 | } 122 | } 123 | pub const ENTRY_MAIN: &str = "main"; 124 | #[derive(Debug)] 125 | pub struct WgpuBindGroup0EntriesParams<'a> { 126 | pub frame: wgpu::BufferBinding<'a>, 127 | } 128 | #[derive(Clone, Debug)] 129 | pub struct WgpuBindGroup0Entries<'a> { 130 | pub frame: wgpu::BindGroupEntry<'a>, 131 | } 132 | impl<'a> WgpuBindGroup0Entries<'a> { 133 | pub fn new(params: WgpuBindGroup0EntriesParams<'a>) -> Self { 134 | Self { 135 | frame: wgpu::BindGroupEntry { 136 | binding: 0, 137 | resource: wgpu::BindingResource::Buffer(params.frame), 138 | }, 139 | } 140 | } 141 | pub fn as_array(self) -> [wgpu::BindGroupEntry<'a>; 1] { 142 | [self.frame] 143 | } 144 | pub fn collect>>(self) -> B { 145 | self.as_array().into_iter().collect() 146 | } 147 | } 148 | #[derive(Debug)] 149 | pub struct WgpuBindGroup0(wgpu::BindGroup); 150 | impl WgpuBindGroup0 { 151 | pub const LAYOUT_DESCRIPTOR: wgpu::BindGroupLayoutDescriptor<'static> = wgpu::BindGroupLayoutDescriptor { 152 | label: Some("Padding::BindGroup0::LayoutDescriptor"), 153 | entries: &[ 154 | /// @binding(0): "frame" 155 | wgpu::BindGroupLayoutEntry { 156 | binding: 0, 157 | visibility: wgpu::ShaderStages::COMPUTE, 158 | ty: wgpu::BindingType::Buffer { 159 | ty: wgpu::BufferBindingType::Storage { 160 | read_only: true, 161 | }, 162 | has_dynamic_offset: false, 163 | min_binding_size: std::num::NonZeroU64::new( 164 | std::mem::size_of::<_root::padding::Style>() as _, 165 | ), 166 | }, 167 | count: None, 168 | }, 169 | ], 170 | }; 171 | pub fn get_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { 172 | device.create_bind_group_layout(&Self::LAYOUT_DESCRIPTOR) 173 | } 174 | pub fn from_bindings( 175 | device: &wgpu::Device, 176 | bindings: WgpuBindGroup0Entries, 177 | ) -> Self { 178 | let bind_group_layout = Self::get_bind_group_layout(&device); 179 | let entries = bindings.as_array(); 180 | let bind_group = device 181 | .create_bind_group( 182 | &wgpu::BindGroupDescriptor { 183 | label: Some("Padding::BindGroup0"), 184 | layout: &bind_group_layout, 185 | entries: &entries, 186 | }, 187 | ); 188 | Self(bind_group) 189 | } 190 | pub fn set(&self, pass: &mut impl SetBindGroup) { 191 | pass.set_bind_group(0, &self.0, &[]); 192 | } 193 | } 194 | /// Bind groups can be set individually using their set(render_pass) method, or all at once using `WgpuBindGroups::set`. 195 | /// For optimal performance with many draw calls, it's recommended to organize bindings into bind groups based on update frequency: 196 | /// - Bind group 0: Least frequent updates (e.g. per frame resources) 197 | /// - Bind group 1: More frequent updates 198 | /// - Bind group 2: More frequent updates 199 | /// - Bind group 3: Most frequent updates (e.g. per draw resources) 200 | #[derive(Debug, Copy, Clone)] 201 | pub struct WgpuBindGroups<'a> { 202 | pub bind_group0: &'a WgpuBindGroup0, 203 | } 204 | impl<'a> WgpuBindGroups<'a> { 205 | pub fn set(&self, pass: &mut impl SetBindGroup) { 206 | self.bind_group0.set(pass); 207 | } 208 | } 209 | #[derive(Debug)] 210 | pub struct WgpuPipelineLayout; 211 | impl WgpuPipelineLayout { 212 | pub fn bind_group_layout_entries( 213 | entries: [wgpu::BindGroupLayout; 1], 214 | ) -> [wgpu::BindGroupLayout; 1] { 215 | entries 216 | } 217 | } 218 | pub fn create_pipeline_layout(device: &wgpu::Device) -> wgpu::PipelineLayout { 219 | device 220 | .create_pipeline_layout( 221 | &wgpu::PipelineLayoutDescriptor { 222 | label: Some("Padding::PipelineLayout"), 223 | bind_group_layouts: &[ 224 | &WgpuBindGroup0::get_bind_group_layout(device), 225 | ], 226 | push_constant_ranges: &[], 227 | }, 228 | ) 229 | } 230 | pub fn create_shader_module_embed_source( 231 | device: &wgpu::Device, 232 | ) -> wgpu::ShaderModule { 233 | let source = std::borrow::Cow::Borrowed(SHADER_STRING); 234 | device 235 | .create_shader_module(wgpu::ShaderModuleDescriptor { 236 | label: Some("padding.wgsl"), 237 | source: wgpu::ShaderSource::Wgsl(source), 238 | }) 239 | } 240 | pub const SHADER_STRING: &'static str = r#" 241 | struct Style { 242 | color: vec4, 243 | width: f32, 244 | _padding: vec2, 245 | } 246 | 247 | @group(0) @binding(0) 248 | var frame: Style; 249 | 250 | @compute @workgroup_size(1, 1, 1) 251 | fn main(@builtin(global_invocation_id) id: vec3) { 252 | return; 253 | } 254 | "#; 255 | } 256 | pub mod bytemuck_impls { 257 | use super::{_root, _root::*}; 258 | unsafe impl bytemuck::Zeroable for padding::Style {} 259 | unsafe impl bytemuck::Pod for padding::Style {} 260 | } 261 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/output/issue_35.expected.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused, non_snake_case, non_camel_case_types, non_upper_case_globals)] 2 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 3 | pub enum ShaderEntry { 4 | Clear, 5 | } 6 | impl ShaderEntry { 7 | pub fn create_pipeline_layout(&self, device: &wgpu::Device) -> wgpu::PipelineLayout { 8 | match self { 9 | Self::Clear => clear::create_pipeline_layout(device), 10 | } 11 | } 12 | pub fn create_shader_module_embedded( 13 | &self, 14 | device: &wgpu::Device, 15 | shader_defs: std::collections::HashMap, 16 | ) -> Result { 17 | match self { 18 | Self::Clear => clear::create_shader_module_embedded(device, shader_defs), 19 | } 20 | } 21 | pub fn load_shader_module_embedded( 22 | &self, 23 | composer: &mut naga_oil::compose::Composer, 24 | shader_defs: std::collections::HashMap, 25 | ) -> Result { 26 | match self { 27 | Self::Clear => clear::load_shader_module_embedded(composer, shader_defs), 28 | } 29 | } 30 | } 31 | mod _root { 32 | pub use super::*; 33 | } 34 | pub mod layout_asserts { 35 | use super::{_root, _root::*}; 36 | const WGSL_BASE_TYPE_ASSERTS: () = { 37 | assert!(std::mem::size_of:: < glam::Vec3A > () == 16); 38 | assert!(std::mem::align_of:: < glam::Vec3A > () == 16); 39 | assert!(std::mem::size_of:: < glam::Vec4 > () == 16); 40 | assert!(std::mem::align_of:: < glam::Vec4 > () == 16); 41 | assert!(std::mem::size_of:: < glam::Mat3A > () == 48); 42 | assert!(std::mem::align_of:: < glam::Mat3A > () == 16); 43 | assert!(std::mem::size_of:: < glam::Mat4 > () == 64); 44 | assert!(std::mem::align_of:: < glam::Mat4 > () == 16); 45 | }; 46 | } 47 | pub mod vertices { 48 | use super::{_root, _root::*}; 49 | #[repr(C)] 50 | #[derive(Debug, PartialEq, Clone, Copy)] 51 | pub struct VertexIn { 52 | pub position: glam::Vec4, 53 | } 54 | pub const fn VertexIn(position: glam::Vec4) -> VertexIn { 55 | VertexIn { position } 56 | } 57 | impl VertexIn { 58 | pub const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 1] = [ 59 | wgpu::VertexAttribute { 60 | format: wgpu::VertexFormat::Float32x4, 61 | offset: std::mem::offset_of!(Self, position) as u64, 62 | shader_location: 0, 63 | }, 64 | ]; 65 | pub const fn vertex_buffer_layout( 66 | step_mode: wgpu::VertexStepMode, 67 | ) -> wgpu::VertexBufferLayout<'static> { 68 | wgpu::VertexBufferLayout { 69 | array_stride: std::mem::size_of::() as u64, 70 | step_mode, 71 | attributes: &Self::VERTEX_ATTRIBUTES, 72 | } 73 | } 74 | } 75 | } 76 | pub mod bytemuck_impls { 77 | use super::{_root, _root::*}; 78 | unsafe impl bytemuck::Zeroable for vertices::VertexIn {} 79 | unsafe impl bytemuck::Pod for vertices::VertexIn {} 80 | } 81 | pub mod clear { 82 | use super::{_root, _root::*}; 83 | pub const ENTRY_VERTEX_MAIN: &str = "vertex_main"; 84 | pub const ENTRY_FRAGMENT_MAIN: &str = "fragment_main"; 85 | #[derive(Debug)] 86 | pub struct VertexEntry { 87 | pub entry_point: &'static str, 88 | pub buffers: [wgpu::VertexBufferLayout<'static>; N], 89 | pub constants: std::collections::HashMap, 90 | } 91 | pub fn vertex_state<'a, const N: usize>( 92 | module: &'a wgpu::ShaderModule, 93 | entry: &'a VertexEntry, 94 | ) -> wgpu::VertexState<'a> { 95 | wgpu::VertexState { 96 | module, 97 | entry_point: Some(entry.entry_point), 98 | buffers: &entry.buffers, 99 | compilation_options: wgpu::PipelineCompilationOptions { 100 | constants: &entry.constants, 101 | ..Default::default() 102 | }, 103 | } 104 | } 105 | pub fn vertex_main_entry(vertex_in: wgpu::VertexStepMode) -> VertexEntry<1> { 106 | VertexEntry { 107 | entry_point: ENTRY_VERTEX_MAIN, 108 | buffers: [vertices::VertexIn::vertex_buffer_layout(vertex_in)], 109 | constants: Default::default(), 110 | } 111 | } 112 | #[derive(Debug)] 113 | pub struct FragmentEntry { 114 | pub entry_point: &'static str, 115 | pub targets: [Option; N], 116 | pub constants: std::collections::HashMap, 117 | } 118 | pub fn fragment_state<'a, const N: usize>( 119 | module: &'a wgpu::ShaderModule, 120 | entry: &'a FragmentEntry, 121 | ) -> wgpu::FragmentState<'a> { 122 | wgpu::FragmentState { 123 | module, 124 | entry_point: Some(entry.entry_point), 125 | targets: &entry.targets, 126 | compilation_options: wgpu::PipelineCompilationOptions { 127 | constants: &entry.constants, 128 | ..Default::default() 129 | }, 130 | } 131 | } 132 | pub fn fragment_main_entry( 133 | targets: [Option; 1], 134 | ) -> FragmentEntry<1> { 135 | FragmentEntry { 136 | entry_point: ENTRY_FRAGMENT_MAIN, 137 | targets, 138 | constants: Default::default(), 139 | } 140 | } 141 | #[derive(Debug)] 142 | pub struct WgpuPipelineLayout; 143 | impl WgpuPipelineLayout { 144 | pub fn bind_group_layout_entries( 145 | entries: [wgpu::BindGroupLayout; 0], 146 | ) -> [wgpu::BindGroupLayout; 0] { 147 | entries 148 | } 149 | } 150 | pub fn create_pipeline_layout(device: &wgpu::Device) -> wgpu::PipelineLayout { 151 | device 152 | .create_pipeline_layout( 153 | &wgpu::PipelineLayoutDescriptor { 154 | label: Some("Clear::PipelineLayout"), 155 | bind_group_layouts: &[], 156 | push_constant_ranges: &[], 157 | }, 158 | ) 159 | } 160 | pub fn load_shader_module_embedded( 161 | composer: &mut naga_oil::compose::Composer, 162 | shader_defs: std::collections::HashMap, 163 | ) -> Result { 164 | composer 165 | .add_composable_module(naga_oil::compose::ComposableModuleDescriptor { 166 | source: include_str!("../shaders/issue_35/vertices.wgsl"), 167 | file_path: "../shaders/issue_35/vertices.wgsl", 168 | language: naga_oil::compose::ShaderLanguage::Wgsl, 169 | shader_defs: shader_defs.clone(), 170 | as_name: Some("vertices".into()), 171 | ..Default::default() 172 | })?; 173 | composer 174 | .make_naga_module(naga_oil::compose::NagaModuleDescriptor { 175 | source: include_str!("../shaders/issue_35/clear.wgsl"), 176 | file_path: "../shaders/issue_35/clear.wgsl", 177 | shader_defs, 178 | ..Default::default() 179 | }) 180 | } 181 | pub fn create_shader_module_embedded( 182 | device: &wgpu::Device, 183 | shader_defs: std::collections::HashMap, 184 | ) -> Result { 185 | let mut composer = naga_oil::compose::Composer::default(); 186 | let module = load_shader_module_embedded(&mut composer, shader_defs)?; 187 | let info = wgpu::naga::valid::Validator::new( 188 | wgpu::naga::valid::ValidationFlags::empty(), 189 | wgpu::naga::valid::Capabilities::all(), 190 | ) 191 | .validate(&module) 192 | .unwrap(); 193 | let shader_string = wgpu::naga::back::wgsl::write_string( 194 | &module, 195 | &info, 196 | wgpu::naga::back::wgsl::WriterFlags::empty(), 197 | ) 198 | .expect("failed to convert naga module to source"); 199 | let source = std::borrow::Cow::Owned(shader_string); 200 | let shader_module = device 201 | .create_shader_module(wgpu::ShaderModuleDescriptor { 202 | label: Some("clear.wgsl"), 203 | source: wgpu::ShaderSource::Wgsl(source), 204 | }); 205 | Ok(shader_module) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/additional/types.wgsl: -------------------------------------------------------------------------------- 1 | struct Fp64 { 2 | high: f32, 3 | low: f32 4 | } -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/basic/bindings.wgsl: -------------------------------------------------------------------------------- 1 | @group(1) @binding(0) 2 | var ONE: f32; 3 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/basic/main.wgsl: -------------------------------------------------------------------------------- 1 | #import bindings; 2 | #import types::{Fp64}; 3 | 4 | struct Style { 5 | color: vec4f, 6 | width: f32, 7 | } 8 | 9 | @group(0) @binding(0) 10 | var buffer: array; 11 | 12 | @group(0) @binding(1) 13 | var texture_float: texture_2d; 14 | 15 | @group(0) @binding(2) 16 | var texture_sint: texture_2d; 17 | 18 | @group(0) @binding(3) 19 | var texture_uint: texture_2d; 20 | 21 | var const_style: Style; 22 | 23 | @compute @workgroup_size(1) 24 | fn main(@builtin(global_invocation_id) id: vec3) { 25 | buffer[id.x] *= 2 * bindings::ONE * const_style.color.a * const_style.width; 26 | } 27 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/basic/path_import.wgsl: -------------------------------------------------------------------------------- 1 | #import "../../../example/src/more-shader-files/reachme" as reachme 2 | 3 | @group(0) @binding(0) 4 | var rts: array; 5 | 6 | @compute @workgroup_size(1) 7 | fn main(@builtin(global_invocation_id) id: vec3) { 8 | // buffer[id.x] *= 2 * other::ONE; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/clustered_forward.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::clustered_forward 2 | 3 | #import bevy_pbr::mesh_view_bindings as Bindings 4 | 5 | // NOTE: Keep in sync with bevy_pbr/src/light.rs 6 | fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { 7 | var z_slice: u32 = 0u; 8 | if (is_orthographic) { 9 | // NOTE: view_z is correct in the orthographic case 10 | z_slice = u32(floor((view_z - Bindings::lights.cluster_factors.z) * Bindings::lights.cluster_factors.w)); 11 | } else { 12 | // NOTE: had to use -view_z to make it positive else log(negative) is nan 13 | z_slice = u32(log(-view_z) * Bindings::lights.cluster_factors.z - Bindings::lights.cluster_factors.w + 1.0); 14 | } 15 | // NOTE: We use min as we may limit the far z plane used for clustering to be closeer than 16 | // the furthest thing being drawn. This means that we need to limit to the maximum cluster. 17 | return min(z_slice, Bindings::lights.cluster_dimensions.z - 1u); 18 | } 19 | 20 | fn fragment_cluster_index(frag_coord: vec2, view_z: f32, is_orthographic: bool) -> u32 { 21 | let xy = vec2(floor(frag_coord * Bindings::lights.cluster_factors.xy)); 22 | let z_slice = view_z_to_z_slice(view_z, is_orthographic); 23 | // NOTE: Restricting cluster index to avoid undefined behavior when accessing uniform buffer 24 | // arrays based on the cluster index. 25 | return min( 26 | (xy.y * Bindings::lights.cluster_dimensions.x + xy.x) * Bindings::lights.cluster_dimensions.z + z_slice, 27 | Bindings::lights.cluster_dimensions.w - 1u 28 | ); 29 | } 30 | 31 | // this must match CLUSTER_COUNT_SIZE in light.rs 32 | const CLUSTER_COUNT_SIZE = 9u; 33 | fn unpack_offset_and_counts(cluster_index: u32) -> vec3 { 34 | #ifdef NO_STORAGE_BUFFERS_SUPPORT 35 | let offset_and_counts = Bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; 36 | // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] 37 | // [ offset | point light count | spot light count ] 38 | return vec3( 39 | (offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), 40 | (offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u), 41 | offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u), 42 | ); 43 | #else 44 | return Bindings::cluster_offsets_and_counts.data[cluster_index].xyz; 45 | #endif 46 | } 47 | 48 | fn get_light_id(index: u32) -> u32 { 49 | #ifdef NO_STORAGE_BUFFERS_SUPPORT 50 | // The index is correct but in cluster_light_index_lists we pack 4 u8s into a u32 51 | // This means the index into cluster_light_index_lists is index / 4 52 | let indices = Bindings::cluster_light_index_lists.data[index >> 4u][(index >> 2u) & ((1u << 2u) - 1u)]; 53 | // And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index 54 | return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u); 55 | #else 56 | return Bindings::cluster_light_index_lists.data[index]; 57 | #endif 58 | } 59 | 60 | fn cluster_debug_visualization( 61 | output_color: vec4, 62 | view_z: f32, 63 | is_orthographic: bool, 64 | offset_and_counts: vec3, 65 | cluster_index: u32, 66 | ) -> vec4 { 67 | // Cluster allocation debug (using 'over' alpha blending) 68 | #ifdef CLUSTERED_FORWARD_DEBUG_Z_SLICES 69 | // NOTE: This debug mode visualises the z-slices 70 | let cluster_overlay_alpha = 0.1; 71 | var z_slice: u32 = view_z_to_z_slice(view_z, is_orthographic); 72 | // A hack to make the colors alternate a bit more 73 | if ((z_slice & 1u) == 1u) { 74 | z_slice = z_slice + Bindings::lights.cluster_dimensions.z / 2u; 75 | } 76 | let slice_color = hsv2rgb(f32(z_slice) / f32(Bindings::lights.cluster_dimensions.z + 1u), 1.0, 0.5); 77 | output_color = vec4( 78 | (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color, 79 | output_color.a 80 | ); 81 | #endif // CLUSTERED_FORWARD_DEBUG_Z_SLICES 82 | #ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_LIGHT_COMPLEXITY 83 | // NOTE: This debug mode visualises the number of lights within the cluster that contains 84 | // the fragment. It shows a sort of lighting complexity measure. 85 | let cluster_overlay_alpha = 0.1; 86 | let max_light_complexity_per_cluster = 64.0; 87 | output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r 88 | + cluster_overlay_alpha * smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_counts[1] + offset_and_counts[2])); 89 | output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g 90 | + cluster_overlay_alpha * (1.0 - smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_counts[1] + offset_and_counts[2]))); 91 | #endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_LIGHT_COMPLEXITY 92 | #ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY 93 | // NOTE: Visualizes the cluster to which the fragment belongs 94 | let cluster_overlay_alpha = 0.1; 95 | let cluster_color = hsv2rgb(random1D(f32(cluster_index)), 1.0, 0.5); 96 | output_color = vec4( 97 | (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color, 98 | output_color.a 99 | ); 100 | #endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY 101 | 102 | return output_color; 103 | } 104 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/depth.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::mesh_view_types 2 | #import bevy_pbr::mesh_types 3 | 4 | @group(0) @binding(0) 5 | var view: View; 6 | 7 | @group(1) @binding(0) 8 | var mesh: Mesh; 9 | 10 | #ifdef SKINNED 11 | @group(1) @binding(1) 12 | var joint_matrices: SkinnedMesh; 13 | #import bevy_pbr::skinning 14 | #endif 15 | 16 | // NOTE: Bindings must come before functions that use them! 17 | #import bevy_pbr::mesh_functions 18 | 19 | struct Vertex { 20 | @location(0) position: vec3, 21 | #ifdef SKINNED 22 | @location(4) joint_indices: vec4, 23 | @location(5) joint_weights: vec4, 24 | #endif 25 | }; 26 | 27 | struct VertexOutput { 28 | @builtin(position) clip_position: vec4, 29 | }; 30 | 31 | @vertex 32 | fn vertex(vertex: Vertex) -> VertexOutput { 33 | #ifdef SKINNED 34 | let model = skin_model(vertex.joint_indices, vertex.joint_weights); 35 | #else 36 | let model = mesh.model; 37 | #endif 38 | 39 | var out: VertexOutput; 40 | out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); 41 | return out; 42 | } 43 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::mesh_view_bindings 2 | #import bevy_pbr::mesh_bindings 3 | 4 | // NOTE: Bindings must come before functions that use them! 5 | #import bevy_pbr::mesh_functions 6 | 7 | struct Vertex { 8 | @location(0) position: vec3, 9 | @location(1) normal: vec3, 10 | #ifdef VERTEX_UVS 11 | @location(2) uv: vec2, 12 | #endif 13 | #ifdef VERTEX_TANGENTS 14 | @location(3) tangent: vec4, 15 | #endif 16 | #ifdef VERTEX_COLORS 17 | @location(4) color: vec4, 18 | #endif 19 | #ifdef SKINNED 20 | @location(5) joint_indices: vec4, 21 | @location(6) joint_weights: vec4, 22 | #endif 23 | }; 24 | 25 | struct VertexOutput { 26 | @builtin(position) clip_position: vec4, 27 | #import bevy_pbr::mesh_vertex_output 28 | }; 29 | 30 | @vertex 31 | fn vertex(vertex: Vertex) -> VertexOutput { 32 | var out: VertexOutput; 33 | #ifdef SKINNED 34 | var model = skin_model(vertex.joint_indices, vertex.joint_weights); 35 | out.world_normal = skin_normals(model, vertex.normal); 36 | #else 37 | var model = mesh.model; 38 | out.world_normal = mesh_normal_local_to_world(vertex.normal); 39 | #endif 40 | out.world_position = mesh_position_local_to_world(model, vec4(vertex.position, 1.0)); 41 | #ifdef VERTEX_UVS 42 | out.uv = vertex.uv; 43 | #endif 44 | #ifdef VERTEX_TANGENTS 45 | out.world_tangent = mesh_tangent_local_to_world(model, vertex.tangent); 46 | #endif 47 | #ifdef VERTEX_COLORS 48 | out.color = vertex.color; 49 | #endif 50 | 51 | out.clip_position = mesh_position_world_to_clip(out.world_position); 52 | return out; 53 | } 54 | 55 | struct FragmentInput { 56 | @builtin(front_facing) is_front: bool, 57 | #import bevy_pbr::mesh_vertex_output 58 | }; 59 | 60 | @fragment 61 | fn fragment(in: FragmentInput) -> @location(0) vec4 { 62 | #ifdef VERTEX_COLORS 63 | return in.color; 64 | #else 65 | return vec4(1.0, 0.0, 1.0, 1.0); 66 | #endif 67 | } 68 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_bindings.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_bindings 2 | 3 | #import bevy_pbr::mesh_types as Types 4 | 5 | @group(2) @binding(0) 6 | var mesh: Types::Mesh; 7 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_functions.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_functions 2 | 3 | fn mesh_position_local_to_world(model: mat4x4, vertex_position: vec4) -> vec4 { 4 | return model * vertex_position; 5 | } 6 | 7 | fn mesh_position_world_to_clip(world_position: vec4) -> vec4 { 8 | return view.view_proj * world_position; 9 | } 10 | 11 | // NOTE: The intermediate world_position assignment is important 12 | // for precision purposes when using the 'equals' depth comparison 13 | // function. 14 | fn mesh_position_local_to_clip(model: mat4x4, vertex_position: vec4) -> vec4 { 15 | let world_position = mesh_position_local_to_world(model, vertex_position); 16 | return mesh_position_world_to_clip(world_position); 17 | } 18 | 19 | fn mesh_normal_local_to_world(vertex_normal: vec3) -> vec3 { 20 | return mat3x3( 21 | mesh.inverse_transpose_model[0].xyz, 22 | mesh.inverse_transpose_model[1].xyz, 23 | mesh.inverse_transpose_model[2].xyz 24 | ) * vertex_normal; 25 | } 26 | 27 | fn mesh_tangent_local_to_world(model: mat4x4, vertex_tangent: vec4) -> vec4 { 28 | return vec4( 29 | mat3x3( 30 | model[0].xyz, 31 | model[1].xyz, 32 | model[2].xyz 33 | ) * vertex_tangent.xyz, 34 | vertex_tangent.w 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_types.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_types 2 | 3 | struct Mesh { 4 | model: mat4x4, 5 | inverse_transpose_model: mat4x4, 6 | // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. 7 | flags: u32, 8 | }; 9 | 10 | #ifdef SKINNED 11 | struct SkinnedMesh { 12 | data: array, 256u>, 13 | }; 14 | #endif 15 | 16 | const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u; 17 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_vertex_output.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_vertex_output 2 | 3 | struct MeshVertexOutput { 4 | @location(0) world_position: vec4, 5 | @location(1) world_normal: vec3, 6 | #ifdef VERTEX_UVS 7 | @location(2) uv: vec2, 8 | #endif 9 | #ifdef VERTEX_TANGENTS 10 | @location(3) world_tangent: vec4, 11 | #endif 12 | #ifdef VERTEX_COLORS 13 | @location(4) color: vec4, 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_view_bindings.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_view_bindings 2 | 3 | #import bevy_pbr::mesh_view_types as Types 4 | 5 | @group(0) @binding(0) 6 | var view: Types::View; 7 | @group(0) @binding(1) 8 | var lights: Types::Lights; 9 | #ifdef NO_ARRAY_TEXTURES_SUPPORT 10 | @group(0) @binding(2) 11 | var point_shadow_textures: texture_depth_cube; 12 | #else 13 | @group(0) @binding(2) 14 | var point_shadow_textures: texture_depth_cube_array; 15 | #endif 16 | @group(0) @binding(3) 17 | var point_shadow_textures_sampler: sampler_comparison; 18 | #ifdef NO_ARRAY_TEXTURES_SUPPORT 19 | @group(0) @binding(4) 20 | var directional_shadow_textures: texture_depth_2d; 21 | #else 22 | @group(0) @binding(4) 23 | var directional_shadow_textures: texture_depth_2d_array; 24 | #endif 25 | @group(0) @binding(5) 26 | var directional_shadow_textures_sampler: sampler_comparison; 27 | 28 | #ifdef NO_STORAGE_BUFFERS_SUPPORT 29 | @group(0) @binding(6) 30 | var point_lights: Types::PointLights; 31 | @group(0) @binding(7) 32 | var cluster_light_index_lists: Types::ClusterLightIndexLists; 33 | @group(0) @binding(8) 34 | var cluster_offsets_and_counts: Types::ClusterOffsetsAndCounts; 35 | #else 36 | @group(0) @binding(6) 37 | var point_lights: Types::PointLights; 38 | @group(0) @binding(7) 39 | var cluster_light_index_lists: Types::ClusterLightIndexLists; 40 | @group(0) @binding(8) 41 | var cluster_offsets_and_counts: Types::ClusterOffsetsAndCounts; 42 | #endif 43 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/mesh_view_types.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::mesh_view_types 2 | 3 | struct View { 4 | view_proj: mat4x4, 5 | inverse_view_proj: mat4x4, 6 | view: mat4x4, 7 | inverse_view: mat4x4, 8 | projection: mat4x4, 9 | inverse_projection: mat4x4, 10 | world_position: vec3, 11 | width: f32, 12 | height: f32, 13 | }; 14 | 15 | struct PointLight { 16 | // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] 17 | // For spot lights: the direction (x,z), spot_scale and spot_offset 18 | light_custom_data: vec4, 19 | color_inverse_square_range: vec4, 20 | position_radius: vec4, 21 | // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. 22 | flags: u32, 23 | shadow_depth_bias: f32, 24 | shadow_normal_bias: f32, 25 | spot_light_tan_angle: f32, 26 | }; 27 | 28 | const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; 29 | const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u; 30 | 31 | struct DirectionalLight { 32 | view_projection: mat4x4, 33 | color: vec4, 34 | direction_to_light: vec3, 35 | // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. 36 | flags: u32, 37 | shadow_depth_bias: f32, 38 | shadow_normal_bias: f32, 39 | }; 40 | 41 | const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; 42 | 43 | struct Lights { 44 | // NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs 45 | directional_lights: array, 46 | ambient_color: vec4, 47 | // x/y/z dimensions and n_clusters in w 48 | cluster_dimensions: vec4, 49 | // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) 50 | // 51 | // For perspective projections: 52 | // z is cluster_dimensions.z / log(far / near) 53 | // w is cluster_dimensions.z * log(near) / log(far / near) 54 | // 55 | // For orthographic projections: 56 | // NOTE: near and far are +ve but -z is infront of the camera 57 | // z is -near 58 | // w is cluster_dimensions.z / (-far - -near) 59 | cluster_factors: vec4, 60 | n_directional_lights: u32, 61 | spot_light_shadowmap_offset: i32, 62 | }; 63 | 64 | #ifdef NO_STORAGE_BUFFERS_SUPPORT 65 | struct PointLights { 66 | data: array, 67 | }; 68 | struct ClusterLightIndexLists { 69 | // each u32 contains 4 u8 indices into the PointLights array 70 | data: array, 1024u>, 71 | }; 72 | struct ClusterOffsetsAndCounts { 73 | // each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits 74 | // and an 8-bit count of the number of lights in the low 8 bits 75 | data: array, 1024u>, 76 | }; 77 | #else 78 | struct PointLights { 79 | data: array, 80 | }; 81 | struct ClusterLightIndexLists { 82 | data: array, 83 | }; 84 | struct ClusterOffsetsAndCounts { 85 | data: array>, 86 | }; 87 | #endif 88 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/pbr.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::mesh_vertex_output as OutputTypes 2 | #import bevy_pbr::pbr::functions as PbrCore 3 | #import bevy_pbr::pbr::bindings as MaterialBindings 4 | #import bevy_pbr::pbr::types as PbrTypes 5 | #import bevy_pbr::mesh_view_bindings as ViewBindings 6 | 7 | @fragment 8 | fn fragment( 9 | mesh: OutputTypes::MeshVertexOutput, 10 | @builtin(front_facing) is_front: bool, 11 | @builtin(position) frag_coord: vec4, 12 | ) -> @location(0) vec4 { 13 | var output_color: vec4 = MaterialBindings::material.base_color; 14 | 15 | #ifdef VERTEX_COLORS 16 | output_color = output_color * mesh.color; 17 | #endif 18 | #ifdef VERTEX_UVS 19 | if ((MaterialBindings::material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { 20 | output_color = output_color * textureSample(MaterialBindings::base_color_texture, MaterialBindings::base_color_sampler, mesh.uv); 21 | } 22 | #endif 23 | 24 | // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit 25 | if ((MaterialBindings::material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { 26 | // Prepare a 'processed' StandardMaterial by sampling all textures to resolve 27 | // the material members 28 | var pbr_input: PbrCore::PbrInput; 29 | 30 | pbr_input.material.base_color = output_color; 31 | pbr_input.material.reflectance = MaterialBindings::material.reflectance; 32 | pbr_input.material.flags = MaterialBindings::material.flags; 33 | pbr_input.material.alpha_cutoff = MaterialBindings::material.alpha_cutoff; 34 | 35 | // TODO use .a for exposure compensation in HDR 36 | var emissive: vec4 = MaterialBindings::material.emissive; 37 | #ifdef VERTEX_UVS 38 | if ((MaterialBindings::material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { 39 | emissive = vec4(emissive.rgb * textureSample(MaterialBindings::emissive_texture, MaterialBindings::emissive_sampler, mesh.uv).rgb, 1.0); 40 | } 41 | #endif 42 | pbr_input.material.emissive = emissive; 43 | 44 | var metallic: f32 = MaterialBindings::material.metallic; 45 | var perceptual_roughness: f32 = MaterialBindings::material.perceptual_roughness; 46 | #ifdef VERTEX_UVS 47 | if ((MaterialBindings::material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { 48 | let metallic_roughness = textureSample(MaterialBindings::metallic_roughness_texture, MaterialBindings::metallic_roughness_sampler, mesh.uv); 49 | // Sampling from GLTF standard channels for now 50 | metallic = metallic * metallic_roughness.b; 51 | perceptual_roughness = perceptual_roughness * metallic_roughness.g; 52 | } 53 | #endif 54 | pbr_input.material.metallic = metallic; 55 | pbr_input.material.perceptual_roughness = perceptual_roughness; 56 | 57 | var occlusion: f32 = 1.0; 58 | #ifdef VERTEX_UVS 59 | if ((MaterialBindings::material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { 60 | occlusion = textureSample(MaterialBindings::occlusion_texture, MaterialBindings::occlusion_sampler, mesh.uv).r; 61 | } 62 | #endif 63 | pbr_input.occlusion = occlusion; 64 | 65 | pbr_input.frag_coord = frag_coord; 66 | pbr_input.world_position = mesh.world_position; 67 | pbr_input.world_normal = mesh.world_normal; 68 | 69 | pbr_input.is_orthographic = ViewBindings::view.projection[3].w == 1.0; 70 | 71 | pbr_input.N = PbrCore::prepare_normal( 72 | MaterialBindings::material.flags, 73 | mesh.world_normal, 74 | #ifdef VERTEX_TANGENTS 75 | #ifdef STANDARDMATERIAL_NORMAL_MAP 76 | mesh.world_tangent, 77 | #endif 78 | #endif 79 | #ifdef VERTEX_UVS 80 | mesh.uv, 81 | #endif 82 | is_front, 83 | ); 84 | pbr_input.V = PbrCore::calculate_view(mesh.world_position, pbr_input.is_orthographic); 85 | 86 | output_color = PbrCore::tone_mapping(PbrCore::pbr(pbr_input)); 87 | } 88 | 89 | return output_color; 90 | } 91 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/pbr/bindings.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::pbr::bindings 2 | 3 | #import bevy_pbr::pbr::types as Types 4 | 5 | @group(1) @binding(0) 6 | var material: Types::StandardMaterial; 7 | @group(1) @binding(1) 8 | var base_color_texture: texture_2d; 9 | @group(1) @binding(2) 10 | var base_color_sampler: sampler; 11 | @group(1) @binding(3) 12 | var emissive_texture: texture_2d; 13 | @group(1) @binding(4) 14 | var emissive_sampler: sampler; 15 | @group(1) @binding(5) 16 | var metallic_roughness_texture: texture_2d; 17 | @group(1) @binding(6) 18 | var metallic_roughness_sampler: sampler; 19 | @group(1) @binding(7) 20 | var occlusion_texture: texture_2d; 21 | @group(1) @binding(8) 22 | var occlusion_sampler: sampler; 23 | @group(1) @binding(9) 24 | var normal_map_texture: texture_2d; 25 | @group(1) @binding(10) 26 | var normal_map_sampler: sampler; 27 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/pbr/functions.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::pbr::functions 2 | 3 | #import bevy_pbr::pbr::types as PbrTypes 4 | #import bevy_pbr::mesh_types as MeshTypes 5 | #import bevy_pbr::mesh_bindings as MeshBindings 6 | #import bevy_pbr::mesh_view_types as ViewTypes 7 | #import bevy_pbr::mesh_view_bindings as ViewBindings 8 | #import bevy_pbr::pbr::lighting as Lighting 9 | #import bevy_pbr::clustered_forward as Clustering 10 | #import bevy_pbr::shadows as Shadows 11 | 12 | // NOTE: This ensures that the world_normal is normalized and if 13 | // vertex tangents and normal maps then normal mapping may be applied. 14 | fn prepare_normal( 15 | standard_material_flags: u32, 16 | world_normal: vec3, 17 | #ifdef VERTEX_TANGENTS 18 | #ifdef STANDARDMATERIAL_NORMAL_MAP 19 | world_tangent: vec4, 20 | #endif 21 | #endif 22 | #ifdef VERTEX_UVS 23 | uv: vec2, 24 | #endif 25 | is_front: bool, 26 | ) -> vec3 { 27 | var N: vec3 = normalize(world_normal); 28 | 29 | #ifdef VERTEX_TANGENTS 30 | #ifdef STANDARDMATERIAL_NORMAL_MAP 31 | // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be 32 | // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the 33 | // vertex tangent! Do not change this code unless you really know what you are doing. 34 | // http://www.mikktspace.com/ 35 | var T: vec3 = world_tangent.xyz; 36 | var B: vec3 = world_tangent.w * cross(N, T); 37 | #endif 38 | #endif 39 | 40 | if ((standard_material_flags & PbrTypes::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) { 41 | if (!is_front) { 42 | N = -N; 43 | #ifdef VERTEX_TANGENTS 44 | #ifdef STANDARDMATERIAL_NORMAL_MAP 45 | T = -T; 46 | B = -B; 47 | #endif 48 | #endif 49 | } 50 | } 51 | 52 | #ifdef VERTEX_TANGENTS 53 | #ifdef VERTEX_UVS 54 | #ifdef STANDARDMATERIAL_NORMAL_MAP 55 | // Nt is the tangent-space normal. 56 | var Nt = textureSample(normal_map_texture, normal_map_sampler, uv).rgb; 57 | if ((standard_material_flags & PbrTypes::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { 58 | // Only use the xy components and derive z for 2-component normal maps. 59 | Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); 60 | Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); 61 | } else { 62 | Nt = Nt * 2.0 - 1.0; 63 | } 64 | // Normal maps authored for DirectX require flipping the y component 65 | if ((standard_material_flags & PbrTypes::STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u) { 66 | Nt.y = -Nt.y; 67 | } 68 | // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from 69 | // the normal map texture in this way to be an EXACT inverse of how the normal map baker 70 | // calculates the normal maps so there is no error introduced. Do not change this code 71 | // unless you really know what you are doing. 72 | // http://www.mikktspace.com/ 73 | N = normalize(Nt.x * T + Nt.y * B + Nt.z * N); 74 | #endif 75 | #endif 76 | #endif 77 | 78 | return N; 79 | } 80 | 81 | // NOTE: Correctly calculates the view vector depending on whether 82 | // the projection is orthographic or perspective. 83 | fn calculate_view( 84 | world_position: vec4, 85 | is_orthographic: bool, 86 | ) -> vec3 { 87 | var V: vec3; 88 | if (is_orthographic) { 89 | // Orthographic view vector 90 | V = normalize(vec3(ViewBindings::view.view_proj[0].z, ViewBindings::view.view_proj[1].z, ViewBindings::view.view_proj[2].z)); 91 | } else { 92 | // Only valid for a perpective projection 93 | V = normalize(ViewBindings::view.world_position.xyz - world_position.xyz); 94 | } 95 | return V; 96 | } 97 | 98 | struct PbrInput { 99 | material: PbrTypes::StandardMaterial, 100 | occlusion: f32, 101 | frag_coord: vec4, 102 | world_position: vec4, 103 | // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow 104 | // mapping 105 | world_normal: vec3, 106 | // Normalized normal-mapped world normal used for lighting 107 | N: vec3, 108 | // Normalized view vector in world space, pointing from the fragment world position toward the 109 | // view world position 110 | V: vec3, 111 | is_orthographic: bool, 112 | }; 113 | 114 | // Creates a PbrInput with default values 115 | fn pbr_input_new() -> PbrInput { 116 | var pbr_input: PbrInput; 117 | 118 | pbr_input.material = PbrTypes::standard_material_new(); 119 | pbr_input.occlusion = 1.0; 120 | 121 | pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); 122 | pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); 123 | pbr_input.world_normal = vec3(0.0, 0.0, 1.0); 124 | 125 | pbr_input.is_orthographic = false; 126 | 127 | pbr_input.N = vec3(0.0, 0.0, 1.0); 128 | pbr_input.V = vec3(1.0, 0.0, 0.0); 129 | 130 | return pbr_input; 131 | } 132 | 133 | fn pbr( 134 | in: PbrInput, 135 | ) -> vec4 { 136 | var output_color: vec4 = in.material.base_color; 137 | 138 | // TODO use .a for exposure compensation in HDR 139 | let emissive = in.material.emissive; 140 | 141 | // calculate non-linear roughness from linear perceptualRoughness 142 | let metallic = in.material.metallic; 143 | let perceptual_roughness = in.material.perceptual_roughness; 144 | let roughness = Lighting::perceptualRoughnessToRoughness(perceptual_roughness); 145 | 146 | let occlusion = in.occlusion; 147 | 148 | if ((in.material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { 149 | // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 150 | output_color.a = 1.0; 151 | } else if ((in.material.flags & PbrTypes::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { 152 | if (output_color.a >= in.material.alpha_cutoff) { 153 | // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque 154 | output_color.a = 1.0; 155 | } else { 156 | // NOTE: output_color.a < in.material.alpha_cutoff should not is not rendered 157 | // NOTE: This and any other discards mean that early-z testing cannot be done! 158 | discard; 159 | } 160 | } 161 | 162 | // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" 163 | let NdotV = max(dot(in.N, in.V), 0.0001); 164 | 165 | // Remapping [0,1] reflectance to F0 166 | // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping 167 | let reflectance = in.material.reflectance; 168 | let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; 169 | 170 | // Diffuse strength inversely related to metallicity 171 | let diffuse_color = output_color.rgb * (1.0 - metallic); 172 | 173 | let R = reflect(-in.V, in.N); 174 | 175 | // accumulate color 176 | var light_accum: vec3 = vec3(0.0); 177 | 178 | let view_z = dot(vec4( 179 | ViewBindings::view.inverse_view[0].z, 180 | ViewBindings::view.inverse_view[1].z, 181 | ViewBindings::view.inverse_view[2].z, 182 | ViewBindings::view.inverse_view[3].z 183 | ), in.world_position); 184 | let cluster_index = Clustering::fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); 185 | let offset_and_counts = Clustering::unpack_offset_and_counts(cluster_index); 186 | 187 | // point lights 188 | for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1]; i = i + 1u) { 189 | let light_id = Clustering::get_light_id(i); 190 | let light = ViewBindings::point_lights.data[light_id]; 191 | var shadow: f32 = 1.0; 192 | if ((MeshBindings::mesh.flags & MeshTypes::MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u 193 | && (light.flags & ViewTypes::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { 194 | shadow = Shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); 195 | } 196 | let light_contrib = Lighting::point_light(in.world_position.xyz, light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); 197 | light_accum = light_accum + light_contrib * shadow; 198 | } 199 | 200 | // spot lights 201 | for (var i: u32 = offset_and_counts[0] + offset_and_counts[1]; i < offset_and_counts[0] + offset_and_counts[1] + offset_and_counts[2]; i = i + 1u) { 202 | let light_id = Clustering::get_light_id(i); 203 | let light = ViewBindings::point_lights.data[light_id]; 204 | var shadow: f32 = 1.0; 205 | if ((MeshBindings::mesh.flags & MeshTypes::MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u 206 | && (light.flags & ViewTypes::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { 207 | shadow = Shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal); 208 | } 209 | let light_contrib = Lighting::spot_light(in.world_position.xyz, light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); 210 | light_accum = light_accum + light_contrib * shadow; 211 | } 212 | 213 | let n_directional_lights = ViewBindings::lights.n_directional_lights; 214 | for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { 215 | let light = ViewBindings::lights.directional_lights[i]; 216 | var shadow: f32 = 1.0; 217 | if ((MeshBindings::mesh.flags & MeshTypes::MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u 218 | && (light.flags & ViewTypes::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { 219 | shadow = Shadows::fetch_directional_shadow(i, in.world_position, in.world_normal); 220 | } 221 | let light_contrib = Lighting::directional_light(light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); 222 | light_accum = light_accum + light_contrib * shadow; 223 | } 224 | 225 | let diffuse_ambient = Lighting::EnvBRDFApprox(diffuse_color, 1.0, NdotV); 226 | let specular_ambient = Lighting::EnvBRDFApprox(F0, perceptual_roughness, NdotV); 227 | 228 | output_color = vec4( 229 | light_accum + 230 | (diffuse_ambient + specular_ambient) * ViewBindings::lights.ambient_color.rgb * occlusion + 231 | emissive.rgb * output_color.a, 232 | output_color.a); 233 | 234 | output_color = Clustering::cluster_debug_visualization( 235 | output_color, 236 | view_z, 237 | in.is_orthographic, 238 | offset_and_counts, 239 | cluster_index, 240 | ); 241 | 242 | return output_color; 243 | } 244 | 245 | fn tone_mapping(in: vec4) -> vec4 { 246 | // tone_mapping 247 | return vec4(Lighting::reinhard_luminance(in.rgb), in.a); 248 | 249 | // Gamma correction. 250 | // Not needed with sRGB buffer 251 | // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); 252 | } 253 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/pbr/types.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::pbr::types 2 | 3 | struct StandardMaterial { 4 | base_color: vec4, 5 | emissive: vec4, 6 | perceptual_roughness: f32, 7 | metallic: f32, 8 | reflectance: f32, 9 | // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. 10 | flags: u32, 11 | alpha_cutoff: f32, 12 | }; 13 | 14 | const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u; 15 | const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u; 16 | const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u; 17 | const STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u; 18 | const STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u; 19 | const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; 20 | const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u; 21 | const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u; 22 | const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u; 23 | const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u; 24 | const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1024u; 25 | 26 | // Creates a StandardMaterial with default values 27 | fn standard_material_new() -> StandardMaterial { 28 | var material: StandardMaterial; 29 | 30 | // NOTE: Keep in-sync with src/pbr_material.rs! 31 | material.base_color = vec4(1.0, 1.0, 1.0, 1.0); 32 | material.emissive = vec4(0.0, 0.0, 0.0, 1.0); 33 | material.perceptual_roughness = 0.089; 34 | material.metallic = 0.01; 35 | material.reflectance = 0.5; 36 | material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; 37 | material.alpha_cutoff = 0.5; 38 | 39 | return material; 40 | } 41 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/shadows.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::shadows 2 | 3 | #import bevy_pbr::mesh_view_types as Types 4 | #import bevy_pbr::mesh_view_bindings as Bindings 5 | 6 | fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { 7 | let light = Bindings::point_lights.data[light_id]; 8 | 9 | // because the shadow maps align with the axes and the frustum planes are at 45 degrees 10 | // we can get the worldspace depth by taking the largest absolute axis 11 | let surface_to_light = light.position_radius.xyz - frag_position.xyz; 12 | let surface_to_light_abs = abs(surface_to_light); 13 | let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); 14 | 15 | // The normal bias here is already scaled by the texel size at 1 world unit from the light. 16 | // The texel size increases proportionally with distance from the light so multiplying by 17 | // distance to light scales the normal bias to the texel size at the fragment distance. 18 | let normal_offset = light.shadow_normal_bias * distance_to_light * surface_normal.xyz; 19 | let depth_offset = light.shadow_depth_bias * normalize(surface_to_light.xyz); 20 | let offset_position = frag_position.xyz + normal_offset + depth_offset; 21 | 22 | // similar largest-absolute-axis trick as above, but now with the offset fragment position 23 | let frag_ls = light.position_radius.xyz - offset_position.xyz; 24 | let abs_position_ls = abs(frag_ls); 25 | let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); 26 | 27 | // NOTE: These simplifications come from multiplying: 28 | // projection * vec4(0, 0, -major_axis_magnitude, 1.0) 29 | // and keeping only the terms that have any impact on the depth. 30 | // Projection-agnostic approach: 31 | let zw = -major_axis_magnitude * light.light_custom_data.xy + light.light_custom_data.zw; 32 | let depth = zw.x / zw.y; 33 | 34 | // do the lookup, using HW PCF and comparison 35 | // NOTE: Due to the non-uniform control flow above, we must use the Level variant of 36 | // textureSampleCompare to avoid undefined behaviour due to some of the fragments in 37 | // a quad (2x2 fragments) being processed not being sampled, and this messing with 38 | // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples 39 | // from LOD 0. 40 | #ifdef NO_ARRAY_TEXTURES_SUPPORT 41 | return textureSampleCompare(Bindings::point_shadow_textures, Bindings::point_shadow_textures_sampler, frag_ls, depth); 42 | #else 43 | return textureSampleCompareLevel(Bindings::point_shadow_textures, Bindings::point_shadow_textures_sampler, frag_ls, i32(light_id), depth); 44 | #endif 45 | } 46 | 47 | fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { 48 | let light = Bindings::point_lights.data[light_id]; 49 | 50 | let surface_to_light = light.position_radius.xyz - frag_position.xyz; 51 | 52 | // construct the light view matrix 53 | var spot_dir = vec3(light.light_custom_data.x, 0.0, light.light_custom_data.y); 54 | // reconstruct spot dir from x/z and y-direction flag 55 | spot_dir.y = sqrt(1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z); 56 | if ((light.flags & Types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { 57 | spot_dir.y = -spot_dir.y; 58 | } 59 | 60 | // view matrix z_axis is the reverse of transform.forward() 61 | let fwd = -spot_dir; 62 | let distance_to_light = dot(fwd, surface_to_light); 63 | let offset_position = 64 | -surface_to_light 65 | + (light.shadow_depth_bias * normalize(surface_to_light)) 66 | + (surface_normal.xyz * light.shadow_normal_bias) * distance_to_light; 67 | 68 | // the construction of the up and right vectors needs to precisely mirror the code 69 | // in render/light.rs:spot_light_view_matrix 70 | var sign = -1.0; 71 | if (fwd.z >= 0.0) { 72 | sign = 1.0; 73 | } 74 | let a = -1.0 / (fwd.z + sign); 75 | let b = fwd.x * fwd.y * a; 76 | let up_dir = vec3(1.0 + sign * fwd.x * fwd.x * a, sign * b, -sign * fwd.x); 77 | let right_dir = vec3(-b, -sign - fwd.y * fwd.y * a, fwd.y); 78 | let light_inv_rot = mat3x3(right_dir, up_dir, fwd); 79 | 80 | // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate 81 | // the product of the transpose with a vector we can just post-multiply instead of pre-multplying. 82 | // this allows us to keep the matrix construction code identical between CPU and GPU. 83 | let projected_position = offset_position * light_inv_rot; 84 | 85 | // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w) 86 | // to get ndc coordinates 87 | let f_div_minus_z = 1.0 / (light.spot_light_tan_angle * -projected_position.z); 88 | let shadow_xy_ndc = projected_position.xy * f_div_minus_z; 89 | // convert to uv coordinates 90 | let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); 91 | 92 | // 0.1 must match POINT_LIGHT_NEAR_Z 93 | let depth = 0.1 / -projected_position.z; 94 | 95 | #ifdef NO_ARRAY_TEXTURES_SUPPORT 96 | return textureSampleCompare(Bindings::directional_shadow_textures, Bindings::directional_shadow_textures_sampler, 97 | shadow_uv, depth); 98 | #else 99 | return textureSampleCompareLevel(Bindings::directional_shadow_textures, Bindings::directional_shadow_textures_sampler, 100 | shadow_uv, i32(light_id) + Bindings::lights.spot_light_shadowmap_offset, depth); 101 | #endif 102 | } 103 | 104 | fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { 105 | let light = Bindings::lights.directional_lights[light_id]; 106 | 107 | // The normal bias is scaled to the texel size. 108 | let normal_offset = light.shadow_normal_bias * surface_normal.xyz; 109 | let depth_offset = light.shadow_depth_bias * light.direction_to_light.xyz; 110 | let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); 111 | 112 | let offset_position_clip = light.view_projection * offset_position; 113 | if (offset_position_clip.w <= 0.0) { 114 | return 1.0; 115 | } 116 | let offset_position_ndc = offset_position_clip.xyz / offset_position_clip.w; 117 | // No shadow outside the orthographic projection volume 118 | if (any(offset_position_ndc.xy < vec2(-1.0)) || offset_position_ndc.z < 0.0 119 | || any(offset_position_ndc > vec3(1.0))) { 120 | return 1.0; 121 | } 122 | 123 | // compute texture coordinates for shadow lookup, compensating for the Y-flip difference 124 | // between the NDC and texture coordinates 125 | let flip_correction = vec2(0.5, -0.5); 126 | let light_local = offset_position_ndc.xy * flip_correction + vec2(0.5, 0.5); 127 | 128 | let depth = offset_position_ndc.z; 129 | // do the lookup, using HW PCF and comparison 130 | // NOTE: Due to non-uniform control flow above, we must use the level variant of the texture 131 | // sampler to avoid use of implicit derivatives causing possible undefined behavior. 132 | #ifdef NO_ARRAY_TEXTURES_SUPPORT 133 | return textureSampleCompareLevel(Bindings::directional_shadow_textures, Bindings::directional_shadow_textures_sampler, light_local, depth); 134 | #else 135 | return textureSampleCompareLevel(Bindings::directional_shadow_textures, Bindings::directional_shadow_textures_sampler, light_local, i32(light_id), depth); 136 | #endif 137 | } 138 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/skinning.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::skinning 2 | 3 | #ifdef SKINNED 4 | 5 | @group(2) @binding(1) 6 | var joint_matrices: SkinnedMesh; 7 | 8 | fn skin_model( 9 | indexes: vec4, 10 | weights: vec4, 11 | ) -> mat4x4 { 12 | return weights.x * joint_matrices.data[indexes.x] 13 | + weights.y * joint_matrices.data[indexes.y] 14 | + weights.z * joint_matrices.data[indexes.z] 15 | + weights.w * joint_matrices.data[indexes.w]; 16 | } 17 | 18 | fn inverse_transpose_3x3(in: mat3x3) -> mat3x3 { 19 | let x = cross(in[1], in[2]); 20 | let y = cross(in[2], in[0]); 21 | let z = cross(in[0], in[1]); 22 | let det = dot(in[2], z); 23 | return mat3x3( 24 | x / det, 25 | y / det, 26 | z / det 27 | ); 28 | } 29 | 30 | fn skin_normals( 31 | model: mat4x4, 32 | normal: vec3, 33 | ) -> vec3 { 34 | return inverse_transpose_3x3(mat3x3( 35 | model[0].xyz, 36 | model[1].xyz, 37 | model[2].xyz 38 | )) * normal; 39 | } 40 | 41 | #endif -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/utils.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_pbr::utils 2 | 3 | const PI: f32 = 3.141592653589793; 4 | 5 | fn saturate(value: f32) -> f32 { 6 | return clamp(value, 0.0, 1.0); 7 | } 8 | 9 | fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3 { 10 | let rgb = clamp( 11 | abs( 12 | ((hue * 6.0 + vec3(0.0, 4.0, 2.0)) % 6.0) - 3.0 13 | ) - 1.0, 14 | vec3(0.0), 15 | vec3(1.0) 16 | ); 17 | 18 | return value * mix( vec3(1.0), rgb, vec3(saturation)); 19 | } 20 | 21 | fn random1D(s: f32) -> f32 { 22 | return fract(sin(s * 12.9898) * 43758.5453123); 23 | } 24 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/bevy_pbr_wgsl/wireframe.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::mesh_types 2 | #import bevy_pbr::mesh_view_bindings 3 | 4 | @group(1) @binding(0) 5 | var mesh: Mesh; 6 | 7 | #ifdef SKINNED 8 | @group(1) @binding(1) 9 | var joint_matrices: SkinnedMesh; 10 | #import bevy_pbr::skinning 11 | #endif 12 | 13 | // NOTE: Bindings must come before functions that use them! 14 | #import bevy_pbr::mesh_functions 15 | 16 | struct Vertex { 17 | @location(0) position: vec3, 18 | #ifdef SKINNED 19 | @location(4) joint_indexes: vec4, 20 | @location(5) joint_weights: vec4, 21 | #endif 22 | }; 23 | 24 | struct VertexOutput { 25 | @builtin(position) clip_position: vec4, 26 | }; 27 | 28 | @vertex 29 | fn vertex(vertex: Vertex) -> VertexOutput { 30 | #ifdef SKINNED 31 | let model = skin_model(vertex.joint_indexes, vertex.joint_weights); 32 | #else 33 | let model = mesh.model; 34 | #endif 35 | 36 | var out: VertexOutput; 37 | out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); 38 | return out; 39 | } 40 | 41 | @fragment 42 | fn fragment() -> @location(0) vec4 { 43 | return vec4(1.0, 1.0, 1.0, 1.0); 44 | } 45 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/issue_35/clear.wgsl: -------------------------------------------------------------------------------- 1 | #import vertices::VertexIn 2 | 3 | struct VertexOutput { 4 | @builtin(position) position: vec4, 5 | } 6 | 7 | @vertex 8 | fn vertex_main(input: VertexIn) -> VertexOutput { 9 | var output: VertexOutput; 10 | output.position = input.position; 11 | return output; 12 | } 13 | 14 | @fragment 15 | fn fragment_main(input: VertexOutput) -> @location(0) vec4 { 16 | return vec4(1.0, 1.0, 1.0, 1.0); 17 | } -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/issue_35/vertices.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexIn { 2 | @location(0) position: vec4, 3 | } -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/layouts.wgsl: -------------------------------------------------------------------------------- 1 | struct Scalars { 2 | a: u32, 3 | b: i32, 4 | c: f32, 5 | @builtin(vertex_index) d: u32, 6 | }; 7 | 8 | struct VectorsU32 { 9 | a: vec2, 10 | b: vec3, 11 | c: vec4, 12 | _padding: f32, 13 | }; 14 | 15 | struct VectorsI32 { 16 | a: vec2, 17 | b: vec3, 18 | c: vec4, 19 | }; 20 | 21 | struct VectorsF32 { 22 | a: vec2, 23 | b: vec3, 24 | c: vec4, 25 | }; 26 | 27 | struct MatricesF32 { 28 | a: mat4x4, 29 | b: mat4x3, 30 | c: mat4x2, 31 | d: mat3x4, 32 | e: mat3x3, 33 | f: mat3x2, 34 | g: mat2x4, 35 | h: mat2x3, 36 | i: mat2x2, 37 | }; 38 | 39 | struct StaticArrays { 40 | a: array, 41 | b: array, 42 | c: array, 512>, 43 | d: array, 4> 44 | }; 45 | 46 | struct Nested { 47 | a: MatricesF32, 48 | b: VectorsF32 49 | }; 50 | 51 | struct Uniforms { 52 | color_rgb: vec4, 53 | scalars: Scalars 54 | } 55 | 56 | @group(0) @binding(0) var color_texture: texture_2d; 57 | @group(0) @binding(1) var color_sampler: sampler; 58 | 59 | @group(1) @binding(0) var uniforms: Uniforms; 60 | 61 | @group(2) @binding(2) var a: Scalars; 62 | @group(2) @binding(3) var b: VectorsU32; 63 | @group(2) @binding(4) var c: VectorsI32; 64 | @group(2) @binding(5) var d: VectorsF32; 65 | @group(2) @binding(6) var f: MatricesF32; 66 | @group(2) @binding(8) var h: StaticArrays; 67 | @group(2) @binding(9) var i: Nested; 68 | 69 | struct VertexIn { 70 | @location(0) position: vec4, 71 | } 72 | 73 | struct VertexOutput { 74 | @builtin(position) position: vec4, 75 | } 76 | 77 | @vertex 78 | fn vertex_main(input: VertexIn) -> VertexOutput { 79 | var output: VertexOutput; 80 | output.position = input.position; 81 | return output; 82 | } 83 | 84 | @fragment 85 | fn fragment_main(input: VertexOutput) -> @location(0) vec4 { 86 | return vec4(1.0, 1.0, 1.0, 1.0); 87 | } -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/minimal.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | color: vec4f, 3 | width: f32, 4 | } 5 | 6 | @group(0) @binding(0) 7 | var uniform_buf: Uniforms; 8 | 9 | @compute @workgroup_size(1) 10 | fn main(@builtin(global_invocation_id) id: vec3) { 11 | } 12 | -------------------------------------------------------------------------------- /wgsl_bindgen/tests/shaders/padding.wgsl: -------------------------------------------------------------------------------- 1 | struct Style { 2 | color: vec4f, 3 | width: f32, 4 | _padding: vec2 5 | } 6 | 7 | @group(0) @binding(0) 8 | var frame: Style; 9 | 10 | @compute @workgroup_size(1) 11 | fn main(@builtin(global_invocation_id) id: vec3) { 12 | } 13 | --------------------------------------------------------------------------------