├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── github-release │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── action.yml │ │ ├── main.js │ │ └── package.json ├── dependabot.yml └── workflows │ ├── ci-comment-failures.yml │ ├── ci.yml │ ├── dependencies.yml │ ├── weekly.yml │ └── welcome.yml ├── .gitignore ├── .gitmodules ├── .markdownlint.jsonc ├── CONTRIBUTING.md ├── COPYRIGHT ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── tokrepr-derive │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── tokrepr │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── wesl-cli │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── wesl-macros │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── query_macro.rs │ │ └── quote_macro.rs ├── wesl-test │ ├── Cargo.toml │ ├── bevy │ │ ├── README.md │ │ ├── animate_shader.wgsl │ │ ├── array_texture.wgsl │ │ ├── automatic_instancing.wgsl │ │ ├── bindless_material.wgsl │ │ ├── cubemap_unlit.wgsl │ │ ├── custom_clustered_decal.wgsl │ │ ├── custom_gltf_2d.wgsl │ │ ├── custom_material.wesl │ │ ├── custom_material.wgsl │ │ ├── custom_material_2d.wgsl │ │ ├── custom_material_import.wgsl │ │ ├── custom_material_screenspace_texture.wgsl │ │ ├── custom_phase_item.wgsl │ │ ├── custom_stencil.wgsl │ │ ├── custom_ui_material.wgsl │ │ ├── custom_vertex_attribute.wgsl │ │ ├── extended_material.wgsl │ │ ├── fallback_image_test.wgsl │ │ ├── game_of_life.wgsl │ │ ├── gpu_readback.wgsl │ │ ├── instancing.wgsl │ │ ├── irradiance_volume_voxel_visualization.wgsl │ │ ├── line_material.wgsl │ │ ├── post_processing.wgsl │ │ ├── shader_defs.wgsl │ │ ├── show_prepass.wgsl │ │ ├── specialized_mesh_pipeline.wgsl │ │ ├── storage_buffer.wgsl │ │ ├── texture_binding_array.wgsl │ │ ├── tonemapping_test_patterns.wgsl │ │ ├── util.wesl │ │ └── water_material.wgsl │ ├── rust-toolchain.toml │ ├── spec-tests │ │ ├── idents.json │ │ └── lit-type-inference.json │ ├── src │ │ ├── lib.rs │ │ └── schemas.rs │ ├── tests │ │ └── testsuite.rs │ ├── unity_web_research │ │ ├── LICENSE-MIT │ │ ├── README.md │ │ └── boat_attack │ │ │ ├── unity_webgpu_0000014DFB395020.fs.wgsl │ │ │ ├── unity_webgpu_0000017E9E2D81A0.vs.wgsl │ │ │ ├── unity_webgpu_000001D9D2114040.fs.wgsl │ │ │ └── unity_webgpu_0000020A44565050.fs.wgsl │ └── webgpu-samples │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── basic.vert.wgsl │ │ ├── blur.wgsl │ │ ├── common.wgsl │ │ ├── cube.wgsl │ │ ├── frag.wgsl │ │ ├── fullscreenTexturedQuad.wgsl │ │ ├── gltf.wgsl │ │ ├── grid.wgsl │ │ ├── mesh.wgsl │ │ ├── opaque.wgsl │ │ ├── red.frag.wgsl │ │ ├── sampleExternalTexture.frag.wgsl │ │ ├── sprite.wgsl │ │ ├── triangle.vert.wgsl │ │ ├── vert.wgsl │ │ ├── vertex.wgsl │ │ └── vertexPositionColor.frag.wgsl ├── wesl-web │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── wesl │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── builtin.rs │ │ ├── condcomp.rs │ │ ├── error.rs │ │ ├── eval │ │ ├── attrs.rs │ │ ├── builtin.rs │ │ ├── constant.rs │ │ ├── conv.rs │ │ ├── display.rs │ │ ├── error.rs │ │ ├── eval.rs │ │ ├── exec.rs │ │ ├── instance.rs │ │ ├── lower.rs │ │ ├── mem.rs │ │ ├── mod.rs │ │ ├── ops.rs │ │ ├── ops_std.rs │ │ ├── to_expr.rs │ │ └── ty.rs │ │ ├── generics │ │ ├── mangle.rs │ │ └── mod.rs │ │ ├── import.rs │ │ ├── lib.rs │ │ ├── lower.rs │ │ ├── mangle.rs │ │ ├── overload.rs │ │ ├── package.rs │ │ ├── resolve.rs │ │ ├── sourcemap.rs │ │ ├── strip.rs │ │ ├── syntax_util.rs │ │ ├── to_naga.rs │ │ ├── validate │ │ └── mod.rs │ │ └── visit.rs └── wgsl-parse │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ ├── error.rs │ ├── lexer.rs │ ├── lib.rs │ ├── parser.rs │ ├── parser_support.rs │ ├── span.rs │ ├── syntax.rs │ ├── syntax_display.rs │ ├── syntax_impl.rs │ ├── tokrepr.rs │ ├── wgsl.lalrpop │ └── wgsl_recognize.lalrpop ├── deny.toml ├── examples ├── random_wgsl │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── lib.rs │ │ └── shaders │ │ └── random.wesl └── wesl-consumer │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ ├── main.rs │ └── shaders │ ├── main.wesl │ └── util.wesl ├── rustfmt.toml ├── samples ├── binding0.bin ├── binding1.bin ├── downsample_depth.wesl ├── eval_shadowing.wesl ├── imp1.wgsl ├── imp2.wgsl ├── main.wgsl ├── shader_cache_output_deferred_lighting.wgsl └── shader_cache_output_pbr.wgsl ├── taplo.toml └── typos.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = tab 7 | indent_style = space 8 | insert_final_newline = true 9 | spelling_language = en-us 10 | tab_width = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.toml] 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_size = 2 18 | 19 | [*.{yaml,yml}] 20 | indent_size = 2 21 | 22 | [*.{json, jsonc}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From: https://docs.github.com/en/get-started/git-basics/configuring-git-to-handle-line-endings 2 | 3 | # Set the default behavior, in case people don't have core.autocrlf set. 4 | * text=auto 5 | 6 | # Explicitly declare text files you want to always be normalized and converted 7 | # to native line endings on checkout. 8 | 9 | # Declare files that will always have LF line endings on checkout. 10 | .gitattributes text eol=lf 11 | .gitignore text eol=lf 12 | *.json text eol=lf 13 | *.md text eol=lf 14 | *.rs text eol=lf 15 | *.sh text eol=lf 16 | *.toml text eol=lf 17 | *.yml text eol=lf 18 | 19 | # Declare files that will always have CRLF line endings on checkout. 20 | 21 | # Denote all files that are truly binary and should not be modified. 22 | -------------------------------------------------------------------------------- /.github/actions/github-release/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | 3 | COPY . /action 4 | WORKDIR /action 5 | 6 | RUN npm install --production 7 | 8 | ENTRYPOINT ["node", "/action/main.js"] 9 | -------------------------------------------------------------------------------- /.github/actions/github-release/README.md: -------------------------------------------------------------------------------- 1 | # github-release 2 | 3 | Copy-pasted from 4 | 5 | An action used to publish GitHub releases for `wasmtime`. 6 | 7 | As of the time of this writing there's a few actions floating around which 8 | perform github releases but they all tend to have their set of drawbacks. 9 | Additionally nothing handles deleting releases which we need for our rolling 10 | `dev` release. 11 | 12 | To handle all this, this action rolls its own implementation using the 13 | actions/toolkit repository and packages published there. These run in a Docker 14 | container and take various inputs to orchestrate the release from the build. 15 | 16 | More comments can be found in `main.js`. 17 | 18 | Testing this is really hard. If you want to try though run `npm install` and 19 | then `node main.js`. You'll have to configure a bunch of env vars though to get 20 | anything reasonably working. 21 | -------------------------------------------------------------------------------- /.github/actions/github-release/action.yml: -------------------------------------------------------------------------------- 1 | name: 'wasmtime github releases' 2 | description: 'wasmtime github releases' 3 | inputs: 4 | token: 5 | description: '' 6 | required: true 7 | name: 8 | description: '' 9 | required: true 10 | files: 11 | description: '' 12 | required: true 13 | runs: 14 | using: 'docker' 15 | image: 'Dockerfile' 16 | -------------------------------------------------------------------------------- /.github/actions/github-release/main.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const github = require('@actions/github'); 5 | const glob = require('glob'); 6 | 7 | function sleep(milliseconds) { 8 | return new Promise(resolve => setTimeout(resolve, milliseconds)); 9 | } 10 | 11 | async function runOnce() { 12 | // Load all our inputs and env vars. Note that `getInput` reads from `INPUT_*` 13 | const files = core.getInput('files'); 14 | const name = core.getInput('name'); 15 | const token = core.getInput('token'); 16 | const slug = process.env.GITHUB_REPOSITORY; 17 | const owner = slug.split('/')[0]; 18 | const repo = slug.split('/')[1]; 19 | const sha = process.env.HEAD_SHA; 20 | 21 | core.info(`files: ${files}`); 22 | core.info(`name: ${name}`); 23 | 24 | const options = { 25 | request: { 26 | timeout: 30000, 27 | } 28 | }; 29 | const octokit = github.getOctokit(token, options); 30 | 31 | // Delete the previous release since we can't overwrite one. This may happen 32 | // due to retrying an upload or it may happen because we're doing the dev 33 | // release. 34 | const releases = await octokit.paginate("GET /repos/:owner/:repo/releases", { owner, repo }); 35 | for (const release of releases) { 36 | if (release.tag_name !== name) { 37 | continue; 38 | } 39 | const release_id = release.id; 40 | core.info(`deleting release ${release_id}`); 41 | await octokit.rest.repos.deleteRelease({ owner, repo, release_id }); 42 | } 43 | 44 | // We also need to update the `dev` tag while we're at it on the `dev` branch. 45 | if (name == 'nightly') { 46 | try { 47 | core.info(`updating nightly tag`); 48 | await octokit.rest.git.updateRef({ 49 | owner, 50 | repo, 51 | ref: 'tags/nightly', 52 | sha, 53 | force: true, 54 | }); 55 | } catch (e) { 56 | core.error(e); 57 | core.info(`creating nightly tag`); 58 | await octokit.rest.git.createTag({ 59 | owner, 60 | repo, 61 | tag: 'nightly', 62 | message: 'nightly release', 63 | object: sha, 64 | type: 'commit', 65 | }); 66 | } 67 | } 68 | 69 | // Creates an official GitHub release for this `tag`, and if this is `dev` 70 | // then we know that from the previous block this should be a fresh release. 71 | core.info(`creating a release`); 72 | const release = await octokit.rest.repos.createRelease({ 73 | owner, 74 | repo, 75 | name, 76 | tag_name: name, 77 | target_commitish: sha, 78 | prerelease: name === 'nightly', 79 | }); 80 | const release_id = release.data.id; 81 | 82 | // Upload all the relevant assets for this release as just general blobs. 83 | for (const file of glob.sync(files)) { 84 | const size = fs.statSync(file).size; 85 | const name = path.basename(file); 86 | 87 | await runWithRetry(async function () { 88 | // We can't overwrite assets, so remove existing ones from a previous try. 89 | let assets = await octokit.rest.repos.listReleaseAssets({ 90 | owner, 91 | repo, 92 | release_id 93 | }); 94 | for (const asset of assets.data) { 95 | if (asset.name === name) { 96 | core.info(`delete asset ${name}`); 97 | const asset_id = asset.id; 98 | await octokit.rest.repos.deleteReleaseAsset({ owner, repo, asset_id }); 99 | } 100 | } 101 | 102 | core.info(`upload ${file}`); 103 | const headers = { 'content-length': size, 'content-type': 'application/octet-stream' }; 104 | const data = fs.createReadStream(file); 105 | await octokit.rest.repos.uploadReleaseAsset({ 106 | data, 107 | headers, 108 | name, 109 | url: release.data.upload_url, 110 | }); 111 | }); 112 | } 113 | } 114 | 115 | async function runWithRetry(my_function) { 116 | const retries = 10; 117 | const maxDelay = 4000; 118 | let delay = 1000; 119 | 120 | for (let index = 0; index < retries; index++) { 121 | try { 122 | await my_function(); 123 | break; 124 | } catch (error) { 125 | if (index === retries - 1) 126 | { 127 | throw error; 128 | } 129 | 130 | core.error(error); 131 | const currentDelay = Math.round(Math.random() * delay); 132 | core.info(`sleeping ${currentDelay} ms`); 133 | await sleep(currentDelay); 134 | delay = Math.min(delay * 2, maxDelay); 135 | } 136 | } 137 | } 138 | 139 | async function run() { 140 | await runWithRetry(runOnce); 141 | } 142 | 143 | run().catch(error => { 144 | core.error(error); 145 | core.setFailed(error.message); 146 | }); 147 | -------------------------------------------------------------------------------- /.github/actions/github-release/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmtime-github-release", 3 | "version": "0.0.0", 4 | "main": "main.js", 5 | "dependencies": { 6 | "@actions/core": "^1.6", 7 | "@actions/github": "^5.0", 8 | "glob": "^7.1.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | labels: 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | labels: 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | # Based on and adapted from https://github.com/bevyengine/bevy/blob/main/.github/workflows/ci.yml 4 | 5 | on: 6 | pull_request: 7 | merge_group: 8 | 9 | permissions: 10 | actions: none 11 | attestations: none 12 | checks: none 13 | contents: none 14 | deployments: none 15 | id-token: none 16 | issues: none 17 | discussions: none 18 | packages: none 19 | pages: none 20 | pull-requests: none 21 | repository-projects: none 22 | security-events: none 23 | statuses: none 24 | 25 | env: 26 | CARGO_TERM_COLOR: always 27 | # If nightly is breaking CI, modify this variable to target a specific nightly version. 28 | NIGHTLY_TOOLCHAIN: nightly 29 | STABLE_TOOLCHAIN: "1.87.0" 30 | MIRIFLAGS: "-Zmiri-disable-isolation" 31 | 32 | concurrency: 33 | group: ${{github.workflow}}-${{github.ref}} 34 | cancel-in-progress: ${{github.event_name == 'pull_request'}} 35 | 36 | jobs: 37 | build: 38 | strategy: 39 | matrix: 40 | os: [windows-latest, ubuntu-latest, macos-latest] 41 | runs-on: ${{ matrix.os }} 42 | timeout-minutes: 30 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: "true" 47 | - uses: actions/cache@v4 48 | with: 49 | path: | 50 | ~/.cargo/bin/ 51 | ~/.cargo/registry/index/ 52 | ~/.cargo/registry/cache/ 53 | ~/.cargo/git/db/ 54 | target/ 55 | key: ${{ runner.os }}-cargo-build-stable-${{ hashFiles('**/Cargo.toml') }} 56 | - uses: dtolnay/rust-toolchain@stable 57 | with: 58 | toolchain: ${{ env.STABLE_TOOLCHAIN }} 59 | - name: Build & run tests 60 | run: cargo test --workspace --lib --bins --tests --benches 61 | env: 62 | CARGO_INCREMENTAL: 0 63 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 64 | 65 | lint: 66 | runs-on: ubuntu-latest 67 | timeout-minutes: 30 68 | steps: 69 | - uses: actions/checkout@v4 70 | with: 71 | submodules: "true" 72 | - uses: actions/cache@v4 73 | with: 74 | path: | 75 | ~/.cargo/bin/ 76 | ~/.cargo/registry/index/ 77 | ~/.cargo/registry/cache/ 78 | ~/.cargo/git/db/ 79 | target/ 80 | key: ${{ runner.os }}-cargo-ci-${{ hashFiles('**/Cargo.toml') }} 81 | - uses: dtolnay/rust-toolchain@stable 82 | with: 83 | toolchain: ${{ env.STABLE_TOOLCHAIN }} 84 | components: rustfmt, clippy 85 | - name: Check formatting 86 | run: cargo fmt --all -- --check 87 | - name: Clippy 88 | run: cargo clippy --workspace --all-targets --all-features -- -Dwarnings 89 | 90 | # COMBAK: we disable miri for now because it hangs indefinitely. issue #73 91 | # miri: 92 | # # Explicitly use macOS 14 to take advantage of M1 chip. 93 | # runs-on: macos-14 94 | # timeout-minutes: 10 95 | # steps: 96 | # - uses: actions/checkout@v4 97 | # with: 98 | # submodules: "true" 99 | # - uses: actions/cache@v4 100 | # with: 101 | # path: | 102 | # ~/.cargo/bin/ 103 | # ~/.cargo/registry/index/ 104 | # ~/.cargo/registry/cache/ 105 | # ~/.cargo/git/db/ 106 | # target/ 107 | # key: ${{ runner.os }}-cargo-miri-${{ hashFiles('**/Cargo.toml') }} 108 | # - uses: dtolnay/rust-toolchain@master 109 | # with: 110 | # toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} 111 | # components: miri 112 | # - name: CI job 113 | # run: cargo miri test 114 | # env: 115 | # RUSTFLAGS: -Zrandomize-layout 116 | 117 | markdownlint: 118 | runs-on: ubuntu-latest 119 | timeout-minutes: 30 120 | steps: 121 | - uses: actions/checkout@v4 122 | - uses: DavidAnson/markdownlint-cli2-action@v20 123 | with: 124 | config: '.markdownlint.jsonc' 125 | globs: '**/*.md' 126 | 127 | toml: 128 | runs-on: ubuntu-latest 129 | timeout-minutes: 30 130 | steps: 131 | - uses: actions/checkout@v4 132 | with: 133 | submodules: "true" 134 | - uses: dtolnay/rust-toolchain@stable 135 | with: 136 | toolchain: ${{ env.STABLE_TOOLCHAIN }} 137 | - name: Install taplo 138 | run: cargo install taplo-cli --locked 139 | - name: Run Taplo 140 | id: taplo 141 | run: taplo fmt --check --diff 142 | - name: Taplo info 143 | if: failure() 144 | run: | 145 | echo 'To fix toml fmt, please run `taplo fmt`.' 146 | echo 'To check for a diff, run `taplo fmt --check --diff`.' 147 | echo 'You can find taplo here: https://taplo.tamasfe.dev/.' 148 | echo 'Also use the `Even Better Toml` extension.' 149 | echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml' 150 | 151 | typos: 152 | runs-on: ubuntu-latest 153 | timeout-minutes: 30 154 | steps: 155 | - uses: actions/checkout@v4 156 | - name: Check for typos 157 | uses: crate-ci/typos@v1.31.1 158 | - name: Typos info 159 | if: failure() 160 | run: | 161 | echo 'To fix typos, please run `typos -w`' 162 | echo 'To check for a diff, run `typos`' 163 | echo 'You can find typos here: https://crates.io/crates/typos' 164 | echo 'if you use VS Code, you can also install `Typos Spell Checker' 165 | echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tekumara.typos-vscode' 166 | 167 | check-doc: 168 | runs-on: ubuntu-latest 169 | timeout-minutes: 30 170 | steps: 171 | - uses: actions/checkout@v4 172 | - uses: actions/cache@v4 173 | with: 174 | path: | 175 | ~/.cargo/bin/ 176 | ~/.cargo/registry/index/ 177 | ~/.cargo/registry/cache/ 178 | ~/.cargo/git/db/ 179 | target/ 180 | key: ${{ runner.os }}-check-doc-${{ hashFiles('**/Cargo.toml') }} 181 | - uses: dtolnay/rust-toolchain@master 182 | with: 183 | toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} 184 | - name: Build doc 185 | run: cargo doc --workspace --all-features --no-deps --document-private-items --keep-going 186 | env: 187 | CARGO_INCREMENTAL: 0 188 | RUSTFLAGS: "-C debuginfo=0 --cfg docsrs_dep" 189 | - name: Check doc 190 | run: cargo test --workspace --doc 191 | env: 192 | CARGO_INCREMENTAL: 0 193 | RUSTFLAGS: "-C debuginfo=0 --cfg docsrs_dep" 194 | - name: Install cargo-deadlinks 195 | run: cargo install --force cargo-deadlinks 196 | - name: Checks dead links 197 | run: cargo deadlinks 198 | continue-on-error: true 199 | 200 | check-unused-dependencies: 201 | if: ${{ github.event_name == 'pull_request' }} 202 | runs-on: ubuntu-latest 203 | timeout-minutes: 30 204 | steps: 205 | - name: Checkout 206 | uses: actions/checkout@v4 207 | with: 208 | submodules: "true" 209 | 210 | - name: Cache 211 | uses: actions/cache@v4 212 | with: 213 | path: | 214 | ~/.cargo/bin/ 215 | ~/.cargo/registry/index/ 216 | ~/.cargo/registry/cache/ 217 | ~/.cargo/git/db/ 218 | target/ 219 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} 220 | 221 | - name: Install nightly toolchain 222 | uses: dtolnay/rust-toolchain@master 223 | with: 224 | toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} 225 | 226 | - name: Installs cargo-udeps 227 | run: cargo install --force cargo-udeps 228 | 229 | - name: Run cargo udeps 230 | run: cargo udeps 231 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Dependencies 2 | 3 | # Based on https://github.com/bevyengine/bevy/blob/main/.github/workflows/dependencies.yml 4 | 5 | on: 6 | pull_request: 7 | paths: 8 | - "**/Cargo.toml" 9 | - "deny.toml" 10 | push: 11 | paths: 12 | - "**/Cargo.toml" 13 | - "deny.toml" 14 | branches: 15 | - main 16 | 17 | concurrency: 18 | group: ${{github.workflow}}-${{github.ref}} 19 | cancel-in-progress: ${{github.event_name == 'pull_request'}} 20 | 21 | env: 22 | CARGO_TERM_COLOR: always 23 | 24 | jobs: 25 | check-advisories: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | - name: Install cargo-deny 31 | run: cargo install cargo-deny 32 | - name: Check for security advisories and unmaintained crates 33 | run: cargo deny check advisories 34 | 35 | check-bans: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: dtolnay/rust-toolchain@stable 40 | - name: Install cargo-deny 41 | run: cargo install cargo-deny 42 | - name: Check for banned and duplicated dependencies 43 | run: cargo deny check bans 44 | 45 | check-licenses: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: dtolnay/rust-toolchain@stable 50 | - name: Install cargo-deny 51 | run: cargo install cargo-deny 52 | - name: Check for unauthorized licenses 53 | run: cargo deny check licenses 54 | 55 | check-sources: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: dtolnay/rust-toolchain@stable 60 | - name: Install cargo-deny 61 | run: cargo install cargo-deny 62 | - name: Checked for unauthorized crate sources 63 | run: cargo deny check sources 64 | -------------------------------------------------------------------------------- /.github/workflows/weekly.yml: -------------------------------------------------------------------------------- 1 | name: Weekly beta compile test 2 | 3 | # Based on https://github.com/bevyengine/bevy/blob/main/.github/workflows/weekly.yml 4 | 5 | on: 6 | schedule: 7 | # New versions of rust release on Thursdays. We test on Mondays to get at least 3 days of warning before all our CI breaks again. 8 | # https://forge.rust-lang.org/release/process.html#release-day-thursday 9 | - cron: "0 12 * * 1" 10 | workflow_dispatch: 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | CARGO_INCREMENTAL: 0 15 | CARGO_PROFILE_TEST_DEBUG: 0 16 | CARGO_PROFILE_DEV_DEBUG: 0 17 | ISSUE_TITLE: Main branch fails to compile on Rust beta. 18 | 19 | # The jobs listed here are intentionally skipped when running on forks, for a number of reasons: 20 | # 21 | # * Scheduled workflows run on the base/default branch, with no way (currently) to change this. On 22 | # forks, the base/default branch is usually kept in sync with the upstream repository, meaning 23 | # that running this workflow on forks would just be a waste of resources. 24 | # 25 | # * Even if there was a way to change the branch that a scheduled workflow runs on, forks default 26 | # to not having an issue tracker. 27 | # 28 | # * Even in the event that a fork's issue tracker is enabled, most users probably don't want to 29 | # receive automated issues in the event of a compilation failure. 30 | # 31 | # Because of these reasons, this workflow is irrelevant for 99% of forks. Thus, the jobs here will 32 | # be skipped when running on any repository that isn't the upstream repository. 33 | 34 | jobs: 35 | test: 36 | strategy: 37 | matrix: 38 | os: [windows-latest, ubuntu-latest, macos-latest] 39 | runs-on: ${{ matrix.os }} 40 | # Disable this job when running on a fork. 41 | if: github.repository == 'wgsl-tooling-wg/wesl-rs/issues' 42 | timeout-minutes: 30 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: "true" 47 | - uses: dtolnay/rust-toolchain@beta 48 | - name: Build & run tests 49 | # See tools/ci/src/main.rs for the commands this runs 50 | run: cargo run -p ci -- test 51 | env: 52 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 53 | 54 | lint: 55 | runs-on: ubuntu-latest 56 | # Disable this job when running on a fork. 57 | if: github.repository == 'wgsl-tooling-wg/wesl-rs/issues' 58 | timeout-minutes: 30 59 | steps: 60 | - uses: actions/checkout@v4 61 | with: 62 | submodules: "true" 63 | - uses: dtolnay/rust-toolchain@beta 64 | with: 65 | components: rustfmt, clippy 66 | - name: Run lints 67 | # See tools/ci/src/main.rs for the commands this runs 68 | run: cargo run -p ci -- lints 69 | 70 | check-compiles: 71 | runs-on: ubuntu-latest 72 | # Disable this job when running on a fork. 73 | if: github.repository == 'wgsl-tooling-wg/wesl-rs/issues' 74 | timeout-minutes: 30 75 | needs: ["test"] 76 | steps: 77 | - uses: actions/checkout@v4 78 | with: 79 | submodules: "true" 80 | - uses: dtolnay/rust-toolchain@beta 81 | - name: Check compile test 82 | # See tools/ci/src/main.rs for the commands this runs 83 | run: cargo run -p ci -- compile 84 | 85 | close-any-open-issues: 86 | runs-on: ubuntu-latest 87 | needs: ["test", "lint", "check-compiles"] 88 | permissions: 89 | issues: write 90 | steps: 91 | - name: Close issues 92 | run: | 93 | previous_issue_number=$(gh issue list \ 94 | --search "$ISSUE_TITLE in:title" \ 95 | --json number \ 96 | --jq '.[0].number') 97 | if [[ -n $previous_issue_number ]]; then 98 | gh issue close $previous_issue_number \ 99 | -r completed \ 100 | -c $COMMENT 101 | fi 102 | env: 103 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | GH_REPO: ${{ github.repository }} 105 | COMMENT: | 106 | [Last pipeline run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) successfully completed. Closing issue. 107 | 108 | open-issue: 109 | name: Warn that weekly CI fails 110 | runs-on: ubuntu-latest 111 | needs: [test, lint, check-compiles] 112 | permissions: 113 | issues: write 114 | # We disable this job on forks, because 115 | # Use always() so the job doesn't get canceled if any other jobs fail 116 | if: ${{ github.repository == 'wgsl-tooling-wg/wesl-rs/issues' && always() && contains(needs.*.result, 'failure') }} 117 | steps: 118 | - name: Create issue 119 | run: | 120 | previous_issue_number=$(gh issue list \ 121 | --search "$ISSUE_TITLE in:title" \ 122 | --json number \ 123 | --jq '.[0].number') 124 | if [[ -n $previous_issue_number ]]; then 125 | gh issue comment $previous_issue_number \ 126 | --body "Weekly pipeline still fails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" 127 | else 128 | gh issue create \ 129 | --title "$ISSUE_TITLE" \ 130 | --label "$LABELS" \ 131 | --body "$BODY" 132 | fi 133 | env: 134 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 135 | GH_REPO: ${{ github.repository }} 136 | LABELS: 137 | BODY: | 138 | ## Weekly CI run has failed. 139 | [The offending run.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) 140 | -------------------------------------------------------------------------------- /.github/workflows/welcome.yml: -------------------------------------------------------------------------------- 1 | name: Welcome new contributors 2 | 3 | # Based on https://github.com/bevyengine/bevy/blob/main/.github/workflows/welcome.yml 4 | 5 | # This workflow has write permissions on the repo 6 | # It must not checkout a PR and run untrusted code 7 | 8 | on: 9 | pull_request_target: 10 | types: 11 | - opened 12 | 13 | permissions: 14 | pull-requests: write 15 | 16 | jobs: 17 | welcome: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/github-script@v7 21 | with: 22 | script: | 23 | // Get a list of all issues created by the PR opener 24 | // See: https://octokit.github.io/rest.js/#pagination 25 | const creator = context.payload.sender.login 26 | const opts = github.rest.issues.listForRepo.endpoint.merge({ 27 | ...context.issue, 28 | creator, 29 | state: 'all' 30 | }) 31 | const issues = await github.paginate(opts) 32 | 33 | for (const issue of issues) { 34 | if (issue.number === context.issue.number) { 35 | continue 36 | } 37 | 38 | if (issue.pull_request) { 39 | return // Creator is already a contributor. 40 | } 41 | } 42 | 43 | await github.rest.issues.createComment({ 44 | issue_number: context.issue.number, 45 | owner: context.repo.owner, 46 | repo: context.repo.repo, 47 | body: `**Welcome**, new contributor! 48 | 49 | Please make sure you have read our [contributing guide](https://github.com/wgsl-tooling-wg/wesl-rs/blob/main/CONTRIBUTING.md) and we look forward to reviewing your pull request shortly ✨` 50 | }) 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "crates/wesl-test/wesl-testsuite"] 2 | path = crates/wesl-test/wesl-testsuite 3 | url = https://github.com/wgsl-tooling-wg/wesl-testsuite 4 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-inline-html": { 4 | "allowed_elements": [ 5 | "details", 6 | "summary" 7 | ] 8 | }, 9 | "proper-names": { 10 | "code_blocks": false, 11 | "html_elements": true, 12 | "names": [ 13 | "mdBook", 14 | "Rust", 15 | "wesl-consumer", 16 | "wesl-lang", 17 | "wesl-rs", 18 | "wesl-test", 19 | "wesl-web", 20 | ".wesl", 21 | "WESL", 22 | "wgsl-parse", 23 | ".wgsl", 24 | "WGSL" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to wesl-rs 2 | 3 | Thank you for your interest in contributing to WESL! 4 | There are many ways to contribute and we appreciate all of them. 5 | 6 | The best way to get started is by asking for help in the [**# help-questions** channel on our discord](https://discord.gg/avHreKxJbe). 7 | 8 | We do not yet have documentation or a guide for wesl-rs developers. 9 | 10 | ## Bug reports 11 | 12 | Did an error message tell you to come here? 13 | If you want to create a bug report, open an issue. 14 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) The wgsl-tooling-wg Contributors. 2 | 3 | Copyrights in the wgsl-tooling-wg projects are retained by their contributors. 4 | No copyright assignment is required to contribute to the wgsl-tooling-wg projects. 5 | 6 | Some files include explicit copyright notices and/or license notices. 7 | For full authorship information, see the version control history. 8 | 9 | Except as otherwise noted, the wgsl-tooling-wg projects are licensed under the 10 | Apache License, Version 2.0 or 11 | or the MIT license or , at your option. 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/wgsl-parse", 4 | "crates/wesl-macros", 5 | "crates/wesl", 6 | "crates/wesl-test", 7 | "crates/wesl-web", 8 | "crates/wesl-cli", 9 | "crates/tokrepr", 10 | "crates/tokrepr-derive", 11 | "examples/random_wgsl", 12 | "examples/wesl-consumer", 13 | ] 14 | 15 | resolver = "3" 16 | 17 | [workspace.package] 18 | authors = ["WGSL Contributors", "Mathis Brossier "] 19 | edition = "2024" 20 | license = "MIT OR Apache-2.0" 21 | repository = "https://github.com/wgsl-tooling-wg/wesl-rs" 22 | version = "0.1.2" 23 | rust-version = "1.87.0" 24 | 25 | [workspace.dependencies] 26 | wesl = { path = "crates/wesl", version = "0.1.2" } 27 | wesl-macros = { path = "crates/wesl-macros", version = "0.1.2" } 28 | wgsl-parse = { path = "crates/wgsl-parse", version = "0.1.2" } 29 | tokrepr = { path = "crates/tokrepr", version = "0.0.1" } 30 | tokrepr-derive = { path = "crates/tokrepr-derive", version = "0.0.1" } 31 | 32 | [workspace.lints.rust] 33 | 34 | [workspace.lints.clippy] 35 | # TODO: change eventually 36 | mutable_key_type = "allow" 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WESL-Rust 2 | 3 | [![crates.io](https://img.shields.io/crates/v/wesl)][crates-io] 4 | [![docs.rs](https://img.shields.io/docsrs/wesl)][docs-rs] 5 | 6 | [crates-io]: https://crates.io/crates/wesl 7 | [docs-rs]: https://docs.rs/wesl/ 8 | [spec]: https://github.com/wgsl-tooling-wg/wesl-spec 9 | [discord]: https://discord.gg/Ng5FWmHuSv 10 | 11 | `wesl-rs` implements the necessary tools to build complex WGSL shaders, like what [naga_oil](https://github.com/bevyengine/naga_oil) does for [Bevy](https://bevyengine.org/), but in a framework-agnostic way. Visit [wesl-lang.dev](https://wesl-lang.dev/) to learn more about WGSL shader tools and language extensions. 12 | 13 | ## Status 14 | 15 | (*update: 2025-04*) 16 | 17 | * WESL recently released its first [M1 release](https://github.com/wgsl-tooling-wg/wesl-spec/issues/54). It includes imports, Conditional Compilation and Packaging. 18 | * Experimental support for WESL in Bevy was merged. 19 | 20 | Currently implemented: 21 | 22 | * [x] Imports & Modules 23 | * [x] Conditional Compilation 24 | 25 | Experimental: 26 | 27 | * [x] Cargo Packages 28 | * [x] Validation 29 | * [x] Compile-time Evaluation and Execution 30 | * [x] Polyfills 31 | 32 | Probable future work: 33 | 34 | * [ ] Namespaces 35 | * [ ] Generics 36 | 37 | ## Usage 38 | 39 | Read the [WESL for Rust tutorial](https://wesl-lang.dev/docs/Getting-Started-Rust). 40 | 41 | This project can be used as a Rust library or as a standalone CLI, refer to the following crates documentation. 42 | 43 | [![crates.io](https://img.shields.io/crates/v/wesl)](https://crates.io/crates/wesl) 44 | [![docs.rs](https://img.shields.io/docsrs/wesl)](https://docs.rs/wesl) 45 | **The crate `wesl`** is a WGSL compiler that implements the [WESL specification][spec]. 46 | 47 | [![crates.io](https://img.shields.io/crates/v/wesl)](https://crates.io/crates/wesl) 48 | [![docs.rs](https://img.shields.io/docsrs/wesl)](https://docs.rs/wesl) 49 | **The crate `wesl-cli`** is the command-line tool to run the compiler. 50 | 51 | [![crates.io](https://img.shields.io/crates/v/wgsl-parse)](https://crates.io/crates/wgsl-parse) 52 | [![docs.rs](https://img.shields.io/docsrs/wgsl-parse)](https://docs.rs/wgsl-parse) 53 | **The crate `wgsl-parse`** is a WGSL-compliant syntax tree and parser, with optional syntax extensions from the [WESL specification][spec]. 54 | 55 | ## Contributing 56 | 57 | Contributions are welcome. Please join the [discord][discord] to get in touch with the community. Read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting Pull Requests. 58 | 59 | ## License 60 | 61 | Except where noted (below and/or in individual files), all code in this repository is dual-licensed under either: 62 | 63 | * MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 64 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 65 | 66 | at your option. 67 | 68 | ### Your contributions 69 | 70 | Unless you explicitly state otherwise, 71 | any contribution intentionally submitted for inclusion in the work by you, 72 | as defined in the Apache-2.0 license, 73 | shall be dual licensed as above, 74 | without any additional terms or conditions. 75 | -------------------------------------------------------------------------------- /crates/tokrepr-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokrepr-derive" 3 | description = "Derive macro for tokrepr" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version = "0.0.1" 9 | rust-version.workspace = true 10 | 11 | [dependencies] 12 | itertools = "0.14.0" 13 | proc-macro2 = "1.0.86" 14 | quote = "1.0.37" 15 | syn = "2.0.77" 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /crates/tokrepr-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for tokrepr. 2 | //! 3 | //! 4 | 5 | use itertools::Itertools; 6 | use proc_macro2::TokenStream; 7 | use quote::{format_ident, quote}; 8 | use syn::{Data, DeriveInput, Fields, parse_macro_input}; 9 | 10 | #[proc_macro_derive(TokRepr)] 11 | pub fn derive_tokrepr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 12 | let input = parse_macro_input!(input as DeriveInput); 13 | tokrepr_impl(input).into() 14 | } 15 | 16 | pub(crate) fn tokrepr_impl(input: DeriveInput) -> TokenStream { 17 | let name = &input.ident; 18 | let tokrepr_path = quote! { tokrepr }; 19 | let self_path = quote! {}; 20 | 21 | let body = match &input.data { 22 | Data::Struct(data) => match &data.fields { 23 | Fields::Named(fields) => { 24 | let fields = fields.named.iter().map(|f| &f.ident).collect_vec(); 25 | quote! { 26 | #(let #fields = #tokrepr_path::TokRepr::tok_repr(&self.#fields);)* 27 | 28 | #tokrepr_path::quote::quote! { 29 | #self_path #name { 30 | #(#fields: # #fields,)* 31 | } 32 | } 33 | } 34 | } 35 | Fields::Unnamed(f) => { 36 | let fields = (0..f.unnamed.len()) 37 | .map(|n| format_ident!("f{n}")) 38 | .collect_vec(); 39 | 40 | quote! { 41 | #(let #fields = #tokrepr_path::TokRepr::tok_repr(#fields);)* 42 | 43 | #tokrepr_path::quote::quote!{ 44 | #self_path #name(#(# #fields,)*) 45 | } 46 | } 47 | } 48 | Fields::Unit => { 49 | quote! { 50 | #tokrepr_path::quote::quote! { 51 | #self_path #name 52 | } 53 | } 54 | } 55 | }, 56 | Data::Enum(data) => { 57 | let fields = data.variants.iter().map(|v| { 58 | let variant = &v.ident; 59 | match &v.fields { 60 | Fields::Named(_) => unimplemented!(), 61 | Fields::Unnamed(f) => { 62 | let fields = (0..f.unnamed.len()) 63 | .map(|n| format_ident!("f{n}")) 64 | .collect_vec(); 65 | 66 | quote! { 67 | Self::#variant(#(#fields),*) => { 68 | #(let #fields = #tokrepr_path::TokRepr::tok_repr(#fields);)* 69 | 70 | #tokrepr_path::quote::quote!{ 71 | #self_path #name::#variant(#(# #fields,)*) 72 | } 73 | } 74 | } 75 | }, 76 | Fields::Unit => { 77 | quote! { 78 | Self::#variant => #tokrepr_path::quote::quote! { #self_path #name::#variant } 79 | } 80 | }, 81 | } 82 | }); 83 | 84 | quote! { 85 | match self { 86 | #(#fields,)* 87 | } 88 | } 89 | } 90 | Data::Union(_) => unimplemented!("tokrepr derive is not implemented for unions"), 91 | }; 92 | 93 | quote! { 94 | impl #tokrepr_path::TokRepr for #name { 95 | fn tok_repr(&self) -> #tokrepr_path::proc_macro2::TokenStream { 96 | #body 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/tokrepr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokrepr" 3 | description = "Turn Rust instances into token representations" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version = "0.0.1" 9 | rust-version.workspace = true 10 | 11 | [dependencies] 12 | proc-macro2 = "1.0.86" 13 | quote = "1.0.37" 14 | tokrepr-derive = { workspace = true, optional = true } 15 | 16 | [features] 17 | default = ["derive"] 18 | derive = ["dep:tokrepr-derive"] 19 | rc = [] 20 | 21 | [lints] 22 | workspace = true 23 | -------------------------------------------------------------------------------- /crates/wesl-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors.workspace = true 3 | default-run = "wesl" 4 | description = "Various tools to parse, verify, evaluate and modify wgsl shader source." 5 | documentation = "https://docs.rs/wesl-cli" 6 | edition.workspace = true 7 | license.workspace = true 8 | name = "wesl-cli" 9 | repository.workspace = true 10 | version.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | clap = { version = "4.5.11", features = ["derive"] } 15 | naga = { version = "25.0.1", optional = true, features = ["wgsl-in"] } 16 | thiserror = "2.0.11" 17 | wesl = { workspace = true, features = ["eval", "generics", "package"] } 18 | wgsl-parse = { workspace = true } 19 | 20 | [[bin]] 21 | name = "wesl" 22 | path = "src/main.rs" 23 | 24 | [features] 25 | default = ["naga"] 26 | 27 | [lints] 28 | workspace = true 29 | -------------------------------------------------------------------------------- /crates/wesl-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wesl-macros" 3 | description = "Macros for the WESL rust compiler" 4 | documentation = "https://docs.rs/wesl-macros" 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | rust-version.workspace = true 11 | 12 | [dependencies] 13 | wgsl-parse = { workspace = true, features = ["wesl", "tokrepr"] } 14 | itertools = "0.14.0" 15 | proc-macro2 = "1.0.86" 16 | quote = "1.0.37" 17 | syn = "2.0.77" 18 | proc-macro-error2 = "2.0.1" 19 | token_stream_flatten = "0.1.0" 20 | 21 | [lib] 22 | proc-macro = true 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /crates/wesl-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro_error2::proc_macro_error; 2 | use query_macro::{QueryInput, query_impl}; 3 | use quote_macro::{QuoteNodeKind, quote_impl}; 4 | use syn::parse_macro_input; 5 | 6 | mod query_macro; 7 | mod quote_macro; 8 | 9 | #[proc_macro] 10 | pub fn query(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 11 | let input = parse_macro_input!(input as QueryInput); 12 | query_impl(input, false).into() 13 | } 14 | 15 | #[proc_macro] 16 | pub fn query_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 17 | let input = parse_macro_input!(input as QueryInput); 18 | query_impl(input, true).into() 19 | } 20 | 21 | #[proc_macro_error] 22 | #[proc_macro] 23 | pub fn quote_module(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 24 | quote_impl(QuoteNodeKind::TranslationUnit, input.into()).into() 25 | } 26 | #[proc_macro_error] 27 | #[proc_macro] 28 | pub fn quote_import(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 29 | quote_impl(QuoteNodeKind::ImportStatement, input.into()).into() 30 | } 31 | #[proc_macro_error] 32 | #[proc_macro] 33 | pub fn quote_declaration(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 34 | quote_impl(QuoteNodeKind::GlobalDeclaration, input.into()).into() 35 | } 36 | #[proc_macro_error] 37 | #[proc_macro] 38 | pub fn quote_literal(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 39 | quote_impl(QuoteNodeKind::Literal, input.into()).into() 40 | } 41 | #[proc_macro_error] 42 | #[proc_macro] 43 | pub fn quote_directive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 44 | quote_impl(QuoteNodeKind::GlobalDirective, input.into()).into() 45 | } 46 | #[proc_macro_error] 47 | #[proc_macro] 48 | pub fn quote_expression(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 49 | quote_impl(QuoteNodeKind::Expression, input.into()).into() 50 | } 51 | #[proc_macro_error] 52 | #[proc_macro] 53 | pub fn quote_statement(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 54 | quote_impl(QuoteNodeKind::Statement, input.into()).into() 55 | } 56 | -------------------------------------------------------------------------------- /crates/wesl-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wesl-test" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | 10 | [dependencies] 11 | serde = { version = "1.0.210", features = ["derive"] } 12 | regex = "1.11.1" 13 | 14 | [dev-dependencies] 15 | libtest-mimic = "0.8.1" 16 | serde_json = "1.0.140" 17 | wgsl-parse = { workspace = true } 18 | wesl = { workspace = true, features = ["eval", "naga_ext"] } 19 | 20 | [lints] 21 | workspace = true 22 | 23 | [[test]] 24 | name = "testsuite" 25 | harness = false 26 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/README.md: -------------------------------------------------------------------------------- 1 | # Bevy Shader Assets 2 | 3 | These files come from the Bevy engine, here: . 4 | 5 | Files translated to WESL. For the sole purpose of testing. 6 | 7 | Original files relased under various licenses by various authors, see . 8 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/animate_shader.wgsl: -------------------------------------------------------------------------------- 1 | // The time since startup data is in the globals binding which is part of the mesh_view_bindings import 2 | import bevy_pbr::{ 3 | mesh_view_bindings::globals, 4 | forward_io::VertexOutput, 5 | }; 6 | 7 | fn oklab_to_linear_srgb(c: vec3) -> vec3 { 8 | let L = c.x; 9 | let a = c.y; 10 | let b = c.z; 11 | 12 | let l_ = L + 0.3963377774 * a + 0.2158037573 * b; 13 | let m_ = L - 0.1055613458 * a - 0.0638541728 * b; 14 | let s_ = L - 0.0894841775 * a - 1.2914855480 * b; 15 | 16 | let l = l_ * l_ * l_; 17 | let m = m_ * m_ * m_; 18 | let s = s_ * s_ * s_; 19 | 20 | return vec3( 21 | 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 22 | -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 23 | -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, 24 | ); 25 | } 26 | 27 | @fragment 28 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 29 | let speed = 2.0; 30 | // The globals binding contains various global values like time 31 | // which is the time since startup in seconds 32 | let t_1 = sin(globals.time * speed) * 0.5 + 0.5; 33 | let t_2 = cos(globals.time * speed); 34 | 35 | let distance_to_center = distance(in.uv, vec2(0.5)) * 1.4; 36 | 37 | // blending is done in a perceptual color space: https://bottosson.github.io/posts/oklab/ 38 | let red = vec3(0.627955, 0.224863, 0.125846); 39 | let green = vec3(0.86644, -0.233887, 0.179498); 40 | let blue = vec3(0.701674, 0.274566, -0.169156); 41 | let white = vec3(1.0, 0.0, 0.0); 42 | let mixed = mix(mix(red, blue, t_1), mix(green, white, t_2), distance_to_center); 43 | 44 | return vec4(oklab_to_linear_srgb(mixed), 1.0); 45 | } 46 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/array_texture.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | forward_io::VertexOutput, 3 | mesh_view_bindings::view, 4 | pbr_types::{STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, PbrInput, pbr_input_new}, 5 | pbr_functions as fns, 6 | pbr_bindings, 7 | }; 8 | import bevy_core_pipeline::tonemapping::tone_mapping; 9 | 10 | @group(2) @binding(0) var my_array_texture: texture_2d_array; 11 | @group(2) @binding(1) var my_array_texture_sampler: sampler; 12 | 13 | @fragment 14 | fn fragment( 15 | @builtin(front_facing) is_front: bool, 16 | mesh: VertexOutput, 17 | ) -> @location(0) vec4 { 18 | let layer = i32(mesh.world_position.x) & 0x3; 19 | 20 | // Prepare a 'processed' StandardMaterial by sampling all textures to resolve 21 | // the material members 22 | var pbr_input: PbrInput = pbr_input_new(); 23 | 24 | pbr_input.material.base_color = textureSample(my_array_texture, my_array_texture_sampler, mesh.uv, layer); 25 | @if(VERTEX_COLORS) { 26 | pbr_input.material.base_color = pbr_input.material.base_color * mesh.color; 27 | } 28 | 29 | let double_sided = (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; 30 | 31 | pbr_input.frag_coord = mesh.position; 32 | pbr_input.world_position = mesh.world_position; 33 | pbr_input.world_normal = fns::prepare_world_normal( 34 | mesh.world_normal, 35 | double_sided, 36 | is_front, 37 | ); 38 | 39 | pbr_input.is_orthographic = view.clip_from_view[3].w == 1.0; 40 | 41 | pbr_input.N = normalize(pbr_input.world_normal); 42 | 43 | @if(VERTEX_TANGENTS) 44 | { 45 | let Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, mesh.uv, view.mip_bias).rgb; 46 | let TBN = fns::calculate_tbn_mikktspace(mesh.world_normal, mesh.world_tangent); 47 | pbr_input.N = fns::apply_normal_mapping( 48 | pbr_input.material.flags, 49 | TBN, 50 | double_sided, 51 | is_front, 52 | Nt, 53 | ); 54 | } 55 | 56 | pbr_input.V = fns::calculate_view(mesh.world_position, pbr_input.is_orthographic); 57 | 58 | return tone_mapping(fns::apply_pbr_lighting(pbr_input), view.color_grading); 59 | } 60 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/automatic_instancing.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | mesh_functions, 3 | view_transformations::position_world_to_clip 4 | }; 5 | 6 | @group(2) @binding(0) var texture: texture_2d; 7 | @group(2) @binding(1) var texture_sampler: sampler; 8 | 9 | struct Vertex { 10 | @builtin(instance_index) instance_index: u32, 11 | @location(0) position: vec3, 12 | }; 13 | 14 | struct VertexOutput { 15 | @builtin(position) clip_position: vec4, 16 | @location(0) world_position: vec4, 17 | @location(1) color: vec4, 18 | }; 19 | 20 | @vertex 21 | fn vertex(vertex: Vertex) -> VertexOutput { 22 | var out: VertexOutput; 23 | 24 | // Lookup the tag for the given mesh 25 | let tag = mesh_functions::get_tag(vertex.instance_index); 26 | var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); 27 | out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); 28 | out.clip_position = position_world_to_clip(out.world_position.xyz); 29 | 30 | let tex_dim = textureDimensions(texture); 31 | // Find the texel coordinate as derived from the tag 32 | let texel_coord = vec2(tag % tex_dim.x, tag / tex_dim.x); 33 | 34 | out.color = textureLoad(texture, texel_coord, 0); 35 | return out; 36 | } 37 | 38 | @fragment 39 | fn fragment( 40 | mesh: VertexOutput, 41 | ) -> @location(0) vec4 { 42 | return mesh.color; 43 | } 44 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/bindless_material.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | import bevy_pbr::mesh_bindings::mesh; 3 | 4 | struct Color { 5 | base_color: vec4, 6 | } 7 | 8 | @if(BINDLESS) @group(2) @binding(0) var material_color: binding_array; 9 | @else @group(2) @binding(0) var material_color: Color; 10 | @if(BINDLESS) @group(2) @binding(1) var material_color_texture: binding_array, 4>; 11 | @else @group(2) @binding(1) var material_color_texture: texture_2d; 12 | @if(BINDLESS) @group(2) @binding(2) var material_color_sampler: binding_array; 13 | @else @group(2) @binding(2) var material_color_sampler: sampler; 14 | 15 | @fragment 16 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 17 | @if(BINDLESS) 18 | let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; 19 | @if(BINDLESS) 20 | let base_color = material_color[slot].base_color; 21 | @else 22 | let base_color = material_color.base_color; 23 | 24 | @if(BINDLESS) 25 | return base_color * textureSampleLevel( 26 | material_color_texture[slot], 27 | material_color_sampler[slot], 28 | in.uv, 29 | 0.0 30 | ); 31 | @else 32 | return base_color * textureSampleLevel( 33 | material_color_texture, 34 | material_color_sampler, 35 | in.uv, 36 | 0.0 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/cubemap_unlit.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | 3 | @if(CUBEMAP_ARRAY) 4 | @group(2) @binding(0) var base_color_texture: texture_cube_array; 5 | @else 6 | @group(2) @binding(0) var base_color_texture: texture_cube; 7 | 8 | @group(2) @binding(1) var base_color_sampler: sampler; 9 | 10 | @fragment 11 | fn fragment( 12 | mesh: VertexOutput, 13 | ) -> @location(0) vec4 { 14 | let fragment_position_view_lh = mesh.world_position.xyz * vec3(1.0, 1.0, -1.0); 15 | return textureSample( 16 | base_color_texture, 17 | base_color_sampler, 18 | fragment_position_view_lh 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_clustered_decal.wgsl: -------------------------------------------------------------------------------- 1 | // This shader, a part of the `clustered_decals` example, shows how to use the 2 | // decal `tag` field to apply arbitrary decal effects. 3 | 4 | import bevy_pbr::{ 5 | clustered_forward, 6 | decal::clustered, 7 | forward_io::{VertexOutput, FragmentOutput}, 8 | mesh_view_bindings, 9 | pbr_fragment::pbr_input_from_standard_material, 10 | pbr_functions::{alpha_discard, apply_pbr_lighting, main_pass_post_lighting_processing}, 11 | }; 12 | 13 | @fragment 14 | fn fragment( 15 | in: VertexOutput, 16 | @builtin(front_facing) is_front: bool, 17 | ) -> FragmentOutput { 18 | // Generate a `PbrInput` struct from the `StandardMaterial` bindings. 19 | var pbr_input = pbr_input_from_standard_material(in, is_front); 20 | 21 | // Alpha discard. 22 | pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); 23 | 24 | // Apply the normal decals. 25 | pbr_input.material.base_color = clustered::apply_decal_base_color( 26 | in.world_position.xyz, 27 | in.position.xy, 28 | pbr_input.material.base_color 29 | ); 30 | 31 | // Here we tint the color based on the tag of the decal. 32 | // We could optionally do other things, such as adjust the normal based on a normal map. 33 | let view_z = clustered::get_view_z(in.world_position.xyz); 34 | let is_orthographic = clustered::view_is_orthographic(); 35 | let cluster_index = 36 | clustered_forward::fragment_cluster_index(in.position.xy, view_z, is_orthographic); 37 | var clusterable_object_index_ranges = 38 | clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); 39 | var decal_iterator = clustered::clustered_decal_iterator_new( 40 | in.world_position.xyz, 41 | &clusterable_object_index_ranges 42 | ); 43 | while (clustered::clustered_decal_iterator_next(&decal_iterator)) { 44 | var decal_base_color = textureSampleLevel( 45 | mesh_view_bindings::clustered_decal_textures[decal_iterator.texture_index], 46 | mesh_view_bindings::clustered_decal_sampler, 47 | decal_iterator.uv, 48 | 0.0 49 | ); 50 | 51 | switch (decal_iterator.tag) { 52 | case 1u: { 53 | // Tint with red. 54 | decal_base_color = vec4( 55 | mix(pbr_input.material.base_color.rgb, vec3(1.0, 0.0, 0.0), 0.5), 56 | decal_base_color.a, 57 | ); 58 | } 59 | case 2u: { 60 | // Tint with blue. 61 | decal_base_color = vec4( 62 | mix(pbr_input.material.base_color.rgb, vec3(0.0, 0.0, 1.0), 0.5), 63 | decal_base_color.a, 64 | ); 65 | } 66 | default: {} 67 | } 68 | 69 | pbr_input.material.base_color = vec4( 70 | mix(pbr_input.material.base_color.rgb, decal_base_color.rgb, decal_base_color.a), 71 | pbr_input.material.base_color.a + decal_base_color.a 72 | ); 73 | } 74 | 75 | // Apply lighting. 76 | var out: FragmentOutput; 77 | out.color = apply_pbr_lighting(pbr_input); 78 | 79 | // Apply in-shader post processing (fog, alpha-premultiply, and also 80 | // tonemapping, debanding if the camera is non-HDR). Note this does not 81 | // include fullscreen postprocessing effects like bloom. 82 | out.color = main_pass_post_lighting_processing(pbr_input, out.color); 83 | 84 | return out; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_gltf_2d.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_sprite::{ 2 | mesh2d_view_bindings::globals, 3 | mesh2d_functions::{get_world_from_local, mesh2d_position_local_to_clip}, 4 | }; 5 | 6 | struct Vertex { 7 | @builtin(instance_index) instance_index: u32, 8 | @location(0) position: vec3, 9 | @location(1) color: vec4, 10 | @location(2) barycentric: vec3, 11 | }; 12 | 13 | struct VertexOutput { 14 | @builtin(position) clip_position: vec4, 15 | @location(0) color: vec4, 16 | @location(1) barycentric: vec3, 17 | }; 18 | 19 | @vertex 20 | fn vertex(vertex: Vertex) -> VertexOutput { 21 | var out: VertexOutput; 22 | let world_from_local = get_world_from_local(vertex.instance_index); 23 | out.clip_position = mesh2d_position_local_to_clip(world_from_local, vec4(vertex.position, 1.0)); 24 | out.color = vertex.color; 25 | out.barycentric = vertex.barycentric; 26 | return out; 27 | } 28 | 29 | struct FragmentInput { 30 | @location(0) color: vec4, 31 | @location(1) barycentric: vec3, 32 | }; 33 | 34 | @fragment 35 | fn fragment(input: FragmentInput) -> @location(0) vec4 { 36 | let d = min(input.barycentric.x, min(input.barycentric.y, input.barycentric.z)); 37 | let t = 0.05 * (0.85 + sin(5.0 * globals.time)); 38 | return mix(vec4(1.0,1.0,1.0,1.0), input.color, smoothstep(t, t+0.01, d)); 39 | } 40 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_material.wesl: -------------------------------------------------------------------------------- 1 | import shaders::util::make_polka_dots; 2 | 3 | struct VertexOutput { 4 | @builtin(position) position: vec4, 5 | @location(2) uv: vec2, 6 | } 7 | 8 | struct CustomMaterial { 9 | time: f32, 10 | } 11 | 12 | @group(2) @binding(0) var material: CustomMaterial; 13 | 14 | @fragment 15 | fn fragment( 16 | mesh: VertexOutput, 17 | ) -> @location(0) vec4 { 18 | return make_polka_dots(mesh.uv, material.time); 19 | } -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_material.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | // we can import items from shader modules in the assets folder with a quoted path 3 | import super::custom_material_import::COLOR_MULTIPLIER; 4 | 5 | @group(2) @binding(0) var material_color: vec4; 6 | @group(2) @binding(1) var material_color_texture: texture_2d; 7 | @group(2) @binding(2) var material_color_sampler: sampler; 8 | 9 | @fragment 10 | fn fragment( 11 | mesh: VertexOutput, 12 | ) -> @location(0) vec4 { 13 | return material_color * textureSample(material_color_texture, material_color_sampler, mesh.uv) * COLOR_MULTIPLIER; 14 | } 15 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_material_2d.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_sprite::mesh2d_vertex_output::VertexOutput; 2 | // we can import items from shader modules in the assets folder with a quoted path 3 | import super::custom_material_import::COLOR_MULTIPLIER; 4 | 5 | @group(2) @binding(0) var material_color: vec4; 6 | @group(2) @binding(1) var base_color_texture: texture_2d; 7 | @group(2) @binding(2) var base_color_sampler: sampler; 8 | 9 | @fragment 10 | fn fragment(mesh: VertexOutput) -> @location(0) vec4 { 11 | return material_color * textureSample(base_color_texture, base_color_sampler, mesh.uv) * COLOR_MULTIPLIER; 12 | } 13 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_material_import.wgsl: -------------------------------------------------------------------------------- 1 | // this is made available to the importing module 2 | const COLOR_MULTIPLIER: vec4 = vec4(1.0, 1.0, 1.0, 0.5); 3 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_material_screenspace_texture.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | mesh_view_bindings::view, 3 | forward_io::VertexOutput, 4 | utils::coords_to_viewport_uv, 5 | }; 6 | 7 | @group(2) @binding(0) var texture: texture_2d; 8 | @group(2) @binding(1) var texture_sampler: sampler; 9 | 10 | @fragment 11 | fn fragment( 12 | mesh: VertexOutput, 13 | ) -> @location(0) vec4 { 14 | let viewport_uv = coords_to_viewport_uv(mesh.position.xy, view.viewport); 15 | let color = textureSample(texture, texture_sampler, viewport_uv); 16 | return color; 17 | } 18 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_phase_item.wgsl: -------------------------------------------------------------------------------- 1 | // `custom_phase_item.wgsl` 2 | // 3 | // This shader goes with the `custom_phase_item` example. It demonstrates how to 4 | // enqueue custom rendering logic in a `RenderPhase`. 5 | 6 | // The GPU-side vertex structure. 7 | struct Vertex { 8 | // The world-space position of the vertex. 9 | @location(0) position: vec3, 10 | // The color of the vertex. 11 | @location(1) color: vec3, 12 | }; 13 | 14 | // Information passed from the vertex shader to the fragment shader. 15 | struct VertexOutput { 16 | // The clip-space position of the vertex. 17 | @builtin(position) clip_position: vec4, 18 | // The color of the vertex. 19 | @location(0) color: vec3, 20 | }; 21 | 22 | // The vertex shader entry point. 23 | @vertex 24 | fn vertex(vertex: Vertex) -> VertexOutput { 25 | // Use an orthographic projection. 26 | var vertex_output: VertexOutput; 27 | vertex_output.clip_position = vec4(vertex.position.xyz, 1.0); 28 | vertex_output.color = vertex.color; 29 | return vertex_output; 30 | } 31 | 32 | // The fragment shader entry point. 33 | @fragment 34 | fn fragment(vertex_output: VertexOutput) -> @location(0) vec4 { 35 | return vec4(vertex_output.color, 1.0); 36 | } 37 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_stencil.wgsl: -------------------------------------------------------------------------------- 1 | //! A shader showing how to use the vertex position data to output the 2 | //! stencil in the right position 3 | 4 | // First we import everything we need from bevy_pbr 5 | // A 2d shader would be vevry similar but import from bevy_sprite instead 6 | import bevy_pbr::{ 7 | mesh_functions, 8 | view_transformations::position_world_to_clip 9 | }; 10 | 11 | struct Vertex { 12 | // This is needed if you are using batching and/or gpu preprocessing 13 | // It's a built in so you don't need to define it in the vertex layout 14 | @builtin(instance_index) instance_index: u32, 15 | // Like we defined for the vertex layout 16 | // position is at location 0 17 | @location(0) position: vec3, 18 | }; 19 | 20 | // This is the output of the vertex shader and we also use it as the input for the fragment shader 21 | struct VertexOutput { 22 | @builtin(position) clip_position: vec4, 23 | @location(0) world_position: vec4, 24 | }; 25 | 26 | @vertex 27 | fn vertex(vertex: Vertex) -> VertexOutput { 28 | var out: VertexOutput; 29 | // This is how bevy computes the world position 30 | // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing 31 | var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); 32 | out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); 33 | out.clip_position = position_world_to_clip(out.world_position.xyz); 34 | return out; 35 | } 36 | 37 | @fragment 38 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 39 | // Output a red color to represent the stencil of the mesh 40 | return vec4(1.0, 0.0, 0.0, 1.0); 41 | } 42 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_ui_material.wgsl: -------------------------------------------------------------------------------- 1 | // Draws a progress bar with properties defined in CustomUiMaterial 2 | import bevy_ui::ui_vertex_output::UiVertexOutput; 3 | 4 | @group(1) @binding(0) var color: vec4; 5 | @group(1) @binding(1) var slider: vec4; 6 | @group(1) @binding(2) var material_color_texture: texture_2d; 7 | @group(1) @binding(3) var material_color_sampler: sampler; 8 | @group(1) @binding(4) var border_color: vec4; 9 | 10 | 11 | @fragment 12 | fn fragment(in: UiVertexOutput) -> @location(0) vec4 { 13 | let output_color = textureSample(material_color_texture, material_color_sampler, in.uv) * color; 14 | 15 | // half size of the UI node 16 | let half_size = 0.5 * in.size; 17 | 18 | // position relative to the center of the UI node 19 | let p = in.uv * in.size - half_size; 20 | 21 | // thickness of the border closest to the current position 22 | let b = vec2( 23 | select(in.border_widths.x, in.border_widths.z, 0. < p.x), 24 | select(in.border_widths.y, in.border_widths.w, 0. < p.y) 25 | ); 26 | 27 | // select radius for the nearest corner 28 | let rs = select(in.border_radius.xy, in.border_radius.wz, 0.0 < p.y); 29 | let radius = select(rs.x, rs.y, 0.0 < p.x); 30 | 31 | // distance along each axis from the corner 32 | let d = half_size - abs(p); 33 | 34 | // if the distance to the edge from the current position on any axis 35 | // is less than the border width on that axis then the position is within 36 | // the border and we return the border color 37 | if d.x < b.x || d.y < b.y { 38 | // select radius for the nearest corner 39 | let rs = select(in.border_radius.xy, in.border_radius.wz, 0.0 < p.y); 40 | let radius = select(rs.x, rs.y, 0.0 < p.x); 41 | 42 | // determine if the point is inside the curved corner and return the corresponding color 43 | let q = radius - d; 44 | if radius < min(max(q.x, q.y), 0.0) + length(vec2(max(q.x, 0.0), max(q.y, 0.0))) { 45 | return vec4(0.0); 46 | } else { 47 | return border_color; 48 | } 49 | } 50 | 51 | // sample the texture at this position if it's to the left of the slider value 52 | // otherwise return a fully transparent color 53 | if in.uv.x < slider.x { 54 | let output_color = textureSample(material_color_texture, material_color_sampler, in.uv) * color; 55 | return output_color; 56 | } else { 57 | return vec4(0.0); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/custom_vertex_attribute.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip}; 2 | 3 | struct CustomMaterial { 4 | color: vec4, 5 | }; 6 | @group(2) @binding(0) var material: CustomMaterial; 7 | 8 | struct Vertex { 9 | @builtin(instance_index) instance_index: u32, 10 | @location(0) position: vec3, 11 | @location(1) blend_color: vec4, 12 | }; 13 | 14 | struct VertexOutput { 15 | @builtin(position) clip_position: vec4, 16 | @location(0) blend_color: vec4, 17 | }; 18 | 19 | @vertex 20 | fn vertex(vertex: Vertex) -> VertexOutput { 21 | var out: VertexOutput; 22 | out.clip_position = mesh_position_local_to_clip( 23 | get_world_from_local(vertex.instance_index), 24 | vec4(vertex.position, 1.0), 25 | ); 26 | out.blend_color = vertex.blend_color; 27 | return out; 28 | } 29 | 30 | struct FragmentInput { 31 | @location(0) blend_color: vec4, 32 | }; 33 | 34 | @fragment 35 | fn fragment(input: FragmentInput) -> @location(0) vec4 { 36 | return material.color * input.blend_color; 37 | } 38 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/extended_material.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | pbr_fragment::pbr_input_from_standard_material, 3 | pbr_functions::alpha_discard, 4 | }; 5 | 6 | @if(PREPASS_PIPELINE) 7 | import bevy_pbr::{ 8 | prepass_io::{VertexOutput, FragmentOutput}, 9 | pbr_deferred_functions::deferred_output, 10 | }; 11 | @else 12 | import bevy_pbr::{ 13 | forward_io::{VertexOutput, FragmentOutput}, 14 | pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, 15 | }; 16 | 17 | struct MyExtendedMaterial { 18 | quantize_steps: u32, 19 | } 20 | 21 | @group(2) @binding(100) 22 | var my_extended_material: MyExtendedMaterial; 23 | 24 | @fragment 25 | fn fragment( 26 | in: VertexOutput, 27 | @builtin(front_facing) is_front: bool, 28 | ) -> FragmentOutput { 29 | // generate a PbrInput struct from the StandardMaterial bindings 30 | var pbr_input = pbr_input_from_standard_material(in, is_front); 31 | 32 | // we can optionally modify the input before lighting and alpha_discard is applied 33 | pbr_input.material.base_color.b = pbr_input.material.base_color.r; 34 | 35 | // alpha discard 36 | pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); 37 | 38 | @if(PREPASS_PIPELINE) { 39 | // in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader. 40 | let out = deferred_output(in, pbr_input); 41 | 42 | return out; 43 | } 44 | @else { 45 | var out: FragmentOutput; 46 | // apply lighting 47 | out.color = apply_pbr_lighting(pbr_input); 48 | 49 | // we can optionally modify the lit color before post-processing is applied 50 | out.color = vec4(vec4(out.color * f32(my_extended_material.quantize_steps))) / f32(my_extended_material.quantize_steps); 51 | 52 | // apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr) 53 | // note this does not include fullscreen postprocessing effects like bloom. 54 | out.color = main_pass_post_lighting_processing(pbr_input, out.color); 55 | 56 | // we can optionally modify the final result here 57 | out.color = out.color * 2.0; 58 | 59 | return out; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/fallback_image_test.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | 3 | @group(2) @binding(0) var test_texture_1d: texture_1d; 4 | @group(2) @binding(1) var test_texture_1d_sampler: sampler; 5 | 6 | @group(2) @binding(2) var test_texture_2d: texture_2d; 7 | @group(2) @binding(3) var test_texture_2d_sampler: sampler; 8 | 9 | @group(2) @binding(4) var test_texture_2d_array: texture_2d_array; 10 | @group(2) @binding(5) var test_texture_2d_array_sampler: sampler; 11 | 12 | @group(2) @binding(6) var test_texture_cube: texture_cube; 13 | @group(2) @binding(7) var test_texture_cube_sampler: sampler; 14 | 15 | @group(2) @binding(8) var test_texture_cube_array: texture_cube_array; 16 | @group(2) @binding(9) var test_texture_cube_array_sampler: sampler; 17 | 18 | @group(2) @binding(10) var test_texture_3d: texture_3d; 19 | @group(2) @binding(11) var test_texture_3d_sampler: sampler; 20 | 21 | @fragment 22 | fn fragment(in: VertexOutput) {} 23 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/game_of_life.wgsl: -------------------------------------------------------------------------------- 1 | // The shader reads the previous frame's state from the `input` texture, and writes the new state of 2 | // each pixel to the `output` texture. The textures are flipped each step to progress the 3 | // simulation. 4 | // Two textures are needed for the game of life as each pixel of step N depends on the state of its 5 | // neighbors at step N-1. 6 | 7 | @group(0) @binding(0) var input: texture_storage_2d; 8 | 9 | @group(0) @binding(1) var output: texture_storage_2d; 10 | 11 | fn hash(value: u32) -> u32 { 12 | var state = value; 13 | state = state ^ 2747636419u; 14 | state = state * 2654435769u; 15 | state = (state ^ state) >> 16u; // FYI: in the upstream bevy file, there are no parenthesis here but it's a Naga bug https://github.com/gfx-rs/wgpu/issues/6397 16 | state = state * 2654435769u; 17 | state = (state ^ state) >> 16u; 18 | state = state * 2654435769u; 19 | return state; 20 | } 21 | 22 | fn randomFloat(value: u32) -> f32 { 23 | return f32(hash(value)) / 4294967295.0; 24 | } 25 | 26 | @compute @workgroup_size(8, 8, 1) 27 | fn init(@builtin(global_invocation_id) invocation_id: vec3, @builtin(num_workgroups) num_workgroups: vec3) { 28 | let location = vec2(i32(invocation_id.x), i32(invocation_id.y)); 29 | 30 | let randomNumber = randomFloat(invocation_id.y << (16u | invocation_id.x)); 31 | let alive = randomNumber > 0.9; 32 | let color = vec4(f32(alive)); 33 | 34 | textureStore(output, location, color); 35 | } 36 | 37 | fn is_alive(location: vec2, offset_x: i32, offset_y: i32) -> i32 { 38 | let value: vec4 = textureLoad(input, location + vec2(offset_x, offset_y)); 39 | return i32(value.x); 40 | } 41 | 42 | fn count_alive(location: vec2) -> i32 { 43 | return is_alive(location, -1, -1) + 44 | is_alive(location, -1, 0) + 45 | is_alive(location, -1, 1) + 46 | is_alive(location, 0, -1) + 47 | is_alive(location, 0, 1) + 48 | is_alive(location, 1, -1) + 49 | is_alive(location, 1, 0) + 50 | is_alive(location, 1, 1); 51 | } 52 | 53 | @compute @workgroup_size(8, 8, 1) 54 | fn update(@builtin(global_invocation_id) invocation_id: vec3) { 55 | let location = vec2(i32(invocation_id.x), i32(invocation_id.y)); 56 | 57 | let n_alive = count_alive(location); 58 | 59 | var alive: bool; 60 | if (n_alive == 3) { 61 | alive = true; 62 | } else if (n_alive == 2) { 63 | let currently_alive = is_alive(location, 0, 0); 64 | alive = bool(currently_alive); 65 | } else { 66 | alive = false; 67 | } 68 | let color = vec4(f32(alive)); 69 | 70 | textureStore(output, location, color); 71 | } 72 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/gpu_readback.wgsl: -------------------------------------------------------------------------------- 1 | // This shader is used for the gpu_readback example 2 | // The actual work it does is not important for the example 3 | 4 | // This is the data that lives in the gpu only buffer 5 | @group(0) @binding(0) var data: array; 6 | @group(0) @binding(1) var texture: texture_storage_2d; 7 | 8 | @compute @workgroup_size(1) 9 | fn main(@builtin(global_invocation_id) global_id: vec3) { 10 | // We use the global_id to index the array to make sure we don't 11 | // access data used in another workgroup 12 | data[global_id.x] += 1u; 13 | // Write the same data to the texture 14 | textureStore(texture, vec2(i32(global_id.x), 0), vec4(data[global_id.x], 0, 0, 0)); 15 | } 16 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/instancing.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip}; 2 | 3 | struct Vertex { 4 | @location(0) position: vec3, 5 | @location(1) normal: vec3, 6 | @location(2) uv: vec2, 7 | 8 | @location(3) i_pos_scale: vec4, 9 | @location(4) i_color: vec4, 10 | }; 11 | 12 | struct VertexOutput { 13 | @builtin(position) clip_position: vec4, 14 | @location(0) color: vec4, 15 | }; 16 | 17 | @vertex 18 | fn vertex(vertex: Vertex) -> VertexOutput { 19 | let position = vertex.position * vertex.i_pos_scale.w + vertex.i_pos_scale.xyz; 20 | var out: VertexOutput; 21 | // NOTE: Passing 0 as the instance_index to get_world_from_local() is a hack 22 | // for this example as the instance_index builtin would map to the wrong 23 | // index in the Mesh array. This index could be passed in via another 24 | // uniform instead but it's unnecessary for the example. 25 | out.clip_position = mesh_position_local_to_clip( 26 | get_world_from_local(0u), 27 | vec4(position, 1.0) 28 | ); 29 | out.color = vertex.i_color; 30 | return out; 31 | } 32 | 33 | @fragment 34 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 35 | return in.color; 36 | } 37 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/irradiance_volume_voxel_visualization.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | import bevy_pbr::irradiance_volume; 3 | import bevy_pbr::mesh_view_bindings; 4 | import bevy_pbr::clustered_forward; 5 | 6 | struct VoxelVisualizationIrradianceVolumeInfo { 7 | world_from_voxel: mat4x4, 8 | voxel_from_world: mat4x4, 9 | resolution: vec3, 10 | // A scale factor that's applied to the diffuse and specular light from the 11 | // light probe. This is in units of cd/m² (candela per square meter). 12 | intensity: f32, 13 | } 14 | 15 | @group(2) @binding(100) 16 | var irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo; 17 | 18 | @fragment 19 | fn fragment(mesh: VertexOutput) -> @location(0) vec4 { 20 | // Snap the world position we provide to `irradiance_volume_light()` to the 21 | // middle of the nearest texel. 22 | var unit_pos = (irradiance_volume_info.voxel_from_world * 23 | vec4(mesh.world_position.xyz, 1.0f)).xyz; 24 | let resolution = vec3(irradiance_volume_info.resolution); 25 | let stp = clamp((unit_pos + 0.5) * resolution, vec3(0.5f), resolution - vec3(0.5f)); 26 | let stp_rounded = round(stp - 0.5f) + 0.5f; 27 | let rounded_world_pos = (irradiance_volume_info.world_from_voxel * vec4(stp_rounded, 1.0f)).xyz; 28 | 29 | // Look up the irradiance volume range in the cluster list. 30 | let view_z = dot(vec4( 31 | mesh_view_bindings::view.view_from_world[0].z, 32 | mesh_view_bindings::view.view_from_world[1].z, 33 | mesh_view_bindings::view.view_from_world[2].z, 34 | mesh_view_bindings::view.view_from_world[3].z 35 | ), mesh.world_position); 36 | let cluster_index = clustered_forward::fragment_cluster_index(mesh.position.xy, view_z, false); 37 | var clusterable_object_index_ranges = 38 | clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); 39 | 40 | // `irradiance_volume_light()` multiplies by intensity, so cancel it out. 41 | // If we take intensity into account, the cubes will be way too bright. 42 | let rgb = irradiance_volume::irradiance_volume_light( 43 | mesh.world_position.xyz, 44 | mesh.world_normal, 45 | &clusterable_object_index_ranges, 46 | ) / irradiance_volume_info.intensity; 47 | 48 | return vec4(rgb, 1.0f); 49 | } 50 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/line_material.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | 3 | struct LineMaterial { 4 | color: vec4, 5 | }; 6 | 7 | @group(2) @binding(0) var material: LineMaterial; 8 | 9 | @fragment 10 | fn fragment( 11 | mesh: VertexOutput, 12 | ) -> @location(0) vec4 { 13 | return material.color; 14 | } 15 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/post_processing.wgsl: -------------------------------------------------------------------------------- 1 | // This shader computes the chromatic aberration effect 2 | 3 | // Since post processing is a fullscreen effect, we use the fullscreen vertex shader provided by bevy. 4 | // This will import a vertex shader that renders a single fullscreen triangle. 5 | // 6 | // A fullscreen triangle is a single triangle that covers the entire screen. 7 | // The box in the top left in that diagram is the screen. The 4 x are the corner of the screen 8 | // 9 | // Y axis 10 | // 1 | x-----x...... 11 | // 0 | | s | . ´ 12 | // -1 | x_____x´ 13 | // -2 | : .´ 14 | // -3 | :´ 15 | // +--------------- X axis 16 | // -1 0 1 2 3 17 | // 18 | // As you can see, the triangle ends up bigger than the screen. 19 | // 20 | // You don't need to worry about this too much since bevy will compute the correct UVs for you. 21 | import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput; 22 | 23 | @group(0) @binding(0) var screen_texture: texture_2d; 24 | @group(0) @binding(1) var texture_sampler: sampler; 25 | struct PostProcessSettings { 26 | intensity: f32, 27 | @if(SIXTEEN_BYTE_ALIGNMENT) 28 | // WebGL2 structs must be 16 byte aligned. 29 | _webgl2_padding: vec3 30 | } 31 | @group(0) @binding(2) var settings: PostProcessSettings; 32 | 33 | @fragment 34 | fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { 35 | // Chromatic aberration strength 36 | let offset_strength = settings.intensity; 37 | 38 | // Sample each color channel with an arbitrary shift 39 | return vec4( 40 | textureSample(screen_texture, texture_sampler, in.uv + vec2(offset_strength, -offset_strength)).r, 41 | textureSample(screen_texture, texture_sampler, in.uv + vec2(-offset_strength, 0.0)).g, 42 | textureSample(screen_texture, texture_sampler, in.uv + vec2(0.0, offset_strength)).b, 43 | 1.0 44 | ); 45 | } 46 | 47 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/shader_defs.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | 3 | struct CustomMaterial { 4 | color: vec4, 5 | }; 6 | 7 | @group(2) @binding(0) var material: CustomMaterial; 8 | 9 | @fragment 10 | fn fragment( 11 | mesh: VertexOutput, 12 | ) -> @location(0) vec4 { 13 | @if(IS_RED) 14 | return vec4(1.0, 0.0, 0.0, 1.0); 15 | @else 16 | return material.color; 17 | } 18 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/show_prepass.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | mesh_view_bindings::globals, 3 | prepass_utils, 4 | forward_io::VertexOutput, 5 | }; 6 | 7 | struct ShowPrepassSettings { 8 | show_depth: u32, 9 | show_normals: u32, 10 | show_motion_vectors: u32, 11 | padding_1: u32, 12 | padding_2: u32, 13 | } 14 | @group(2) @binding(0) var settings: ShowPrepassSettings; 15 | 16 | @fragment 17 | fn fragment( 18 | @if(MULTISAMPLED) @builtin(sample_index) sample_index: u32, 19 | mesh: VertexOutput, 20 | ) -> @location(0) vec4 { 21 | @if(!MULTISAMPLED) let sample_index = 0u; 22 | if settings.show_depth == 1u { 23 | let depth = bevy_pbr::prepass_utils::prepass_depth(mesh.position, sample_index); 24 | return vec4(depth, depth, depth, 1.0); 25 | } else if settings.show_normals == 1u { 26 | let normal = bevy_pbr::prepass_utils::prepass_normal(mesh.position, sample_index); 27 | return vec4(normal, 1.0); 28 | } else if settings.show_motion_vectors == 1u { 29 | let motion_vector = bevy_pbr::prepass_utils::prepass_motion_vector(mesh.position, sample_index); 30 | return vec4(motion_vector / globals.delta_time, 0.0, 1.0); 31 | } 32 | 33 | return vec4(0.0); 34 | } 35 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/specialized_mesh_pipeline.wgsl: -------------------------------------------------------------------------------- 1 | //! Very simple shader used to demonstrate how to get the world position and pass data 2 | //! between the vertex and fragment shader. Also shows the custom vertex layout. 3 | 4 | // First we import everything we need from bevy_pbr 5 | // A 2d shader would be vevry similar but import from bevy_sprite instead 6 | import bevy_pbr::{ 7 | mesh_functions, 8 | view_transformations::position_world_to_clip 9 | }; 10 | 11 | struct Vertex { 12 | // This is needed if you are using batching and/or gpu preprocessing 13 | // It's a built in so you don't need to define it in the vertex layout 14 | @builtin(instance_index) instance_index: u32, 15 | // Like we defined for the vertex layout 16 | // position is at location 0 17 | @location(0) position: vec3, 18 | // and color at location 1 19 | @location(1) color: vec4, 20 | }; 21 | 22 | // This is the output of the vertex shader and we also use it as the input for the fragment shader 23 | struct VertexOutput { 24 | @builtin(position) clip_position: vec4, 25 | @location(0) world_position: vec4, 26 | @location(1) color: vec3, 27 | }; 28 | 29 | @vertex 30 | fn vertex(vertex: Vertex) -> VertexOutput { 31 | var out: VertexOutput; 32 | // This is how bevy computes the world position 33 | // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing 34 | var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); 35 | out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); 36 | out.clip_position = position_world_to_clip(out.world_position.xyz); 37 | 38 | // We just use the raw vertex color 39 | out.color = vertex.color.rgb; 40 | 41 | return out; 42 | } 43 | 44 | @fragment 45 | fn fragment(in: VertexOutput) -> @location(0) vec4 { 46 | // output the color directly 47 | return vec4(in.color, 1.0); 48 | } 49 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/storage_buffer.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | mesh_functions, 3 | view_transformations::position_world_to_clip 4 | }; 5 | 6 | @group(2) @binding(0) var colors: array, 5>; 7 | 8 | struct Vertex { 9 | @builtin(instance_index) instance_index: u32, 10 | @location(0) position: vec3, 11 | }; 12 | 13 | struct VertexOutput { 14 | @builtin(position) clip_position: vec4, 15 | @location(0) world_position: vec4, 16 | @location(1) color: vec4, 17 | }; 18 | 19 | @vertex 20 | fn vertex(vertex: Vertex) -> VertexOutput { 21 | var out: VertexOutput; 22 | let tag = mesh_functions::get_tag(vertex.instance_index); 23 | var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); 24 | out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); 25 | out.clip_position = position_world_to_clip(out.world_position.xyz); 26 | 27 | out.color = colors[tag]; 28 | return out; 29 | } 30 | 31 | @fragment 32 | fn fragment( 33 | mesh: VertexOutput, 34 | ) -> @location(0) vec4 { 35 | return mesh.color; 36 | } 37 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/texture_binding_array.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::forward_io::VertexOutput; 2 | 3 | @group(2) @binding(0) var textures: binding_array>; 4 | @group(2) @binding(1) var nearest_sampler: sampler; 5 | // We can also have array of samplers 6 | // var samplers: binding_array; 7 | 8 | @fragment 9 | fn fragment( 10 | mesh: VertexOutput, 11 | ) -> @location(0) vec4 { 12 | // Select the texture to sample from using non-uniform uv coordinates 13 | let coords = clamp(vec2(mesh.uv * 4.0), vec2(0u), vec2(3u)); 14 | let index = coords.y * 4u + coords.x; 15 | let inner_uv = fract(mesh.uv * 4.0); 16 | return textureSample(textures[index], nearest_sampler, inner_uv); 17 | } 18 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/tonemapping_test_patterns.wgsl: -------------------------------------------------------------------------------- 1 | import bevy_pbr::{ 2 | mesh_view_bindings, 3 | forward_io::VertexOutput, 4 | }; 5 | 6 | import bevy_render::maths::PI; 7 | 8 | @if(TONEMAP_IN_SHADER) 9 | import bevy_core_pipeline::tonemapping::tone_mapping; 10 | 11 | // Sweep across hues on y axis with value from 0.0 to +15EV across x axis 12 | // quantized into 24 steps for both axis. 13 | fn color_sweep(uv_input: vec2) -> vec3 { 14 | var uv = uv_input; 15 | let steps = 24.0; 16 | uv.y = uv.y * (1.0 + 1.0 / steps); 17 | let ratio = 2.0; 18 | 19 | let h = PI * 2.0 * floor(1.0 + steps * uv.y) / steps; 20 | let L = floor(uv.x * steps * ratio) / (steps * ratio) - 0.5; 21 | 22 | var color = vec3(0.0); 23 | if uv.y < 1.0 { 24 | color = cos(h + vec3(0.0, 1.0, 2.0) * PI * 2.0 / 3.0); 25 | let maxRGB = max(color.r, max(color.g, color.b)); 26 | let minRGB = min(color.r, min(color.g, color.b)); 27 | color = exp(15.0 * L) * (color - minRGB) / (maxRGB - minRGB); 28 | } else { 29 | color = vec3(exp(15.0 * L)); 30 | } 31 | return color; 32 | } 33 | 34 | fn hsv_to_srgb(c: vec3) -> vec3 { 35 | let K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 36 | let p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 37 | return c.z * mix(K.xxx, clamp(p - K.xxx, vec3(0.0), vec3(1.0)), c.y); 38 | } 39 | 40 | // Generates a continuous sRGB sweep. 41 | fn continuous_hue(uv: vec2) -> vec3 { 42 | return hsv_to_srgb(vec3(uv.x, 1.0, 1.0)) * max(0.0, exp2(uv.y * 9.0) - 1.0); 43 | } 44 | 45 | @fragment 46 | fn fragment( 47 | in: VertexOutput, 48 | ) -> @location(0) vec4 { 49 | var uv = in.uv; 50 | var out = vec3(0.0); 51 | if uv.y > 0.5 { 52 | uv.y = 1.0 - uv.y; 53 | out = color_sweep(vec2(uv.x, uv.y * 2.0)); 54 | } else { 55 | out = continuous_hue(vec2(uv.y * 2.0, uv.x)); 56 | } 57 | var color = vec4(out, 1.0); 58 | @if(TONEMAP_IN_SHADER) { 59 | color = tone_mapping(color, mesh_view_bindings::view.color_grading); 60 | } 61 | return color; 62 | } 63 | -------------------------------------------------------------------------------- /crates/wesl-test/bevy/util.wesl: -------------------------------------------------------------------------------- 1 | fn make_polka_dots(pos: vec2, time: f32) -> vec4 { 2 | // Create repeating circles 3 | let scaled_pos = pos * 6.0; 4 | let cell = vec2(fract(scaled_pos.x), fract(scaled_pos.y)); 5 | let dist_from_center = distance(cell, vec2(0.5)); 6 | 7 | // Make dots alternate between pink and purple 8 | let is_even = (floor(scaled_pos.x) + floor(scaled_pos.y)) % 2.0; 9 | 10 | var dot_color = vec3(0.0); 11 | @if(!PARTY_MODE) { 12 | let color1 = vec3(1.0, 0.4, 0.8); // pink 13 | let color2 = vec3(0.6, 0.2, 1.0); // purple 14 | dot_color = mix(color1, color2, is_even); 15 | } 16 | // Animate the colors in party mode 17 | @if(PARTY_MODE) { 18 | let color1 = vec3(1.0, 0.2, 0.2); // red 19 | let color2 = vec3(0.2, 0.2, 1.0); // blue 20 | let oscillation = (sin(time * 10.0) + 1.0) * 0.5; 21 | let animated_color1 = mix(color1, color2, oscillation); 22 | let animated_color2 = mix(color2, color1, oscillation); 23 | dot_color = mix(animated_color1, animated_color2, is_even); 24 | } 25 | 26 | // Draw the dot 27 | let is_dot = step(dist_from_center, 0.3); 28 | return vec4(dot_color * is_dot, is_dot); 29 | } -------------------------------------------------------------------------------- /crates/wesl-test/bevy/water_material.wgsl: -------------------------------------------------------------------------------- 1 | // A shader that creates water ripples by overlaying 4 normal maps on top of one 2 | // another. 3 | // 4 | // This is used in the `ssr` example. It only supports deferred rendering. 5 | 6 | import bevy_pbr::{ 7 | pbr_deferred_functions::deferred_output, 8 | pbr_fragment::pbr_input_from_standard_material, 9 | prepass_io::{VertexOutput, FragmentOutput}, 10 | }; 11 | import bevy_render::globals::Globals; 12 | 13 | // Parameters to the water shader. 14 | struct WaterSettings { 15 | // How much to displace each octave each frame, in the u and v directions. 16 | // Two octaves are packed into each `vec4`. 17 | octave_vectors: array, 2>, 18 | // How wide the waves are in each octave. 19 | octave_scales: vec4, 20 | // How high the waves are in each octave. 21 | octave_strengths: vec4, 22 | } 23 | 24 | @group(0) @binding(1) var globals: Globals; 25 | 26 | @group(2) @binding(100) var water_normals_texture: texture_2d; 27 | @group(2) @binding(101) var water_normals_sampler: sampler; 28 | @group(2) @binding(102) var water_settings: WaterSettings; 29 | 30 | // Samples a single octave of noise and returns the resulting normal. 31 | fn sample_noise_octave(uv: vec2, strength: f32) -> vec3 { 32 | let N = textureSample(water_normals_texture, water_normals_sampler, uv).rbg * 2.0 - 1.0; 33 | // This isn't slerp, but it's good enough. 34 | return normalize(mix(vec3(0.0, 1.0, 0.0), N, strength)); 35 | } 36 | 37 | // Samples all four octaves of noise and returns the resulting normal. 38 | fn sample_noise(uv: vec2, time: f32) -> vec3 { 39 | let uv0 = uv * water_settings.octave_scales[0] + water_settings.octave_vectors[0].xy * time; 40 | let uv1 = uv * water_settings.octave_scales[1] + water_settings.octave_vectors[0].zw * time; 41 | let uv2 = uv * water_settings.octave_scales[2] + water_settings.octave_vectors[1].xy * time; 42 | let uv3 = uv * water_settings.octave_scales[3] + water_settings.octave_vectors[1].zw * time; 43 | return normalize( 44 | sample_noise_octave(uv0, water_settings.octave_strengths[0]) + 45 | sample_noise_octave(uv1, water_settings.octave_strengths[1]) + 46 | sample_noise_octave(uv2, water_settings.octave_strengths[2]) + 47 | sample_noise_octave(uv3, water_settings.octave_strengths[3]) 48 | ); 49 | } 50 | 51 | @fragment 52 | fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput { 53 | // Create the PBR input. 54 | var pbr_input = pbr_input_from_standard_material(in, is_front); 55 | // Bump the normal. 56 | pbr_input.N = sample_noise(in.uv, globals.time); 57 | // Send the rest to the deferred shader. 58 | return deferred_output(in, pbr_input); 59 | } 60 | -------------------------------------------------------------------------------- /crates/wesl-test/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /crates/wesl-test/spec-tests/idents.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ident-allow-unicode", 4 | "desc": "Tests valid unicode identifiers", 5 | "kind": "syntax", 6 | "syntax": "declaration", 7 | "code": "const Δέλτα = 1u; const réflexion = 1u; const Кызыл = 1u; const 𐰓𐰏𐰇 = 1u; const 朝焼け = 1u; const سلام = 1u; const 검정 = 1u; const שָׁלוֹם = 1u; const गुलाबी = 1u; const փիրուզ = 1u;", 8 | "expect": "pass" 9 | }, 10 | { 11 | "name": "ident-disallow-digit-first-char", 12 | "desc": "Tests identifier cannot start with a digit", 13 | "kind": "syntax", 14 | "syntax": "declaration", 15 | "code": "const 1 = 1u", 16 | "expect": "fail" 17 | }, 18 | { 19 | "name": "ident-disallow-two-leading-underscores", 20 | "desc": "Tests identifier cannot start with two underscores", 21 | "kind": "syntax", 22 | "syntax": "declaration", 23 | "code": "const __foo = 1u", 24 | "expect": "fail" 25 | }, 26 | { 27 | "name": "ident-disallow-single-underscore", 28 | "desc": "Tests identifier cannot be a single underscore", 29 | "kind": "syntax", 30 | "syntax": "declaration", 31 | "code": "const _ = 1u", 32 | "expect": "fail" 33 | }, 34 | { 35 | "name": "ident-disallow-reserved-words", 36 | "desc": "Tests WGSL reserved words are not valid identifiers", 37 | "kind": "syntax", 38 | "syntax": "declaration", 39 | "code": "const common = 1u;", 40 | "expect": "fail" 41 | }, 42 | { 43 | "name": "import-path-allow-reserved-words", 44 | "desc": "Tests WGSL reserved words are valid module paths", 45 | "kind": "syntax", 46 | "syntax": "declaration", 47 | "code": "import common::NULL::await::export::of::wgsl::item;", 48 | "expect": "pass" 49 | }, 50 | { 51 | "name": "import-path-allow-keywords", 52 | "desc": "Tests WGSL keywords are valid module paths", 53 | "kind": "syntax", 54 | "syntax": "declaration", 55 | "code": "import alias::break::const::override::true::item;", 56 | "expect": "pass" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /crates/wesl-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod schemas; 2 | -------------------------------------------------------------------------------- /crates/wesl-test/src/schemas.rs: -------------------------------------------------------------------------------- 1 | //! File containing the serde data structures for JSON test files. 2 | //! See schemas: 3 | 4 | use std::{ 5 | collections::HashMap, 6 | fmt::{self}, 7 | }; 8 | 9 | use regex::Regex; 10 | use serde::Deserialize; 11 | 12 | #[derive(Deserialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct WgslTestSrc { 15 | pub name: String, 16 | pub wesl_src: HashMap, 17 | #[serde(default)] 18 | pub notes: Option, 19 | #[allow(unused)] 20 | #[serde(default)] 21 | pub expected_wgsl: Option, 22 | #[serde(default)] 23 | pub underscore_wgsl: Option, 24 | } 25 | 26 | impl WgslTestSrc { 27 | pub fn normalize(&mut self) { 28 | self.wesl_src.values_mut().for_each(|src| { 29 | *src = normalize_wgsl(src); 30 | }); 31 | if let Some(expected_wgsl) = &self.expected_wgsl { 32 | self.expected_wgsl = Some(normalize_wgsl(expected_wgsl)) 33 | } 34 | if let Some(underscore_wgsl) = &self.underscore_wgsl { 35 | self.underscore_wgsl = Some(normalize_wgsl(underscore_wgsl)) 36 | } 37 | } 38 | } 39 | 40 | impl fmt::Display for WgslTestSrc { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!(f, "{}", self.name) 43 | } 44 | } 45 | 46 | #[derive(Deserialize)] 47 | pub struct ParsingTest { 48 | pub src: String, 49 | #[serde(default)] 50 | pub fails: bool, 51 | } 52 | 53 | impl ParsingTest { 54 | pub fn normalize(&mut self) { 55 | self.src = normalize_wgsl(&self.src) 56 | } 57 | } 58 | 59 | impl fmt::Display for ParsingTest { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | write!(f, "`{}`", normalize_wgsl(&self.src)) 62 | } 63 | } 64 | 65 | fn normalize_wgsl(wgsl: &str) -> String { 66 | let re = Regex::new(r"\s+").unwrap(); 67 | re.replace_all(wgsl, " ").trim().to_string() 68 | } 69 | 70 | #[allow(unused)] 71 | #[derive(Deserialize)] 72 | #[serde(rename_all = "camelCase")] 73 | pub struct BulkTest { 74 | pub name: String, 75 | pub base_dir: String, 76 | pub exclude: Option>, 77 | pub include: Option>, 78 | pub glob_include: Option>, 79 | } 80 | 81 | #[derive(Deserialize)] 82 | pub struct Test { 83 | pub name: String, 84 | pub desc: String, 85 | #[serde(flatten)] 86 | pub kind: TestKind, 87 | pub code: String, 88 | pub expect: Expectation, 89 | pub note: Option, 90 | pub skip: Option, 91 | pub issue: Option, 92 | } 93 | 94 | impl fmt::Display for Test { 95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 96 | write!(f, "{}", self.name) 97 | } 98 | } 99 | 100 | #[derive(PartialEq, Deserialize)] 101 | #[serde(rename_all = "lowercase")] 102 | pub enum SyntaxKind { 103 | Declaration, 104 | Statement, 105 | Expression, 106 | } 107 | 108 | #[derive(PartialEq, Deserialize)] 109 | #[serde(rename_all = "lowercase")] 110 | #[serde(tag = "kind")] 111 | pub enum TestKind { 112 | Syntax { 113 | syntax: SyntaxKind, 114 | }, 115 | Eval { 116 | eval: String, 117 | result: Option, // must be None when expect is Fail, must be Some when expect is Pass 118 | }, 119 | Context, 120 | } 121 | 122 | impl fmt::Display for TestKind { 123 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 124 | match self { 125 | TestKind::Syntax { .. } => f.write_str("Syntax"), 126 | TestKind::Eval { .. } => f.write_str("Eval"), 127 | TestKind::Context => f.write_str("Context"), 128 | } 129 | } 130 | } 131 | 132 | #[derive(Deserialize, PartialEq, Eq)] 133 | #[serde(rename_all = "lowercase")] 134 | pub enum Expectation { 135 | Pass, 136 | Fail, 137 | } 138 | 139 | impl From for Expectation { 140 | fn from(value: bool) -> Self { 141 | if value { 142 | Self::Pass 143 | } else { 144 | Self::Fail 145 | } 146 | } 147 | } 148 | 149 | impl fmt::Display for Expectation { 150 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 151 | match self { 152 | Expectation::Pass => f.write_str("Pass"), 153 | Expectation::Fail => f.write_str("Fail"), 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crates/wesl-test/unity_web_research/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Unity Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /crates/wesl-test/unity_web_research/README.md: -------------------------------------------------------------------------------- 1 | # Unity WGSL testing shader source code 2 | 3 | The shaders in this repository have been extracted at various points of the compilation process for the WebGPU Shading Language. They are intended for shader compilation research only. 4 | 5 | Shaders are compiled to multiple variants, so there may be multiple shaders that are similar. 6 | The filename is a unique identifier for the shader. Each shader goes through multiple stages of 7 | compilation: 8 | 9 | * **.hlsl**: The original code for the shader. 10 | * **.glsl**: HLSL shader code is tranlsated to GLSL. 11 | * **.spv**: GLSL shader code is translated to SPIR-V. 12 | * **.wgsl**: SPIR-V is translated to WGSL using the Tint library. 13 | * **.wgsl_error**: If there was an error translating SPIR-V into WGSL, this the error message generated by Tint. 14 | 15 | License: MIT (see LICENSE-MIT) 16 | 17 | Unless expressly provided otherwise, the software under this 18 | license is made available strictly on an "AS IS" BASIS WITHOUT 19 | WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the 20 | license for details on these and other terms and conditions. 21 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/README.md: -------------------------------------------------------------------------------- 1 | # webgpu-samples 2 | 3 | Samples taken from . 4 | 5 | ## License 6 | 7 | See [LICENSE.txt](LICENSE.txt) 8 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/basic.vert.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelViewProjectionMatrix : mat4x4f, 3 | } 4 | @binding(0) @group(0) var uniforms : Uniforms; 5 | 6 | struct VertexOutput { 7 | @builtin(position) Position : vec4f, 8 | @location(0) fragUV : vec2f, 9 | @location(1) fragPosition: vec4f, 10 | } 11 | 12 | @vertex 13 | fn main( 14 | @location(0) position : vec4f, 15 | @location(1) uv : vec2f 16 | ) -> VertexOutput { 17 | var output : VertexOutput; 18 | output.Position = uniforms.modelViewProjectionMatrix * position; 19 | output.fragUV = uv; 20 | output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); 21 | return output; 22 | } 23 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/blur.wgsl: -------------------------------------------------------------------------------- 1 | struct Params { 2 | filterDim : i32, 3 | blockDim : u32, 4 | } 5 | 6 | @group(0) @binding(0) var samp : sampler; 7 | @group(0) @binding(1) var params : Params; 8 | @group(1) @binding(1) var inputTex : texture_2d; 9 | @group(1) @binding(2) var outputTex : texture_storage_2d; 10 | 11 | struct Flip { 12 | value : u32, 13 | } 14 | @group(1) @binding(3) var flip : Flip; 15 | 16 | // This shader blurs the input texture in one direction, depending on whether 17 | // |flip.value| is 0 or 1. 18 | // It does so by running (128 / 4) threads per workgroup to load 128 19 | // texels into 4 rows of shared memory. Each thread loads a 20 | // 4 x 4 block of texels to take advantage of the texture sampling 21 | // hardware. 22 | // Then, each thread computes the blur result by averaging the adjacent texel values 23 | // in shared memory. 24 | // Because we're operating on a subset of the texture, we cannot compute all of the 25 | // results since not all of the neighbors are available in shared memory. 26 | // Specifically, with 128 x 128 tiles, we can only compute and write out 27 | // square blocks of size 128 - (filterSize - 1). We compute the number of blocks 28 | // needed in Javascript and dispatch that amount. 29 | 30 | var tile : array, 4>; 31 | 32 | @compute @workgroup_size(32, 1, 1) 33 | fn main( 34 | @builtin(workgroup_id) WorkGroupID : vec3u, 35 | @builtin(local_invocation_id) LocalInvocationID : vec3u 36 | ) { 37 | let filterOffset = (params.filterDim - 1) / 2; 38 | let dims = vec2i(textureDimensions(inputTex, 0)); 39 | let baseIndex = vec2i(WorkGroupID.xy * vec2(params.blockDim, 4) + 40 | LocalInvocationID.xy * vec2(4, 1)) 41 | - vec2(filterOffset, 0); 42 | 43 | for (var r = 0; r < 4; r++) { 44 | for (var c = 0; c < 4; c++) { 45 | var loadIndex = baseIndex + vec2(c, r); 46 | if (flip.value != 0u) { 47 | loadIndex = loadIndex.yx; 48 | } 49 | 50 | tile[r][4 * LocalInvocationID.x + u32(c)] = textureSampleLevel( 51 | inputTex, 52 | samp, 53 | (vec2f(loadIndex) + vec2f(0.25, 0.25)) / vec2f(dims), 54 | 0.0 55 | ).rgb; 56 | } 57 | } 58 | 59 | workgroupBarrier(); 60 | 61 | for (var r = 0; r < 4; r++) { 62 | for (var c = 0; c < 4; c++) { 63 | var writeIndex = baseIndex + vec2(c, r); 64 | if (flip.value != 0) { 65 | writeIndex = writeIndex.yx; 66 | } 67 | 68 | let center = i32(4 * LocalInvocationID.x) + c; 69 | if (center >= filterOffset && 70 | center < 128 - filterOffset && 71 | all(writeIndex < dims)) { 72 | var acc = vec3(0.0, 0.0, 0.0); 73 | for (var f = 0; f < params.filterDim; f++) { 74 | var i = center + f - filterOffset; 75 | acc = acc + (1.0 / f32(params.filterDim)) * tile[r][i]; 76 | } 77 | textureStore(outputTex, writeIndex, vec4(acc, 1.0)); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/common.wgsl: -------------------------------------------------------------------------------- 1 | const pi = 3.14159265359; 2 | 3 | // Quad describes 2D rectangle on a plane 4 | struct Quad { 5 | // The surface plane 6 | plane : vec4f, 7 | // A plane with a normal in the 'u' direction, intersecting the origin, at 8 | // right-angles to the surface plane. 9 | // The dot product of 'right' with a 'vec4(pos, 1)' will range between [-1..1] 10 | // if the projected point is within the quad. 11 | right : vec4f, 12 | // A plane with a normal in the 'v' direction, intersecting the origin, at 13 | // right-angles to the surface plane. 14 | // The dot product of 'up' with a 'vec4(pos, 1)' will range between [-1..1] 15 | // if the projected point is within the quad. 16 | up : vec4f, 17 | // The diffuse color of the quad 18 | color : vec3f, 19 | // Emissive value. 0=no emissive, 1=full emissive. 20 | emissive : f32, 21 | }; 22 | 23 | // Ray is a start point and direction. 24 | struct Ray { 25 | start : vec3f, 26 | dir : vec3f, 27 | } 28 | 29 | // Value for HitInfo.quad if no intersection occurred. 30 | const kNoHit = 0xffffffff; 31 | 32 | // HitInfo describes the hit location of a ray-quad intersection 33 | struct HitInfo { 34 | // Distance along the ray to the intersection 35 | dist : f32, 36 | // The quad index that was hit 37 | quad : u32, 38 | // The position of the intersection 39 | pos : vec3f, 40 | // The UVs of the quad at the point of intersection 41 | uv : vec2f, 42 | } 43 | 44 | // CommonUniforms uniform buffer data 45 | struct CommonUniforms { 46 | // Model View Projection matrix 47 | mvp : mat4x4f, 48 | // Inverse of mvp 49 | inv_mvp : mat4x4f, 50 | // Random seed for the workgroup 51 | seed : vec3u, 52 | } 53 | 54 | // The common uniform buffer binding. 55 | @group(0) @binding(0) var common_uniforms : CommonUniforms; 56 | 57 | // The quad buffer binding. 58 | @group(0) @binding(1) var quads : array; 59 | 60 | // intersect_ray_quad will check to see if the ray 'r' intersects the quad 'q'. 61 | // If an intersection occurs, and the intersection is closer than 'closest' then 62 | // the intersection information is returned, otherwise 'closest' is returned. 63 | fn intersect_ray_quad(r : Ray, quad : u32, closest : HitInfo) -> HitInfo { 64 | let q = quads[quad]; 65 | let plane_dist = dot(q.plane, vec4(r.start, 1)); 66 | let ray_dist = plane_dist / -dot(q.plane.xyz, r.dir); 67 | let pos = r.start + r.dir * ray_dist; 68 | let uv = vec2(dot(vec4f(pos, 1), q.right), 69 | dot(vec4f(pos, 1), q.up)) * 0.5 + 0.5; 70 | let hit = plane_dist > 0 && 71 | ray_dist > 0 && 72 | ray_dist < closest.dist && 73 | all((uv > vec2f()) & (uv < vec2f(1))); 74 | return HitInfo( 75 | select(closest.dist, ray_dist, hit), 76 | select(closest.quad, quad, hit), 77 | select(closest.pos, pos, hit), 78 | select(closest.uv, uv, hit), 79 | ); 80 | } 81 | 82 | // raytrace finds the closest intersecting quad for the given ray 83 | fn raytrace(ray : Ray) -> HitInfo { 84 | var hit = HitInfo(); 85 | hit.dist = 1e20; 86 | hit.quad = kNoHit; 87 | for (var quad = 0u; quad < arrayLength(&quads); quad++) { 88 | hit = intersect_ray_quad(ray, quad, hit); 89 | } 90 | return hit; 91 | } 92 | 93 | // A pseudo random number. Initialized with init_rand(), updated with rand(). 94 | var rnd : vec3u; 95 | 96 | // Initializes the random number generator. 97 | fn init_rand(invocation_id : vec3u) { 98 | const A = vec3(1741651 * 1009, 99 | 140893 * 1609 * 13, 100 | 6521 * 983 * 7 * 2); 101 | rnd = (invocation_id * A) ^ common_uniforms.seed; 102 | } 103 | 104 | // Returns a random number between 0 and 1. 105 | fn rand() -> f32 { 106 | const C = vec3(60493 * 9377, 107 | 11279 * 2539 * 23, 108 | 7919 * 631 * 5 * 3); 109 | 110 | rnd = (rnd * C) ^ (rnd.yzx >> vec3(4u)); 111 | return f32(rnd.x ^ rnd.y) / f32(0xffffffff); 112 | } 113 | 114 | // Returns a random point within a unit sphere centered at (0,0,0). 115 | fn rand_unit_sphere() -> vec3f { 116 | var u = rand(); 117 | var v = rand(); 118 | var theta = u * 2.0 * pi; 119 | var phi = acos(2.0 * v - 1.0); 120 | var r = pow(rand(), 1.0/3.0); 121 | var sin_theta = sin(theta); 122 | var cos_theta = cos(theta); 123 | var sin_phi = sin(phi); 124 | var cos_phi = cos(phi); 125 | var x = r * sin_phi * sin_theta; 126 | var y = r * sin_phi * cos_theta; 127 | var z = r * cos_phi; 128 | return vec3f(x, y, z); 129 | } 130 | 131 | fn rand_concentric_disk() -> vec2f { 132 | let u = vec2f(rand(), rand()); 133 | let uOffset = 2.f * u - vec2f(1, 1); 134 | 135 | if (uOffset.x == 0 && uOffset.y == 0){ 136 | return vec2f(0, 0); 137 | } 138 | 139 | var theta = 0.0; 140 | var r = 0.0; 141 | if (abs(uOffset.x) > abs(uOffset.y)) { 142 | r = uOffset.x; 143 | theta = (pi / 4) * (uOffset.y / uOffset.x); 144 | } else { 145 | r = uOffset.y; 146 | theta = (pi / 2) - (pi / 4) * (uOffset.x / uOffset.y); 147 | } 148 | return r * vec2f(cos(theta), sin(theta)); 149 | } 150 | 151 | fn rand_cosine_weighted_hemisphere() -> vec3f { 152 | let d = rand_concentric_disk(); 153 | let z = sqrt(max(0.0, 1.0 - d.x * d.x - d.y * d.y)); 154 | return vec3f(d.x, d.y, z); 155 | } 156 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/cube.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelViewProjectionMatrix : mat4x4f, 3 | } 4 | 5 | @group(0) @binding(0) var uniforms : Uniforms; 6 | @group(0) @binding(1) var mySampler: sampler; 7 | @group(0) @binding(2) var myTexture: texture_2d; 8 | 9 | struct VertexOutput { 10 | @builtin(position) Position : vec4f, 11 | @location(0) fragUV : vec2f, 12 | } 13 | 14 | @vertex 15 | fn vertex_main( 16 | @location(0) position : vec4f, 17 | @location(1) uv : vec2f 18 | ) -> VertexOutput { 19 | return VertexOutput(uniforms.modelViewProjectionMatrix * position, uv); 20 | } 21 | 22 | @fragment 23 | fn fragment_main(@location(0) fragUV: vec2f) -> @location(0) vec4f { 24 | return textureSample(myTexture, mySampler, fragUV); 25 | } 26 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment 2 | fn main(@location(0) cell: f32) -> @location(0) vec4f { 3 | return vec4f(cell, cell, cell, 1.); 4 | } 5 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/fullscreenTexturedQuad.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var mySampler : sampler; 2 | @group(0) @binding(1) var myTexture : texture_2d; 3 | 4 | struct VertexOutput { 5 | @builtin(position) Position : vec4f, 6 | @location(0) fragUV : vec2f, 7 | } 8 | 9 | @vertex 10 | fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { 11 | const pos = array( 12 | vec2( 1.0, 1.0), 13 | vec2( 1.0, -1.0), 14 | vec2(-1.0, -1.0), 15 | vec2( 1.0, 1.0), 16 | vec2(-1.0, -1.0), 17 | vec2(-1.0, 1.0), 18 | ); 19 | 20 | const uv = array( 21 | vec2(1.0, 0.0), 22 | vec2(1.0, 1.0), 23 | vec2(0.0, 1.0), 24 | vec2(1.0, 0.0), 25 | vec2(0.0, 1.0), 26 | vec2(0.0, 0.0), 27 | ); 28 | 29 | var output : VertexOutput; 30 | output.Position = vec4(pos[VertexIndex], 0.0, 1.0); 31 | output.fragUV = uv[VertexIndex]; 32 | return output; 33 | } 34 | 35 | @fragment 36 | fn frag_main(@location(0) fragUV : vec2f) -> @location(0) vec4f { 37 | return textureSample(myTexture, mySampler, fragUV); 38 | } 39 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/gltf.wgsl: -------------------------------------------------------------------------------- 1 | // VertexInput is not part of webgpu-samples, was added here for WESL 2 | // the VertexInput struct is built here: 3 | // https://github.com/webgpu/webgpu-samples/blob/main/sample/skinnedMesh/glbUtils.ts#L338 4 | struct VertexInput { 5 | @location(0) position: vec3f, 6 | @location(1) normal: vec3f, 7 | @location(2) texcoord: vec2f, 8 | @location(3) joints: vec4u, 9 | @location(4) weights: vec4f, 10 | } 11 | 12 | // Whale.glb Vertex attributes 13 | // Read in VertexInput from attributes 14 | // f32x3 f32x3 f32x2 u8x4 f32x4 15 | struct VertexOutput { 16 | @builtin(position) Position: vec4f, 17 | @location(0) normal: vec3f, 18 | @location(1) joints: vec4f, 19 | @location(2) weights: vec4f, 20 | } 21 | 22 | struct CameraUniforms { 23 | proj_matrix: mat4x4f, 24 | view_matrix: mat4x4f, 25 | model_matrix: mat4x4f, 26 | } 27 | 28 | struct GeneralUniforms { 29 | render_mode: u32, 30 | skin_mode: u32, 31 | } 32 | 33 | struct NodeUniforms { 34 | world_matrix: mat4x4f, 35 | } 36 | 37 | @group(0) @binding(0) var camera_uniforms: CameraUniforms; 38 | @group(1) @binding(0) var general_uniforms: GeneralUniforms; 39 | @group(2) @binding(0) var node_uniforms: NodeUniforms; 40 | @group(3) @binding(0) var joint_matrices: array; 41 | @group(3) @binding(1) var inverse_bind_matrices: array; 42 | 43 | @vertex 44 | fn vertexMain(input: VertexInput) -> VertexOutput { 45 | var output: VertexOutput; 46 | // Compute joint_matrices * inverse_bind_matrices 47 | let joint0 = joint_matrices[input.joints[0]] * inverse_bind_matrices[input.joints[0]]; 48 | let joint1 = joint_matrices[input.joints[1]] * inverse_bind_matrices[input.joints[1]]; 49 | let joint2 = joint_matrices[input.joints[2]] * inverse_bind_matrices[input.joints[2]]; 50 | let joint3 = joint_matrices[input.joints[3]] * inverse_bind_matrices[input.joints[3]]; 51 | // Compute influence of joint based on weight 52 | let skin_matrix = 53 | joint0 * input.weights[0] + 54 | joint1 * input.weights[1] + 55 | joint2 * input.weights[2] + 56 | joint3 * input.weights[3]; 57 | // Position of the vertex relative to our world 58 | let world_position = vec4f(input.position.x, input.position.y, input.position.z, 1.0); 59 | // Vertex position with model rotation, skinning, and the mesh's node transformation applied. 60 | let skinned_position = camera_uniforms.model_matrix * skin_matrix * node_uniforms.world_matrix * world_position; 61 | // Vertex position with only the model rotation applied. 62 | let rotated_position = camera_uniforms.model_matrix * world_position; 63 | // Determine which position to used based on whether skinMode is turnd on or off. 64 | let transformed_position = select( 65 | rotated_position, 66 | skinned_position, 67 | general_uniforms.skin_mode == 0 68 | ); 69 | // Apply the camera and projection matrix transformations to our transformed position; 70 | output.Position = camera_uniforms.proj_matrix * camera_uniforms.view_matrix * transformed_position; 71 | output.normal = input.normal; 72 | // Convert u32 joint data to f32s to prevent flat interpolation error. 73 | output.joints = vec4f(f32(input.joints[0]), f32(input.joints[1]), f32(input.joints[2]), f32(input.joints[3])); 74 | output.weights = input.weights; 75 | return output; 76 | } 77 | 78 | @fragment 79 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { 80 | switch general_uniforms.render_mode { 81 | case 1: { 82 | return input.joints; 83 | } 84 | case 2: { 85 | return input.weights; 86 | } 87 | default: { 88 | return vec4f(input.normal, 1.0); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/grid.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexInput { 2 | @location(0) vert_pos: vec2f, 3 | @location(1) joints: vec4u, 4 | @location(2) weights: vec4f 5 | } 6 | 7 | struct VertexOutput { 8 | @builtin(position) Position: vec4f, 9 | @location(0) world_pos: vec3f, 10 | @location(1) joints: vec4f, 11 | @location(2) weights: vec4f, 12 | } 13 | 14 | struct CameraUniforms { 15 | projMatrix: mat4x4f, 16 | viewMatrix: mat4x4f, 17 | modelMatrix: mat4x4f, 18 | } 19 | 20 | struct GeneralUniforms { 21 | render_mode: u32, 22 | skin_mode: u32, 23 | } 24 | 25 | @group(0) @binding(0) var camera_uniforms: CameraUniforms; 26 | @group(1) @binding(0) var general_uniforms: GeneralUniforms; 27 | @group(2) @binding(0) var joint_matrices: array; 28 | @group(2) @binding(1) var inverse_bind_matrices: array; 29 | 30 | @vertex 31 | fn vertexMain(input: VertexInput) -> VertexOutput { 32 | var output: VertexOutput; 33 | var bones = vec4f(0.0, 0.0, 0.0, 0.0); 34 | let position = vec4f(input.vert_pos.x, input.vert_pos.y, 0.0, 1.0); 35 | // Get relevant 4 bone matrices 36 | let joint0 = joint_matrices[input.joints[0]] * inverse_bind_matrices[input.joints[0]]; 37 | let joint1 = joint_matrices[input.joints[1]] * inverse_bind_matrices[input.joints[1]]; 38 | let joint2 = joint_matrices[input.joints[2]] * inverse_bind_matrices[input.joints[2]]; 39 | let joint3 = joint_matrices[input.joints[3]] * inverse_bind_matrices[input.joints[3]]; 40 | // Compute influence of joint based on weight 41 | let skin_matrix = 42 | joint0 * input.weights[0] + 43 | joint1 * input.weights[1] + 44 | joint2 * input.weights[2] + 45 | joint3 * input.weights[3]; 46 | // Bone transformed mesh 47 | output.Position = select( 48 | camera_uniforms.projMatrix * camera_uniforms.viewMatrix * camera_uniforms.modelMatrix * position, 49 | camera_uniforms.projMatrix * camera_uniforms.viewMatrix * camera_uniforms.modelMatrix * skin_matrix * position, 50 | general_uniforms.skin_mode == 0 51 | ); 52 | 53 | //Get unadjusted world coordinates 54 | output.world_pos = position.xyz; 55 | output.joints = vec4f(f32(input.joints.x), f32(input.joints.y), f32(input.joints.z), f32(input.joints.w)); 56 | output.weights = input.weights; 57 | return output; 58 | } 59 | 60 | 61 | @fragment 62 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { 63 | switch general_uniforms.render_mode { 64 | case 1: { 65 | return input.joints; 66 | } 67 | case 2: { 68 | return input.weights; 69 | } 70 | default: { 71 | return vec4f(255.0, 0.0, 1.0, 1.0); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/mesh.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | viewProjectionMatrix : mat4x4f 3 | } 4 | @group(0) @binding(0) var uniforms : Uniforms; 5 | 6 | @group(1) @binding(0) var modelMatrix : mat4x4f; 7 | 8 | struct VertexInput { 9 | @location(0) position : vec4f, 10 | @location(1) normal : vec3f, 11 | @location(2) uv : vec2f 12 | } 13 | 14 | struct VertexOutput { 15 | @builtin(position) position : vec4f, 16 | @location(0) normal: vec3f, 17 | @location(1) uv : vec2f, 18 | } 19 | 20 | @vertex 21 | fn vertexMain(input: VertexInput) -> VertexOutput { 22 | var output : VertexOutput; 23 | output.position = uniforms.viewProjectionMatrix * modelMatrix * input.position; 24 | output.normal = normalize((modelMatrix * vec4(input.normal, 0)).xyz); 25 | output.uv = input.uv; 26 | return output; 27 | } 28 | 29 | @group(1) @binding(1) var meshSampler: sampler; 30 | @group(1) @binding(2) var meshTexture: texture_2d; 31 | 32 | // Static directional lighting 33 | const lightDir = vec3f(1, 1, 1); 34 | const dirColor = vec3(1); 35 | const ambientColor = vec3f(0.05); 36 | 37 | @fragment 38 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { 39 | let textureColor = textureSample(meshTexture, meshSampler, input.uv); 40 | 41 | // Very simplified lighting algorithm. 42 | let lightColor = saturate(ambientColor + max(dot(input.normal, lightDir), 0.0) * dirColor); 43 | 44 | return vec4f(textureColor.rgb * lightColor, textureColor.a); 45 | } 46 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/opaque.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelViewProjectionMatrix: mat4x4f, 3 | }; 4 | 5 | @binding(0) @group(0) var uniforms: Uniforms; 6 | 7 | struct VertexOutput { 8 | @builtin(position) position: vec4f, 9 | @location(0) @interpolate(flat) instance: u32 10 | }; 11 | 12 | @vertex 13 | fn main_vs(@location(0) position: vec4f, @builtin(instance_index) instance: u32) -> VertexOutput { 14 | var output: VertexOutput; 15 | 16 | // distribute instances into a staggered 4x4 grid 17 | const gridWidth = 125.0; 18 | const cellSize = gridWidth / 4.0; 19 | let row = instance / 2u; 20 | let col = instance % 2u; 21 | 22 | let xOffset = -gridWidth / 2.0 + cellSize / 2.0 + 2.0 * cellSize * f32(col) + f32(row % 2u != 0u) * cellSize; 23 | let zOffset = -gridWidth / 2.0 + cellSize / 2.0 + 2.0 + f32(row) * cellSize; 24 | 25 | let offsetPos = vec4(position.x + xOffset, position.y, position.z + zOffset, position.w); 26 | 27 | output.position = uniforms.modelViewProjectionMatrix * offsetPos; 28 | output.instance = instance; 29 | return output; 30 | } 31 | 32 | @fragment 33 | fn main_fs(@location(0) @interpolate(flat) instance: u32) -> @location(0) vec4f { 34 | const colors = array( 35 | vec3(1.0, 0.0, 0.0), 36 | vec3(0.0, 1.0, 0.0), 37 | vec3(0.0, 0.0, 1.0), 38 | vec3(1.0, 0.0, 1.0), 39 | vec3(1.0, 1.0, 0.0), 40 | vec3(0.0, 1.0, 1.0), 41 | ); 42 | 43 | return vec4(colors[instance % 6u], 1.0); 44 | } 45 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/red.frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment 2 | fn main() -> @location(0) vec4f { 3 | return vec4(1.0, 0.0, 0.0, 1.0); 4 | } -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/sampleExternalTexture.frag.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(1) var mySampler: sampler; 2 | @group(0) @binding(2) var myTexture: texture_external; 3 | 4 | @fragment 5 | fn main(@location(0) fragUV : vec2f) -> @location(0) vec4f { 6 | return textureSampleBaseClampToEdge(myTexture, mySampler, fragUV); 7 | } 8 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/sprite.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position : vec4f, 3 | @location(4) color : vec4f, 4 | } 5 | 6 | @vertex 7 | fn vert_main( 8 | @location(0) a_particlePos : vec2f, 9 | @location(1) a_particleVel : vec2f, 10 | @location(2) a_pos : vec2f 11 | ) -> VertexOutput { 12 | let angle = -atan2(a_particleVel.x, a_particleVel.y); 13 | let pos = vec2( 14 | (a_pos.x * cos(angle)) - (a_pos.y * sin(angle)), 15 | (a_pos.x * sin(angle)) + (a_pos.y * cos(angle)) 16 | ); 17 | 18 | var output : VertexOutput; 19 | output.position = vec4(pos + a_particlePos, 0.0, 1.0); 20 | output.color = vec4( 21 | 1.0 - sin(angle + 1.0) - a_particleVel.y, 22 | pos.x * 100.0 - a_particleVel.y + 0.1, 23 | a_particleVel.x + cos(angle + 0.5), 24 | 1.0); 25 | return output; 26 | } 27 | 28 | @fragment 29 | fn frag_main(@location(4) color : vec4f) -> @location(0) vec4f { 30 | return color; 31 | } 32 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/triangle.vert.wgsl: -------------------------------------------------------------------------------- 1 | @vertex 2 | fn main( 3 | @builtin(vertex_index) VertexIndex : u32 4 | ) -> @builtin(position) vec4f { 5 | var pos = array( 6 | vec2(0.0, 0.5), 7 | vec2(-0.5, -0.5), 8 | vec2(0.5, -0.5) 9 | ); 10 | 11 | return vec4f(pos[VertexIndex], 0.0, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/vert.wgsl: -------------------------------------------------------------------------------- 1 | struct Out { 2 | @builtin(position) pos: vec4f, 3 | @location(0) cell: f32, 4 | } 5 | 6 | @binding(0) @group(0) var size: vec2u; 7 | 8 | @vertex 9 | fn main(@builtin(instance_index) i: u32, @location(0) cell: u32, @location(1) pos: vec2u) -> Out { 10 | let w = size.x; 11 | let h = size.y; 12 | let x = (f32(i % w + pos.x) / f32(w) - 0.5) * 2. * f32(w) / f32(max(w, h)); 13 | let y = (f32((i - (i % w)) / w + pos.y) / f32(h) - 0.5) * 2. * f32(h) / f32(max(w, h)); 14 | 15 | return Out(vec4f(x, y, 0., 1.), f32(cell)); 16 | } 17 | -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/vertex.wgsl: -------------------------------------------------------------------------------- 1 | struct Uniforms { 2 | modelMatrix : array, 3 | } 4 | struct Camera { 5 | viewProjectionMatrix : mat4x4f, 6 | } 7 | 8 | @binding(0) @group(0) var uniforms : Uniforms; 9 | @binding(1) @group(0) var camera : Camera; 10 | 11 | struct VertexOutput { 12 | @builtin(position) Position : vec4f, 13 | @location(0) fragColor : vec4f, 14 | } 15 | 16 | @vertex 17 | fn main( 18 | @builtin(instance_index) instanceIdx : u32, 19 | @location(0) position : vec4f, 20 | @location(1) color : vec4f 21 | ) -> VertexOutput { 22 | var output : VertexOutput; 23 | output.Position = camera.viewProjectionMatrix * uniforms.modelMatrix[instanceIdx] * position; 24 | output.fragColor = color; 25 | return output; 26 | } -------------------------------------------------------------------------------- /crates/wesl-test/webgpu-samples/vertexPositionColor.frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment 2 | fn main( 3 | @location(0) fragUV: vec2f, 4 | @location(1) fragPosition: vec4f 5 | ) -> @location(0) vec4f { 6 | return fragPosition; 7 | } 8 | -------------------------------------------------------------------------------- /crates/wesl-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wesl-web" 3 | description = "WESL compiler in WebAssembly" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | rust-version.workspace = true 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [features] 15 | default = ["ansi-to-html", "naga"] 16 | debug = ["console_log", "console_error_panic_hook"] 17 | 18 | [dependencies] 19 | log = "0.4.22" 20 | serde = { version = "1.0.215", features = ["derive"] } 21 | serde-wasm-bindgen = "0.6.5" 22 | serde_bytes = "0.11.15" 23 | tsify-next = { version = "0.5.5", features = ["js"], default-features = false } 24 | ansi-to-html = { version = "0.2.2", optional = true } 25 | naga = { version = "25.0.1", features = [ 26 | "wgsl-in", 27 | "wgsl-out", 28 | ], optional = true } 29 | thiserror = "2.0.11" 30 | wasm-bindgen = "0.2.95" 31 | 32 | wesl = { workspace = true, features = ["eval", "serde"] } 33 | 34 | # The `console_error_panic_hook` crate provides better debugging of panics by 35 | # logging them with `console.error`. This is great for development, but requires 36 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 37 | # code size when deploying. 38 | console_error_panic_hook = { version = "0.1.7", optional = true } 39 | console_log = { version = "1.0.0", features = ["color"], optional = true } 40 | 41 | [lints] 42 | workspace = true 43 | -------------------------------------------------------------------------------- /crates/wesl-web/README.md: -------------------------------------------------------------------------------- 1 | # wesl-web 2 | 3 | This is a `wasm-pack` shim to the [`wesl-rs`][wesl-rs] compiler. 4 | It is used in [`wesl-playground`][wesl-playground]. 5 | 6 | It generates typescript bindings. Take inspiration from [`wesl-playground`][wesl-playground] for 7 | how to use. 8 | 9 | ## Building 10 | 11 | You need [`wasm-pack`][wasm-pack] installed. 12 | 13 | * release `wasm-pack build . --release --target web --out-dir path/to/out/wesl-web` 14 | * development `wasm-pack build . --dev --target web --out-dir path/to/out/wesl-web --features debug` 15 | That's for `wesl-playground`. you can switch the `--target` to `node` or `deno` depending on your 16 | use-case. Read the [`wasm-pack` book][wasm-pack-book] for more. 17 | 18 | [wesl-rs]: https://github.com/wgsl-tooling-wg/wesl-rs 19 | [wesl-playground]: https://github.com/wgsl-tooling-wg/wesl-playground 20 | [wasm-pack]: https://rustwasm.github.io/wasm-pack/ 21 | [wasm-pack-book]: https://rustwasm.github.io/docs/wasm-pack/ 22 | -------------------------------------------------------------------------------- /crates/wesl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wesl" 3 | description = "The WESL rust compiler" 4 | documentation = "https://docs.rs/wesl" 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | rust-version.workspace = true 11 | 12 | [dependencies] 13 | annotate-snippets = "0.11.5" 14 | derive_more = { version = "2.0.1", features = [ 15 | "as_ref", 16 | "deref", 17 | "deref_mut", 18 | "display", 19 | "from", 20 | "unwrap", 21 | ] } 22 | half = { version = "2.4.1", features = ["num-traits"] } 23 | itertools = "0.14.0" 24 | num-traits = "0.2.19" 25 | serde = { version = "1.0.215", features = ["derive"], optional = true } 26 | thiserror = "2.0.11" 27 | wgsl-parse = { workspace = true, features = ["wesl"] } 28 | wesl-macros = { workspace = true } 29 | # deps for feature 'package' 30 | proc-macro2 = { version = "1.0.93", optional = true } 31 | quote = { version = "1.0.38", optional = true } 32 | 33 | [features] 34 | eval = [] 35 | generics = ["wgsl-parse/generics"] 36 | serde = ["wgsl-parse/serde", "dep:serde"] 37 | package = ["dep:proc-macro2", "dep:quote"] 38 | naga_ext = ["wgsl-parse/naga_ext"] 39 | 40 | [package.metadata.docs.rs] 41 | all-features = true 42 | rustdoc-args = ["--cfg", "docsrs"] 43 | 44 | [lints] 45 | workspace = true 46 | -------------------------------------------------------------------------------- /crates/wesl/README.md: -------------------------------------------------------------------------------- 1 | # WESL: A Community Standard for Enhanced WGSL 2 | 3 | This is the crate for all your [WESL][wesl] needs. 4 | 5 | See also the [standalone CLI][cli]. 6 | 7 | ## Basic Usage 8 | 9 | See [`Wesl`] for an overview of the high-level API. 10 | 11 | ```rust 12 | # use wesl::{Wesl, VirtualResolver}; 13 | let compiler = Wesl::new("src/shaders"); 14 | # // just adding a virtual file here so the doctest runs without a filesystem 15 | # let mut resolver = VirtualResolver::new(); 16 | # resolver.add_module("main", "fn my_fn() {}".into()); 17 | # let compiler = compiler.set_custom_resolver(resolver); 18 | 19 | // compile a WESL file to a WGSL string 20 | let wgsl_str = compiler 21 | .compile("main.wesl") 22 | .inspect_err(|e| eprintln!("WESL error: {e}")) // pretty errors with `display()` 23 | .unwrap() 24 | .to_string(); 25 | ``` 26 | 27 | ## Usage in [`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html) 28 | 29 | In your Rust project you probably want to have your WESL code converted automatically 30 | to a WGSL string at build-time, unless your WGSL code must be assembled at runtime. 31 | 32 | Add this crate to your build dependencies in `Cargo.toml`: 33 | 34 | ```toml 35 | [build-dependencies] 36 | wesl = "0.1" 37 | ``` 38 | 39 | Create the `build.rs` file with the following content: 40 | 41 | ```rust,ignore 42 | # use wesl::{Wesl, FileResolver}; 43 | fn main() { 44 | Wesl::new("src/shaders") 45 | .build_artifact("main.wesl", "my_shader"); 46 | } 47 | ``` 48 | 49 | Include the compiled WGSL string in your code: 50 | 51 | ```rust,ignore 52 | let module = device.create_shader_module(ShaderModuleDescriptor { 53 | label: Some("my_shader"), 54 | source: ShaderSource::Wgsl(include_wesl!("my_shader")), 55 | }); 56 | ``` 57 | 58 | ## Write shaders inline with the [`quote_module`] macro 59 | 60 | With the `quote_*!` macros one can write WGSL shaders directly in source code. This has a 61 | few advantages: 62 | 63 | * Like [`quote`](https://docs.rs/quote), it supports local variable injection. This can be 64 | used e.g. to customize a shader module at runtime. 65 | * The module is parsed at build-time, and syntax errors will be reported at the right 66 | location in the macro invocation. 67 | * WGSL and Rust have a similar syntax. Your Rust syntax highlighter will also highlight 68 | injected WGSL code. 69 | 70 | ```rust 71 | use wesl::syntax::*; // this is necessary for the quote_module macro 72 | 73 | // this i64 value is computed at runtime and injected into the shader. 74 | let num_iterations = 8i64; 75 | 76 | // the following variable has type `TranslationUnit`. 77 | let wgsl = wesl::quote_module! { 78 | @fragment 79 | fn fs_main(@location(0) in_color: vec4) -> @location(0) vec4 { 80 | for (let i = 0; i < #num_iterations; i++) { 81 | // ... 82 | } 83 | return in_color; 84 | } 85 | }; 86 | ``` 87 | 88 | One can inject variables into the following places by prefixing the name with 89 | a `#` symbol: 90 | 91 | | Code location | Injected type | Indirectly supported injection type with `Into` | 92 | |---------------|---------------|-------------------------------------------------| 93 | | name of a global declaration | `GlobalDeclaration` | `Declaration` `TypeAlias` `Struct` `Function` `ConstAssert` | 94 | | name of a struct member | `StructMember` | | 95 | | name of an attribute, after `@` | `Attribute` | `BuiltinValue` `InterpolateAttribute` `WorkgroupSizeAttribute` `TypeConstraint` `CustomAttribute` | 96 | | type or identifier expression | `Expression` | `LiteralExpression` `ParenthesizedExpression` `NamedComponentExpression` `IndexingExpression` `UnaryExpression` `BinaryExpression` `FunctionCallExpression` `TypeOrIdentifierExpression` and transitively: `bool` `i64` (AbstractInt) `f64` (AbstractFloat) `i32` `u32` `f32` `Ident` | 97 | | name of an attribute preceding and empty block statement | `Statement` | `CompoundStatement` `AssignmentStatement` `IncrementStatement` `DecrementStatement` `IfStatement` `SwitchStatement` `LoopStatement` `ForStatement` `WhileStatement` `BreakStatement` `ContinueStatement` `ReturnStatement` `DiscardStatement` `FunctionCallStatement` `ConstAssertStatement` `DeclarationStatement` | 98 | 99 | ```rust 100 | use wesl::syntax::*; // this is necessary for the quote_module macro 101 | 102 | let inject_struct = Struct::new(Ident::new("mystruct".to_string())); 103 | let inject_func = Function::new(Ident::new("myfunc".to_string())); 104 | let inject_stmt = Statement::Void; 105 | let inject_expr = 1f32; 106 | let wgsl = wesl::quote_module! { 107 | struct #inject_struct { dummy: u32 } // structs cannot be empty 108 | fn #inject_func() {} 109 | fn foo() { 110 | @#inject_stmt {} 111 | let x: f32 = #inject_expr; 112 | } 113 | }; 114 | ``` 115 | 116 | ## Evaluating const-expressions 117 | 118 | This is an advanced and experimental feature. `wesl-rs` supports evaluation and execution 119 | of WESL code with the `eval` feature flag. Early evaluation (in particular of 120 | const-expressions) helps developers to catch bugs early by improving the validation and 121 | error reporting capabilities of WESL. Full evaluation of const-expressions can be enabled 122 | with the `lower` compiler option. 123 | 124 | Additionally, the `eval` feature adds support for user-defined `@const` attributes on 125 | functions, which allows one to precompute data ahead of time, and ensure that code has no 126 | runtime dependencies. 127 | 128 | The eval/exec implementation is tested with the [WebGPU Conformance Test Suite][cts]. 129 | 130 | ```rust 131 | # use wesl::{Wesl, VirtualResolver, eval_str}; 132 | // ...standalone expression 133 | let wgsl_expr = eval_str("abs(3 - 5)").unwrap().to_string(); 134 | assert_eq!(wgsl_expr, "2"); 135 | 136 | // ...expression using declarations in a WESL file 137 | let source = "const my_const = 4; @const fn my_fn(v: u32) -> u32 { return v * 10; }"; 138 | # let mut resolver = VirtualResolver::new(); 139 | # resolver.add_module("source", source.into()); 140 | # let compiler = Wesl::new_barebones().set_custom_resolver(resolver); 141 | let wgsl_expr = compiler 142 | .compile("source").unwrap() 143 | .eval("my_fn(my_const) + 2").unwrap() 144 | .to_string(); 145 | assert_eq!(wgsl_expr, "42u"); 146 | ``` 147 | 148 | ## Features 149 | 150 | | name | description | Status/Specification | 151 | |------------|-------------------------------------------------------|---------------------------| 152 | | `generics` | user-defined type-generators and generic functions | [experimental][generics] | 153 | | `package` | create shader libraries published to `crates.io` | [experimental][packaging] | 154 | | `eval` | execute shader code on the CPU and `@const` attribute | experimental | 155 | | `naga_ext` | enable all Naga/WGPU extensions | experimental | 156 | | `serde` | derive `Serialize` and `Deserialize` for syntax nodes | | 157 | 158 | [wesl]: https://wesl-lang.dev 159 | [cli]: https://crates.io/crates/wesl-cli 160 | [generics]: https://github.com/k2d222/wesl-spec/blob/generics/Generics.md 161 | [packaging]: https://github.com/wgsl-tooling-wg/wesl-spec/blob/main/Packaging.md 162 | [cts]: https://github.com/k2d222/wesl-cts 163 | -------------------------------------------------------------------------------- /crates/wesl/src/eval/attrs.rs: -------------------------------------------------------------------------------- 1 | use wgsl_parse::{ 2 | Decorated, 3 | syntax::{Attribute, AttributeNode, BuiltinValue, Expression}, 4 | }; 5 | 6 | use super::{Context, Eval, EvalError, EvalStage, Instance, LiteralInstance, Ty, Type, with_stage}; 7 | 8 | type E = EvalError; 9 | 10 | pub trait EvalAttrs: Decorated { 11 | fn attr_align(&self, ctx: &mut Context) -> Result, E> { 12 | attr_align(self.attributes(), ctx).transpose() 13 | } 14 | fn attr_group_binding(&self, ctx: &mut Context) -> Result<(u32, u32), E> { 15 | attr_group_binding(self.attributes(), ctx) 16 | } 17 | fn attr_size(&self, ctx: &mut Context) -> Result, E> { 18 | attr_size(self.attributes(), ctx).transpose() 19 | } 20 | fn attr_id(&self, ctx: &mut Context) -> Result, E> { 21 | attr_id(self.attributes(), ctx).transpose() 22 | } 23 | fn attr_location(&self, ctx: &mut Context) -> Result, E> { 24 | attr_location(self.attributes(), ctx).transpose() 25 | } 26 | fn attr_workgroup_size(&self, ctx: &mut Context) -> Result<(u32, Option, Option), E> { 27 | attr_workgroup_size(self.attributes(), ctx) 28 | } 29 | fn attr_blend_src(&self, ctx: &mut Context) -> Result, E> { 30 | attr_blend_src(self.attributes(), ctx).transpose() 31 | } 32 | fn attr_builtin(&self) -> Option { 33 | self.attributes().iter().find_map(|attr| match attr.node() { 34 | Attribute::Builtin(attr) => Some(*attr), 35 | _ => None, 36 | }) 37 | } 38 | } 39 | 40 | impl EvalAttrs for T {} 41 | fn eval_positive_integer(expr: &Expression, ctx: &mut Context) -> Result { 42 | let expr = with_stage!(ctx, EvalStage::Const, { expr.eval_value(ctx) })?; 43 | let expr = match expr { 44 | Instance::Literal(g) => match g { 45 | LiteralInstance::AbstractInt(g) => Ok(g), 46 | LiteralInstance::I32(g) => Ok(g as i64), 47 | LiteralInstance::U32(g) => Ok(g as i64), 48 | _ => Err(E::Type(Type::U32, g.ty())), 49 | }, 50 | _ => Err(E::Type(Type::U32, expr.ty())), 51 | }?; 52 | if expr < 0 { 53 | Err(E::NegativeAttr(expr)) 54 | } else { 55 | Ok(expr as u32) 56 | } 57 | } 58 | 59 | fn attr_group_binding(attrs: &[AttributeNode], ctx: &mut Context) -> Result<(u32, u32), E> { 60 | let group = attrs.iter().find_map(|attr| match attr.node() { 61 | Attribute::Group(g) => Some(g), 62 | _ => None, 63 | }); 64 | let binding = attrs.iter().find_map(|attr| match attr.node() { 65 | Attribute::Binding(b) => Some(b), 66 | _ => None, 67 | }); 68 | 69 | let (group, binding) = match (group, binding) { 70 | (Some(g), Some(b)) => Ok(( 71 | eval_positive_integer(g, ctx)?, 72 | eval_positive_integer(b, ctx)?, 73 | )), 74 | _ => Err(E::MissingBindAttr), 75 | }?; 76 | Ok((group, binding)) 77 | } 78 | 79 | fn attr_size(attrs: &[AttributeNode], ctx: &mut Context) -> Option> { 80 | let expr = attrs.iter().find_map(|attr| match attr.node() { 81 | Attribute::Size(e) => Some(e), 82 | _ => None, 83 | })?; 84 | 85 | Some(eval_positive_integer(expr, ctx)) 86 | } 87 | 88 | fn attr_align(attrs: &[AttributeNode], ctx: &mut Context) -> Option> { 89 | let expr = attrs.iter().find_map(|attr| match attr.node() { 90 | Attribute::Align(e) => Some(e), 91 | _ => None, 92 | })?; 93 | 94 | Some(eval_positive_integer(expr, ctx)) 95 | } 96 | 97 | fn attr_id(attrs: &[AttributeNode], ctx: &mut Context) -> Option> { 98 | let expr = attrs.iter().find_map(|attr| match attr.node() { 99 | Attribute::Id(e) => Some(e), 100 | _ => None, 101 | })?; 102 | 103 | Some(eval_positive_integer(expr, ctx)) 104 | } 105 | 106 | fn attr_location(attrs: &[AttributeNode], ctx: &mut Context) -> Option> { 107 | let expr = attrs.iter().find_map(|attr| match attr.node() { 108 | Attribute::Location(e) => Some(e), 109 | _ => None, 110 | })?; 111 | 112 | Some(eval_positive_integer(expr, ctx)) 113 | } 114 | 115 | fn attr_workgroup_size( 116 | attrs: &[AttributeNode], 117 | ctx: &mut Context, 118 | ) -> Result<(u32, Option, Option), E> { 119 | let attr = attrs 120 | .iter() 121 | .find_map(|attr| match attr.node() { 122 | Attribute::WorkgroupSize(attr) => Some(attr), 123 | _ => None, 124 | }) 125 | .ok_or(E::MissingWorkgroupSize)?; 126 | 127 | let x = eval_positive_integer(&attr.x, ctx)?; 128 | let y = attr 129 | .y 130 | .as_ref() 131 | .map(|y| eval_positive_integer(y, ctx)) 132 | .transpose()?; 133 | let z = attr 134 | .z 135 | .as_ref() 136 | .map(|z| eval_positive_integer(z, ctx)) 137 | .transpose()?; 138 | Ok((x, y, z)) 139 | } 140 | 141 | fn attr_blend_src(attrs: &[AttributeNode], ctx: &mut Context) -> Option> { 142 | let expr = attrs.iter().find_map(|attr| match attr.node() { 143 | Attribute::BlendSrc(attr) => Some(attr), 144 | _ => None, 145 | })?; 146 | Some(eval_positive_integer(expr, ctx).and_then(|val| match val { 147 | 0 => Ok(false), 148 | 1 => Ok(true), 149 | _ => Err(E::InvalidBlendSrc(val)), 150 | })) 151 | } 152 | -------------------------------------------------------------------------------- /crates/wesl/src/eval/error.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use thiserror::Error; 3 | use wgsl_parse::syntax::*; 4 | 5 | use super::{EvalStage, Flow, Instance, LiteralInstance, MemView, ScopeKind, Type}; 6 | 7 | /// Evaluation and Execution errors. 8 | #[derive(Clone, Debug, Error)] 9 | pub enum EvalError { 10 | #[error("not implemented: `{0}`")] 11 | Todo(String), 12 | 13 | // types & templates 14 | #[error("expected a scalar type, got `{0}`")] 15 | NotScalar(Type), 16 | #[error("`{0}` is not constructible")] 17 | NotConstructible(Type), 18 | #[error("expected type `{0}`, got `{1}`")] 19 | Type(Type, Type), 20 | #[error("expected a type, got declaration `{0}`")] 21 | NotType(String), 22 | #[error("unknown type `{0}`")] 23 | UnknownType(String), 24 | #[error("unknown struct `{0}`")] 25 | UnknownStruct(String), 26 | #[error("declaration `{0}` is not accessible at {stage} time", stage = match .1 { 27 | EvalStage::Const => "shader-module-creation", 28 | EvalStage::Override => "pipeline-creation", 29 | EvalStage::Exec => "shader-execution" 30 | })] 31 | NotAccessible(String, EvalStage), 32 | #[error("type `{0}` does not take any template arguments")] 33 | UnexpectedTemplate(String), 34 | #[error("missing template arguments for type `{0}`")] 35 | MissingTemplate(&'static str), 36 | 37 | // references 38 | #[error("invalid reference to memory view `{0}{1}`")] 39 | View(Type, MemView), 40 | #[error("invalid reference to `{0}`, expected reference to `{1}`")] 41 | RefType(Type, Type), 42 | #[error("cannot write a `{0}` to a reference to `{1}`")] 43 | WriteRefType(Type, Type), 44 | #[error("attempt to write to a read-only reference")] 45 | NotWrite, 46 | #[error("attempt to read a write-only reference")] 47 | NotRead, 48 | #[error("reference is not read-write")] 49 | NotReadWrite, 50 | 51 | // conversions 52 | #[error("cannot convert from `{0}` to `{1}`")] 53 | Conversion(Type, Type), 54 | #[error("overflow while converting `{0}` to `{1}`")] 55 | ConvOverflow(LiteralInstance, Type), 56 | 57 | // indexing 58 | #[error("`{0}` has no component `{1}`")] 59 | Component(Type, String), 60 | #[error("invalid array index type `{0}`")] 61 | Index(Type), 62 | #[error("`{0}` cannot be indexed")] 63 | NotIndexable(Type), 64 | #[error("invalid vector component or swizzle `{0}`")] 65 | Swizzle(String), 66 | #[error("index `{0}` is out-of-bounds for `{1}` of `{2}` components")] 67 | OutOfBounds(usize, Type, usize), 68 | 69 | // arithmetic 70 | #[error("cannot use unary operator `{0}` on type `{1}`")] 71 | Unary(UnaryOperator, Type), 72 | #[error("cannot use binary operator `{0}` with operands `{1}` and `{2}`")] 73 | Binary(BinaryOperator, Type, Type), 74 | #[error("cannot apply component-wise binary operation on operands `{0}` and `{1}`")] 75 | CompwiseBinary(Type, Type), 76 | #[error("attempt to negate with overflow")] 77 | NegOverflow, 78 | #[error("attempt to add with overflow")] 79 | AddOverflow, 80 | #[error("attempt to subtract with overflow")] 81 | SubOverflow, 82 | #[error("attempt to multiply with overflow")] 83 | MulOverflow, 84 | #[error("attempt to divide by zero")] 85 | DivByZero, 86 | #[error("attempt to calculate the remainder with a divisor of zero")] 87 | RemZeroDiv, 88 | #[error("attempt to shift left by `{0}`, which would overflow `{1}`")] 89 | ShlOverflow(u32, LiteralInstance), 90 | #[error("attempt to shift right by `{0}`, which would overflow `{1}`")] 91 | ShrOverflow(u32, LiteralInstance), 92 | 93 | // functions 94 | #[error("unknown function `{0}`")] 95 | UnknownFunction(String), 96 | #[error("declaration `{0}` is not callable")] 97 | NotCallable(String), 98 | #[error("invalid function call signature: `{0}({args})`", args = (.1).iter().format(", "))] 99 | Signature(TypeExpression, Vec), 100 | #[error("{0}")] 101 | Builtin(&'static str), 102 | #[error("invalid template arguments to `{0}`")] 103 | TemplateArgs(&'static str), 104 | #[error("incorrect number of arguments to `{0}`, expected `{1}`, got `{2}`")] 105 | ParamCount(String, usize, usize), 106 | #[error("invalid parameter type, expected `{0}`, got `{1}`")] 107 | ParamType(Type, Type), 108 | #[error("returned `{0}` from function `{1}` that returns `{2}`")] 109 | ReturnType(Type, String, Type), 110 | #[error("call to function `{0}` did not return any value, expected `{1}`")] 111 | NoReturn(String, Type), 112 | #[error("function `{0}` has no return type, but it returns `{1}`")] 113 | UnexpectedReturn(String, Type), 114 | #[error("calling non-const function `{0}` in const context")] 115 | NotConst(String), 116 | #[error("expected a value, but function `{0}` has no return type")] 117 | Void(String), 118 | #[error("function `{0}` has the `@must_use` attribute, its return value must be used")] 119 | MustUse(String), 120 | #[error("function `{0}` is not an entrypoint")] 121 | NotEntrypoint(String), 122 | #[error("entry point function parameter `{0}` must have a @builtin or @location attribute")] 123 | InvalidEntrypointParam(String), 124 | #[error("missing builtin input `{0}` bound to parameter `{0}`")] 125 | MissingBuiltinInput(BuiltinValue, String), 126 | #[error("missing user-defined input bound to parameter `{0}` at location `{1}`")] 127 | MissingUserInput(String, u32), 128 | 129 | // declarations 130 | #[error("unknown declaration `{0}`")] 131 | UnknownDecl(String), 132 | #[error("override-declarations are not permitted in const contexts")] 133 | OverrideInConst, 134 | #[error("override-declarations are not permitted in function bodies")] 135 | OverrideInFn, 136 | #[error("let-declarations are not permitted at the module scope")] 137 | LetInMod, 138 | #[error("uninitialized const-declaration `{0}`")] 139 | UninitConst(String), 140 | #[error("uninitialized let-declaration `{0}`")] 141 | UninitLet(String), 142 | #[error("uninitialized override-declaration `{0}` with no override")] 143 | UninitOverride(String), 144 | #[error("initializer are not allowed in `{0}` address space")] 145 | ForbiddenInitializer(AddressSpace), 146 | #[error("duplicate declaration of `{0}` in the current scope")] 147 | DuplicateDecl(String), 148 | #[error("a declaration must have an explicit type or an initializer")] 149 | UntypedDecl, 150 | #[error("`{0}` declarations are forbidden in `{1}` scope")] 151 | ForbiddenDecl(DeclarationKind, ScopeKind), 152 | #[error("no resource was bound to `@group({0}) @binding({1})`")] 153 | MissingResource(u32, u32), 154 | #[error("incorrect resource address space, expected `{0}`, got `{1}`")] 155 | AddressSpace(AddressSpace, AddressSpace), 156 | #[error("incorrect resource access mode, expected `{0}`, got `{1}`")] 157 | AccessMode(AccessMode, AccessMode), 158 | 159 | // attributes 160 | #[error("missing `@group` or `@binding` attributes")] 161 | MissingBindAttr, 162 | #[error("missing `@workgroup_size` attribute")] 163 | MissingWorkgroupSize, 164 | #[error("`the attribute must evaluate to a positive integer, got `{0}`")] 165 | NegativeAttr(i64), 166 | #[error("the `@blend_src` attribute must evaluate to 0 or 1, got `{0}`")] 167 | InvalidBlendSrc(u32), 168 | 169 | // statements 170 | #[error("expected a reference, got value `{0}`")] 171 | NotRef(Instance), 172 | #[error("cannot assign a `{0}` to a `{1}`")] 173 | AssignType(Type, Type), 174 | #[error("cannot increment a `{0}`")] 175 | IncrType(Type), 176 | #[error("attempt to increment with overflow")] 177 | IncrOverflow, 178 | #[error("cannot decrement a `{0}`")] 179 | DecrType(Type), 180 | #[error("attempt to decrement with overflow")] 181 | DecrOverflow, 182 | #[error("a continuing body cannot contain a `{0}` statement")] 183 | FlowInContinuing(Flow), 184 | #[error("discard statements are not permitted in const contexts")] 185 | DiscardInConst, 186 | #[error("const assertion failed: `{0}` is `false`")] 187 | ConstAssertFailure(ExpressionNode), 188 | #[error("a function body cannot contain a `{0}` statement")] 189 | FlowInFunction(Flow), 190 | #[error("a global declaration cannot contain a `{0}` statement")] 191 | FlowInModule(Flow), 192 | } 193 | -------------------------------------------------------------------------------- /crates/wesl/src/eval/ops_std.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | apply_conversion, conv::Convert, Instance, LiteralInstance, MatInner, MatInstance, Ty, 3 | VecInner, VecInstance, 4 | }; 5 | use std::{ 6 | iter::zip, 7 | ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Not, Rem, Shl, Shr, Sub}, 8 | }; 9 | 10 | // OPS LITERAL 11 | 12 | impl Not for LiteralInstance { 13 | type Output = Result; 14 | fn not(self) -> Self::Output { 15 | self.op_not() 16 | } 17 | } 18 | impl Neg for LiteralInstance { 19 | type Output = Result; 20 | fn neg(self) -> Result { 21 | self.op_neg() 22 | } 23 | } 24 | impl Add for LiteralInstance { 25 | type Output = Result; 26 | fn add(self, rhs: Self) -> Self::Output { 27 | self.op_add(&rhs) 28 | } 29 | } 30 | impl Add<&VecInstance> for &LiteralInstance { 31 | type Output = Result; 32 | fn add(self, rhs: &VecInstance) -> Self::Output { 33 | rhs.compwise_unary(|k| k + *self).map(Into::into) 34 | } 35 | } 36 | impl Sub for LiteralInstance { 37 | type Output = Result; 38 | fn sub(self, rhs: Self) -> Self::Output { 39 | self.op_sub(&rhs) 40 | } 41 | } 42 | impl Sub<&VecInstance> for &LiteralInstance { 43 | type Output = Result; 44 | fn sub(self, rhs: &VecInstance) -> Self::Output { 45 | rhs.compwise_unary(|k| k - *self).map(Into::into) 46 | } 47 | } 48 | impl Mul for LiteralInstance { 49 | type Output = Result; 50 | fn mul(self, rhs: Self) -> Self::Output { 51 | self.op_mul(&rhs) 52 | } 53 | } 54 | impl Mul<&VecInstance> for &LiteralInstance { 55 | type Output = Result; 56 | fn mul(self, rhs: &VecInstance) -> Self::Output { 57 | rhs.op_mul_scalar(self) 58 | } 59 | } 60 | impl Mul<&MatInstance> for &LiteralInstance { 61 | type Output = Result; 62 | fn mul(self, rhs: &MatInstance) -> Self::Output { 63 | rhs.op_mul_scalar(self) 64 | } 65 | } 66 | impl Div for LiteralInstance { 67 | type Output = Result; 68 | fn div(self, rhs: Self) -> Self::Output { 69 | self.op_div(&rhs) 70 | } 71 | } 72 | impl Rem for LiteralInstance { 73 | type Output = Result; 74 | fn rem(self, rhs: Self) -> Self::Output { 75 | self.op_rem(&rhs) 76 | } 77 | } 78 | impl BitOr for LiteralInstance { 79 | type Output = Result; 80 | fn bitor(self, rhs: Self) -> Self::Output { 81 | self.op_bitor(&rhs) 82 | } 83 | } 84 | impl BitAnd for LiteralInstance { 85 | type Output = Result; 86 | fn bitand(self, rhs: Self) -> Self::Output { 87 | self.op_bitand(&rhs) 88 | } 89 | } 90 | impl BitXor for LiteralInstance { 91 | type Output = Result; 92 | fn bitxor(self, rhs: Self) -> Self::Output { 93 | self.op_bitxor(&rhs) 94 | } 95 | } 96 | impl Shl for LiteralInstance { 97 | type Output = Result; 98 | fn shl(self, rhs: Self) -> Self::Output { 99 | self.op_shl(&rhs) 100 | } 101 | } 102 | impl Shr for LiteralInstance { 103 | type Output = Result; 104 | fn shr(self, rhs: Self) -> Self::Output { 105 | self.op_shr(&rhs) 106 | } 107 | } 108 | 109 | // OPS VEC 110 | 111 | impl Neg for &VecInstance { 112 | type Output = Result; 113 | fn neg(self) -> Self::Output { 114 | self.compwise_unary(Neg::neg) 115 | } 116 | } 117 | impl Add for &VecInstance { 118 | type Output = Result; 119 | fn add(self, rhs: Self) -> Self::Output { 120 | self.op_add_vec(rhs) 121 | } 122 | } 123 | impl Add<&LiteralInstance> for &VecInstance { 124 | type Output = Result; 125 | fn add(self, rhs: &LiteralInstance) -> Self::Output { 126 | self.compwise_unary(|k| *rhs + k).map(Into::into) 127 | } 128 | } 129 | impl Sub for &VecInstance { 130 | type Output = Result; 131 | fn sub(self, rhs: Self) -> Self::Output { 132 | self.op_sub_vec(rhs) 133 | } 134 | } 135 | impl Sub<&LiteralInstance> for &VecInstance { 136 | type Output = Result; 137 | fn sub(self, rhs: &LiteralInstance) -> Self::Output { 138 | self.compwise_unary(|k| *rhs - k).map(Into::into) 139 | } 140 | } 141 | impl Mul for &VecInstance { 142 | type Output = Result; 143 | fn mul(self, rhs: Self) -> Self::Output { 144 | self.op_mul_vec(rhs) 145 | } 146 | } 147 | impl Mul<&LiteralInstance> for &VecInstance { 148 | type Output = Result; 149 | fn mul(self, rhs: &LiteralInstance) -> Self::Output { 150 | self.op_mul_scalar(rhs) 151 | } 152 | } 153 | impl Mul<&MatInstance> for &VecInstance { 154 | type Output = Result; 155 | fn mul(self, rhs: &MatInstance) -> Self::Output { 156 | self.op_mul_mat(rhs) 157 | } 158 | } 159 | impl Div for &VecInstance { 160 | type Output = Result; 161 | fn div(self, rhs: Self) -> Self::Output { 162 | self.op_div_vec(rhs) 163 | } 164 | } 165 | impl Rem for &VecInstance { 166 | type Output = Result; 167 | fn rem(self, rhs: Self) -> Self::Output { 168 | self.op_rem_vec(rhs) 169 | } 170 | } 171 | impl BitOr for &VecInstance { 172 | type Output = Result; 173 | fn bitor(self, rhs: Self) -> Self::Output { 174 | self.op_bitor(rhs) 175 | } 176 | } 177 | impl BitAnd for &VecInstance { 178 | type Output = Result; 179 | fn bitand(self, rhs: Self) -> Self::Output { 180 | self.op_bitand(rhs) 181 | } 182 | } 183 | impl BitXor for &VecInstance { 184 | type Output = Result; 185 | fn bitxor(self, rhs: Self) -> Self::Output { 186 | self.op_bitxor(rhs) 187 | } 188 | } 189 | 190 | // OPS MAT 191 | 192 | impl Add for &MatInstance { 193 | type Output = Result; 194 | fn add(self, rhs: Self) -> Self::Output { 195 | self.op_add_mat(rhs) 196 | } 197 | } 198 | impl Sub for &MatInstance { 199 | type Output = Result; 200 | fn sub(self, rhs: Self) -> Self::Output { 201 | self.op_sub_mat(rhs) 202 | } 203 | } 204 | impl Mul for &MatInstance { 205 | type Output = Result; 206 | fn mul(self, rhs: Self) -> Self::Output { 207 | self.op_mul_mat(rhs) 208 | } 209 | } 210 | impl Mul<&LiteralInstance> for &MatInstance { 211 | type Output = Result; 212 | fn mul(self, rhs: &LiteralInstance) -> Self::Output { 213 | self.op_mul_scalar(rhs) 214 | } 215 | } 216 | impl Mul<&VecInstance> for &MatInstance { 217 | type Output = Result; 218 | fn mul(self, rhs: &VecInstance) -> Self::Output { 219 | self.op_mul_vec(rhs) 220 | } 221 | } 222 | 223 | // OPS INSTANCE 224 | 225 | impl Not for &Instance { 226 | type Output = Result; 227 | fn not(self) -> Self::Output { 228 | self.op_not() 229 | } 230 | } 231 | impl Neg for &Instance { 232 | type Output = Result; 233 | fn neg(self) -> Self::Output { 234 | self.op_neg() 235 | } 236 | } 237 | impl Add for &Instance { 238 | type Output = Result; 239 | fn add(self, rhs: Self) -> Self::Output { 240 | self.op_add(rhs) 241 | } 242 | } 243 | impl Sub for &Instance { 244 | type Output = Result; 245 | fn sub(self, rhs: Self) -> Self::Output { 246 | self.op_sub(rhs) 247 | } 248 | } 249 | impl Mul for &Instance { 250 | type Output = Result; 251 | fn mul(self, rhs: Self) -> Self::Output { 252 | self.op_mul(rhs) 253 | } 254 | } 255 | impl Div for &Instance { 256 | type Output = Result; 257 | fn div(self, rhs: Self) -> Self::Output { 258 | self.op_div(rhs) 259 | } 260 | } 261 | impl Rem for &Instance { 262 | type Output = Result; 263 | fn rem(self, rhs: Self) -> Self::Output { 264 | self.op_rem(rhs) 265 | } 266 | } 267 | impl BitOr for &Instance { 268 | type Output = Result; 269 | fn bitor(self, rhs: Self) -> Self::Output { 270 | self.op_bitor(rhs) 271 | } 272 | } 273 | impl BitAnd for &Instance { 274 | type Output = Result; 275 | fn bitand(self, rhs: Self) -> Self::Output { 276 | self.op_bitand(rhs) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /crates/wesl/src/generics/mangle.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use wgsl_parse::syntax::*; 3 | 4 | // use crate::Mangler; 5 | 6 | // pub fn mangle(wesl: &mut TranslationUnit, mangler: impl Mangler) {} 7 | 8 | // fn mangle_literal(lit: &LiteralExpression) -> String { 9 | // match lit { 10 | // LiteralExpression::Bool(l) => format!("B{l}"), 11 | // LiteralExpression::AbstractInt(_) | 12 | // LiteralExpression::AbstractFloat(_) => panic!("abstract numeric types not allowed in mangling"), 13 | // LiteralExpression::I32(l) => format!("I{l}"), 14 | // LiteralExpression::U32(l) => format!("U{l}"), 15 | // LiteralExpression::F32(l) => format!("F{l}"), 16 | // LiteralExpression::F16(l) => format!("H{l}"), 17 | // } 18 | // } 19 | 20 | fn mangle_arg(ty: &TemplateArg) -> String { 21 | match ty.expression.node() { 22 | Expression::Literal(_) => { 23 | panic!("literal template arguments mangling not implemented") 24 | } 25 | Expression::TypeOrIdentifier(ty) => mangle_ty(ty), 26 | _ => panic!("invalid template argument expression type"), 27 | } 28 | } 29 | 30 | fn mangle_ty(ty: &TypeExpression) -> String { 31 | let args = ty.template_args.as_deref().unwrap_or_default(); 32 | let n1 = ty.ident.name().len(); 33 | let name = &ty.ident; 34 | let n2 = args.len(); 35 | let args = args.iter().map(mangle_arg).format(""); 36 | format!("{n1}{name}{n2}_{args}") 37 | } 38 | 39 | // https://refspecs.linuxbase.org/cxxabi-1.86.html#mangling 40 | pub fn mangle(name: &str, signature: &[TypeExpression]) -> String { 41 | let n1 = name.len(); 42 | let n2 = signature.len(); 43 | let sig = signature.iter().map(mangle_ty).format(""); 44 | format!("_WESL{n1}{name}{n2}_{sig}") 45 | } 46 | -------------------------------------------------------------------------------- /crates/wesl/src/lower.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, visit::Visit}; 2 | 3 | use wgsl_parse::syntax::*; 4 | 5 | /// Performs conversions on the final syntax tree to make it more compatible with WGSL 6 | /// implementations like Naga, catch errors early and perform optimizations. 7 | /// 8 | /// Currently, `lower` performs the following transforms: 9 | /// * remove aliases (inlined) 10 | /// * remove consts (inlined) 11 | /// * remove deprecated, non-standard attributes 12 | /// * remove import declarations 13 | /// 14 | /// with the `eval` feature flag enabled, it performs additional transforms: 15 | /// * evaluate const-expressions (including calls to const functions) 16 | /// * remove unreachable code paths after const-evaluation 17 | /// * remove function call statements to const functions (no side-effects) 18 | /// * make implicit conversions from abstract types explicit (using conversion rank) 19 | /// 20 | /// Customizing this behavior is not possible currently. The following transforms may 21 | /// be available in the future: 22 | /// * make variable types explicit 23 | /// * remove unused variables / code with no side-effects 24 | pub fn lower(wesl: &mut TranslationUnit) -> Result<(), Error> { 25 | wesl.imports.clear(); 26 | 27 | for attrs in Visit::::visit_mut(wesl) { 28 | attrs.retain(|attr| { 29 | !matches!(attr.node(), 30 | Attribute::Custom(CustomAttribute { ident, .. }) if *ident.name() == "generic") 31 | }) 32 | } 33 | 34 | #[cfg(not(feature = "eval"))] 35 | { 36 | // these are redundant with eval::lower. 37 | remove_type_aliases(wesl); 38 | remove_global_consts(wesl); 39 | } 40 | #[cfg(feature = "eval")] 41 | { 42 | use crate::Diagnostic; 43 | use crate::eval::{Context, Exec, Lower, mark_functions_const}; 44 | use wgsl_parse::Decorated; 45 | mark_functions_const(wesl); 46 | 47 | // we want to drop wesl2 at the end of the block for idents use_count 48 | { 49 | let wesl2 = wesl.clone(); 50 | let mut ctx = Context::new(&wesl2); 51 | wesl.exec(&mut ctx)?; // populate the ctx with module-scope declarations 52 | wesl.lower(&mut ctx) 53 | .map_err(|e| Diagnostic::from(e).with_ctx(&ctx))?; 54 | } 55 | 56 | // remove `@const` attributes. 57 | for decl in &mut wesl.global_declarations { 58 | if let GlobalDeclaration::Function(decl) = decl.node_mut() { 59 | decl.retain_attributes_mut(|attr| *attr != Attribute::Const); 60 | } 61 | } 62 | } 63 | Ok(()) 64 | } 65 | 66 | /// Eliminate all type aliases. 67 | /// Naga doesn't like this: `alias T = u32; vec` 68 | #[allow(unused)] 69 | fn remove_type_aliases(wesl: &mut TranslationUnit) { 70 | let take_next_alias = |wesl: &mut TranslationUnit| { 71 | let index = wesl 72 | .global_declarations 73 | .iter() 74 | .position(|decl| matches!(decl.node(), GlobalDeclaration::TypeAlias(_))); 75 | index.map(|index| { 76 | let decl = wesl.global_declarations.swap_remove(index); 77 | match decl.into_inner() { 78 | GlobalDeclaration::TypeAlias(alias) => alias, 79 | _ => unreachable!(), 80 | } 81 | }) 82 | }; 83 | 84 | while let Some(mut alias) = take_next_alias(wesl) { 85 | // we rename the alias and all references to its type expression, 86 | // and drop the alias declaration. 87 | alias.ident.rename(format!("{}", alias.ty)); 88 | } 89 | } 90 | 91 | /// Eliminate all const-declarations. 92 | /// 93 | /// Replace usages of the const-declaration with its expression. 94 | /// 95 | /// # Panics 96 | /// panics if the const-declaration is ill-formed, i.e. has no initializer. 97 | #[allow(unused)] 98 | fn remove_global_consts(wesl: &mut TranslationUnit) { 99 | let take_next_const = |wesl: &mut TranslationUnit| { 100 | let index = wesl.global_declarations.iter().position(|decl| { 101 | matches!( 102 | decl.node(), 103 | GlobalDeclaration::Declaration(Declaration { 104 | kind: DeclarationKind::Const, 105 | .. 106 | }) 107 | ) 108 | }); 109 | index.map(|index| { 110 | let decl = wesl.global_declarations.swap_remove(index); 111 | match decl.into_inner() { 112 | GlobalDeclaration::Declaration(d) => d, 113 | _ => unreachable!(), 114 | } 115 | }) 116 | }; 117 | 118 | while let Some(mut decl) = take_next_const(wesl) { 119 | // we rename the const and all references to its expression in parentheses, 120 | // and drop the const declaration. 121 | decl.ident 122 | .rename(format!("({})", decl.initializer.unwrap())); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/wesl/src/overload.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use itertools::Itertools; 4 | use wgsl_parse::syntax::{ 5 | Function, GlobalDeclaration, Ident, Import, TranslationUnit, TypeExpression, 6 | }; 7 | 8 | use crate::{ 9 | import::{absolute_resource, imported_resources, normalize_resource}, 10 | visit::Visit, 11 | Mangler, Resource, 12 | }; 13 | 14 | fn normalize_ty( 15 | ty: &TypeExpression, 16 | parent_res: &Resource, 17 | imports: &HashMap, 18 | ) -> TypeExpression { 19 | let mut ty = ty.clone(); 20 | if let Some(path) = &ty.path { 21 | let res = normalize_resource(path, parent_res, imports); 22 | ty.path = Some(res.path().to_path_buf()); 23 | } else { 24 | ty.path = Some(parent_res.path().to_path_buf()); 25 | } 26 | ty 27 | } 28 | 29 | pub(crate) fn mangle(wesl: &mut TranslationUnit, resource: &Resource, mangler: &impl Mangler) { 30 | let imports = imported_resources(&wesl.imports, resource); 31 | 32 | for decl in &mut wesl.global_declarations { 33 | if let GlobalDeclaration::Function(decl) = decl { 34 | // TODO: type aliases 35 | let sig = decl 36 | .parameters 37 | .iter() 38 | .map(|p| normalize_ty(&p.ty, resource, &imports)).collect_vec() 39 | let res = mangler.mangle_signature(decl.ident.name().as_str(), &sig); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/wesl/src/package.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{format_ident, quote}; 5 | use wgsl_parse::syntax::{PathOrigin, TranslationUnit}; 6 | 7 | use crate::{Diagnostic, Error, ModulePath, SyntaxUtil, validate::validate_wesl}; 8 | 9 | /// A builder that generates code for WESL packages. 10 | /// 11 | /// It is designed to be used in a build script (`build.rs` file). Add `wesl` to the 12 | /// build-dependencies of your project and enable the `package` feature flag. 13 | /// 14 | /// ```ignore 15 | /// // in build.rs 16 | /// fn main() { 17 | /// wesl::PkgBuilder::new("my_package") 18 | /// // read all wesl files in the directory "src/shaders" 19 | /// .scan_directory("src/shaders") 20 | /// .expect("failed to scan WESL files") 21 | /// // validation is optional 22 | /// .validate() 23 | /// .map_err(|e| eprintln!("{e}")) 24 | /// .expect("validation error") 25 | /// // write "my_package.rs" in OUT_DIR 26 | /// .build_artifact() 27 | /// .expect("failed to build artifact"); 28 | /// } 29 | /// ``` 30 | /// Then, in your `lib.rs` file, expose the generated module with the [`crate::wesl_pkg`] macro. 31 | /// ```ignore 32 | /// // in src/lib.rs 33 | /// wesl::wesl_pkg!(my_package); 34 | /// ``` 35 | /// 36 | /// The package name must be a valid rust identifier, E.g. it must not contain dashes `-`. 37 | /// Dashes are replaced with underscores `_`. 38 | pub struct PkgBuilder { 39 | name: String, 40 | } 41 | 42 | pub struct Module { 43 | name: String, 44 | source: String, 45 | submodules: Vec, 46 | } 47 | 48 | impl PkgBuilder { 49 | pub fn new(name: &str) -> Self { 50 | Self { 51 | name: name.replace('-', "_"), 52 | } 53 | } 54 | 55 | /// Reads all files in a directory to build the package. 56 | pub fn scan_directory(self, path: impl AsRef) -> std::io::Result { 57 | let dir = path.as_ref().to_path_buf(); 58 | // we look for a file with the same name as the dir in the same directory 59 | let mut lib_path = dir.clone(); 60 | lib_path.set_extension("wesl"); 61 | 62 | let source = if lib_path.is_file() { 63 | std::fs::read_to_string(&lib_path)? 64 | } else { 65 | lib_path.set_extension("wgsl"); 66 | if lib_path.is_file() { 67 | std::fs::read_to_string(&lib_path)? 68 | } else { 69 | String::from("") 70 | } 71 | }; 72 | 73 | let mut module = Module { 74 | name: self.name.clone(), 75 | source, 76 | submodules: Vec::new(), 77 | }; 78 | 79 | fn process_dir(module: &mut Module, dir: &Path) -> std::io::Result<()> { 80 | for entry in std::fs::read_dir(dir)? { 81 | let entry = entry?; 82 | let path = entry.path(); 83 | if path.is_file() 84 | && path 85 | .extension() 86 | .is_some_and(|ext| ext == "wesl" || ext == "wgsl") 87 | { 88 | let source = std::fs::read_to_string(&path)?; 89 | let name = path 90 | .file_stem() 91 | .unwrap() 92 | .to_string_lossy() 93 | .replace('-', "_"); 94 | // we look for a dir with the same name as the file in the same directory 95 | let mut subdir = dir.to_path_buf(); 96 | subdir.push(&name); 97 | 98 | let mut submod = Module { 99 | name, 100 | source, 101 | submodules: Vec::new(), 102 | }; 103 | 104 | if subdir.is_dir() { 105 | process_dir(&mut submod, &subdir)?; 106 | } 107 | 108 | module.submodules.push(submod); 109 | } 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | if dir.is_dir() { 116 | process_dir(&mut module, &dir)?; 117 | } 118 | 119 | Ok(module) 120 | } 121 | } 122 | 123 | impl Module { 124 | /// generate the rust code that holds the packaged wesl files. 125 | /// you probably want to use [`Self::build`] instead. 126 | pub fn codegen(&self) -> std::io::Result { 127 | fn codegen_module(module: &Module) -> TokenStream { 128 | let name = &module.name; 129 | let source = &module.source; 130 | 131 | let submodules = module.submodules.iter().map(|submod| { 132 | let name = &submod.name; 133 | let ident = format_ident!("{}", name); 134 | quote! { 135 | &#ident::Mod, 136 | } 137 | }); 138 | 139 | let match_arms = module.submodules.iter().map(|submod| { 140 | let name = &submod.name; 141 | let ident = format_ident!("{}", name); 142 | quote! { 143 | #name => Some(&#ident::Mod), 144 | } 145 | }); 146 | 147 | let subquotes = module.submodules.iter().map(|submod| { 148 | let ident = format_ident!("{}", submod.name); 149 | let module = codegen_module(submod); 150 | quote! { 151 | pub mod #ident { 152 | use super::PkgModule; 153 | #module 154 | } 155 | } 156 | }); 157 | 158 | quote! { 159 | pub struct Mod; 160 | 161 | impl PkgModule for Mod { 162 | fn name(&self) -> &'static str { 163 | #name 164 | } 165 | fn source(&self) -> &'static str { 166 | #source 167 | } 168 | fn submodules(&self) -> &[&dyn PkgModule] { 169 | static SUBMODULES: &[&dyn PkgModule] = &[ 170 | #(#submodules)* 171 | ]; 172 | SUBMODULES 173 | } 174 | fn submodule(&self, name: &str) -> Option<&'static dyn PkgModule> { 175 | #[allow(clippy::match_single_binding)] 176 | match name { 177 | #(#match_arms)* 178 | _ => None, 179 | } 180 | } 181 | } 182 | 183 | #(#subquotes)* 184 | } 185 | } 186 | 187 | let tokens = codegen_module(self); 188 | Ok(tokens.to_string()) 189 | } 190 | 191 | /// run validation checks on each of the scanned files. 192 | pub fn validate(self) -> Result { 193 | fn validate_module(module: &Module, path: ModulePath) -> Result<(), Error> { 194 | let mut wesl: TranslationUnit = module.source.parse().map_err(|e| { 195 | Diagnostic::from(e) 196 | .with_module_path(path.clone(), None) 197 | .with_source(module.source.clone()) 198 | })?; 199 | wesl.retarget_idents(); 200 | validate_wesl(&wesl)?; 201 | for module in &module.submodules { 202 | let mut path = path.clone(); 203 | path.push(&module.name); 204 | validate_module(module, path)?; 205 | } 206 | Ok(()) 207 | } 208 | let path = ModulePath::new(PathOrigin::Package, vec![self.name.clone()]); 209 | validate_module(&self, path)?; 210 | Ok(self) 211 | } 212 | 213 | /// generate the build artifact that can then be exposed by the [`super::wesl_pkg`] macro. 214 | /// 215 | /// this function must be called from a `build.rs` file. Refer to the crate documentation 216 | /// for more details. 217 | /// 218 | /// # Panics 219 | /// panics if the OUT_DIR environment variable is not set. This should not happen if 220 | /// ran from a `build.rs` file. 221 | pub fn build_artifact(&self) -> std::io::Result<()> { 222 | let code = self.codegen()?; 223 | let out_dir = Path::new( 224 | &std::env::var_os("OUT_DIR").expect("OUT_DIR environment variable is not defined"), 225 | ) 226 | .join(format!("{}.rs", self.name)); 227 | std::fs::write(&out_dir, code)?; 228 | Ok(()) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /crates/wesl/src/sourcemap.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, path::PathBuf}; 2 | 3 | use wgsl_parse::syntax::TypeExpression; 4 | 5 | use crate::{Mangler, ModulePath, ResolveError, Resolver}; 6 | 7 | /// A SourceMap is a lookup from compiled WGSL to source WESL. It translates a mangled 8 | /// name into a module path and declaration name. 9 | /// 10 | /// Using SourceMaps improves the readability of error diagnostics, by providing needed 11 | /// information to identify the originating code snippet, file name and declaration name. 12 | /// It is highly recommended to use them, but they can increase the compilation memory 13 | /// footprint, since they cache all loaded files. 14 | /// 15 | /// Typically you record to a SourceMap by passing a [`SourceMapper`] as the [`Resolver`] 16 | /// and [`Mangler`] when compiling code. 17 | pub trait SourceMap { 18 | /// Get the module path and declaration name from a mangled name. 19 | fn get_decl(&self, decl: &str) -> Option<(&ModulePath, &str)>; 20 | /// Get a module contents. 21 | fn get_source(&self, path: &ModulePath) -> Option<&str>; 22 | /// Get a module display name. 23 | fn get_display_name(&self, path: &ModulePath) -> Option<&str>; 24 | /// Get the default module contents. 25 | fn get_default_source(&self) -> Option<&str> { 26 | None 27 | } 28 | } 29 | 30 | /// Basic implementation of [`SourceMap`]. 31 | #[derive(Clone, Debug, Default)] 32 | pub struct BasicSourceMap { 33 | mappings: HashMap, 34 | sources: HashMap, String)>, // res -> (display_name, source) 35 | default_source: Option, 36 | } 37 | 38 | impl BasicSourceMap { 39 | pub fn new() -> Self { 40 | Default::default() 41 | } 42 | pub fn add_decl(&mut self, decl: String, path: ModulePath, item: String) { 43 | self.mappings.insert(decl, (path, item)); 44 | } 45 | pub fn add_source(&mut self, file: ModulePath, name: Option, source: String) { 46 | self.sources.insert(file, (name, source)); 47 | } 48 | pub fn set_default_source(&mut self, source: String) { 49 | self.default_source = Some(source); 50 | } 51 | } 52 | 53 | impl SourceMap for BasicSourceMap { 54 | fn get_decl(&self, decl: &str) -> Option<(&ModulePath, &str)> { 55 | let (path, decl) = self.mappings.get(decl)?; 56 | Some((path, decl)) 57 | } 58 | 59 | fn get_source(&self, path: &ModulePath) -> Option<&str> { 60 | self.sources.get(path).map(|(_, source)| source.as_str()) 61 | } 62 | fn get_display_name(&self, path: &ModulePath) -> Option<&str> { 63 | self.sources.get(path).and_then(|(name, _)| name.as_deref()) 64 | } 65 | fn get_default_source(&self) -> Option<&str> { 66 | self.default_source.as_deref() 67 | } 68 | } 69 | 70 | impl SourceMap for Option { 71 | fn get_decl(&self, decl: &str) -> Option<(&ModulePath, &str)> { 72 | self.as_ref().and_then(|map| map.get_decl(decl)) 73 | } 74 | fn get_source(&self, path: &ModulePath) -> Option<&str> { 75 | self.as_ref().and_then(|map| map.get_source(path)) 76 | } 77 | fn get_display_name(&self, path: &ModulePath) -> Option<&str> { 78 | self.as_ref().and_then(|map| map.get_display_name(path)) 79 | } 80 | fn get_default_source(&self) -> Option<&str> { 81 | self.as_ref().and_then(|map| map.get_default_source()) 82 | } 83 | } 84 | 85 | /// This [`SourceMap`] implementation simply does nothing and returns `None`. 86 | /// 87 | /// It can be useful to pass this struct to functions requiring a sourcemap, but 88 | /// you don't care about sourcemapping. 89 | pub struct NoSourceMap; 90 | 91 | impl SourceMap for NoSourceMap { 92 | fn get_decl(&self, _decl: &str) -> Option<(&ModulePath, &str)> { 93 | None 94 | } 95 | fn get_source(&self, _path: &ModulePath) -> Option<&str> { 96 | None 97 | } 98 | fn get_display_name(&self, _path: &ModulePath) -> Option<&str> { 99 | None 100 | } 101 | fn get_default_source(&self) -> Option<&str> { 102 | None 103 | } 104 | } 105 | 106 | /// Generate a SourceMap by keeping track of loaded files and mangled identifiers. 107 | /// 108 | /// `SourceMapper` is a proxy that implements [`Mangler`] and [`Resolver`]. To record a 109 | /// SourceMap, invoke the compiler with this instance as both the mangler and the 110 | /// resolver. Call [`SourceMapper::finish`] to get the final SourceMap once finished 111 | /// recording. 112 | pub struct SourceMapper<'a> { 113 | pub root: &'a ModulePath, 114 | pub resolver: &'a dyn Resolver, 115 | pub mangler: &'a dyn Mangler, 116 | pub sourcemap: RefCell, 117 | } 118 | 119 | impl<'a> SourceMapper<'a> { 120 | /// Create a new `SourceMapper` from a mangler and a resolver. 121 | pub fn new(root: &'a ModulePath, resolver: &'a dyn Resolver, mangler: &'a dyn Mangler) -> Self { 122 | Self { 123 | root, 124 | resolver, 125 | mangler, 126 | sourcemap: Default::default(), 127 | } 128 | } 129 | /// Consume this and return a [`BasicSourceMap`]. 130 | pub fn finish(self) -> BasicSourceMap { 131 | let mut sourcemap = self.sourcemap.into_inner(); 132 | if let Some(source) = sourcemap.get_source(self.root) { 133 | sourcemap.set_default_source(source.to_string()); 134 | } 135 | sourcemap 136 | } 137 | } 138 | 139 | impl Resolver for SourceMapper<'_> { 140 | fn resolve_source<'a>( 141 | &'a self, 142 | path: &ModulePath, 143 | ) -> Result, ResolveError> { 144 | let res = self.resolver.resolve_source(path)?; 145 | let mut sourcemap = self.sourcemap.borrow_mut(); 146 | sourcemap.add_source( 147 | path.clone(), 148 | self.resolver.display_name(path), 149 | res.clone().into(), 150 | ); 151 | Ok(res) 152 | } 153 | fn display_name(&self, path: &ModulePath) -> Option { 154 | self.resolver.display_name(path) 155 | } 156 | fn fs_path(&self, path: &ModulePath) -> Option { 157 | self.resolver.fs_path(path) 158 | } 159 | } 160 | 161 | impl Mangler for SourceMapper<'_> { 162 | fn mangle(&self, path: &ModulePath, item: &str) -> String { 163 | let res = self.mangler.mangle(path, item); 164 | let mut sourcemap = self.sourcemap.borrow_mut(); 165 | sourcemap.add_decl(res.clone(), path.clone(), item.to_string()); 166 | res 167 | } 168 | fn unmangle(&self, mangled: &str) -> Option<(ModulePath, String)> { 169 | self.mangler.unmangle(mangled) 170 | } 171 | fn mangle_types(&self, item: &str, variant: u32, types: &[TypeExpression]) -> String { 172 | self.mangler.mangle_types(item, variant, types) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /crates/wesl/src/strip.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use wgsl_parse::syntax::{Ident, TranslationUnit}; 4 | 5 | /// Remove unused declarations. 6 | pub(crate) fn strip_except(wgsl: &mut TranslationUnit, keep: &HashSet) { 7 | wgsl.global_declarations.retain_mut(|decl| { 8 | if let Some(id) = decl.ident() { 9 | keep.contains(id) || id.use_count() > 1 10 | } else { 11 | true 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /crates/wgsl-parse/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgsl-parse" 3 | description = "Parse a wgsl source file to a syntax tree" 4 | documentation = "https://docs.rs/wgsl-parse" 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | rust-version.workspace = true 11 | 12 | [dependencies] 13 | annotate-snippets = "0.11.4" 14 | derive_more = { version = "2.0.1", features = [ 15 | "as_ref", 16 | "constructor", 17 | "deref", 18 | "deref_mut", 19 | "from", 20 | "is_variant", 21 | "try_unwrap", 22 | "unwrap", 23 | ] } 24 | itertools = "0.14.0" 25 | lalrpop-util = "0.22.1" 26 | lexical = { version = "7.0.4", features = ["format", "power-of-two"] } 27 | logos = "0.15.0" 28 | thiserror = "2.0.12" 29 | 30 | serde = { version = "1.0.215", optional = true, features = ["derive", "rc"] } 31 | tokrepr = { workspace = true, optional = true } 32 | 33 | [build-dependencies] 34 | lalrpop = { version = "0.22.1", default-features = false } 35 | 36 | [features] 37 | default = [] 38 | 39 | wesl = ["imports", "condcomp"] 40 | 41 | # allow attributes on most declarations and statements. 42 | # reference: https://github.com/wgsl-tooling-wg/wesl-spec/blob/main/ConditionalTranslation.md#appendix-updated-grammar 43 | attributes = [] 44 | 45 | # allow templates on function declarations 46 | # reference: none yet 47 | templates = [] 48 | 49 | # reference: none yet 50 | generics = ["attributes"] 51 | 52 | # conditional translation attribute (@if). 53 | # reference: https://github.com/wgsl-tooling-wg/wesl-spec/blob/main/ConditionalTranslation.md 54 | condcomp = ["attributes"] 55 | 56 | # import declarations. 57 | # reference: https://github.com/wgsl-tooling-wg/wesl-spec/blob/main/Imports.md 58 | imports = [] 59 | 60 | # allow naga/wgpu extensions. 61 | # listed here: https://github.com/gfx-rs/wgpu/issues/4410 62 | # more here: https://github.com/gfx-rs/wgpu/blob/b93b55920a978ef9f013efe8d75cb10e69488629/naga/src/valid/mod.rs#L83 63 | naga_ext = ["push_constant"] 64 | push_constant = [] 65 | 66 | serde = ["dep:serde"] 67 | tokrepr = ["dep:tokrepr"] 68 | 69 | [lints] 70 | workspace = true 71 | -------------------------------------------------------------------------------- /crates/wgsl-parse/README.md: -------------------------------------------------------------------------------- 1 | # wgsl-parse 2 | 3 | 4 | 5 | A parser and syntax tree for WGSL files, written directly from the [specification] with [lalrpop]. 6 | 7 | It supports WESL language extensions guarded by feature flags. 8 | 9 | ## WESL Features 10 | 11 | | name | description | WESL Specification | 12 | |--------------|------------------------------------------------|--------------------------| 13 | | `wesl` | enable all WESL extensions below | | 14 | | `imports` | `import` statements and inline qualified paths | [complete][imports] | 15 | | `attributes` | extra attributes locations on statements | [complete][condcomp] | 16 | | `condcomp` | `@if` attributes | [complete][condcomp] | 17 | | `generics` | `@type` attributes | [experimental][generics] | 18 | 19 | ## Other Features 20 | 21 | | name | description | 22 | |------------|------------------------------------------------------------| 23 | | `naga_ext` | enable all Naga/WGPU extensions (experimental) | 24 | | `serde` | derive `Serialize` and `Deserialize` for syntax tree nodes | 25 | | `tokrepr` | derive `TokRepr` for syntax tree nodes | 26 | 27 | ## Parsing and Stringification 28 | 29 | [`TranslationUnit`][syntax::TranslationUnit] implements [`FromStr`][std::str::FromStr]. 30 | Other syntax nodes also implement `FromStr`: `GlobalDirective`, `GlobalDeclaration`, `Statement`, `Expression` and `ImportStatement`. 31 | 32 | The syntax tree elements implement [`Display`][std::fmt::Display]. 33 | The display is always pretty-printed. 34 | 35 | ```rust 36 | # use wgsl_parse::syntax::TranslationUnit; 37 | # use std::str::FromStr; 38 | let source = "@fragment fn frag_main() -> @location(0) vec4f { return vec4(1); }"; 39 | let mut module = TranslationUnit::from_str(source).unwrap(); 40 | // modify the module as needed... 41 | println!("{module}"); 42 | ``` 43 | 44 | [lalrpop]: https://lalrpop.github.io/lalrpop/ 45 | [specification]: https://www.w3.org/TR/WGSL/ 46 | [imports]: https://wesl-lang.dev/spec/Imports 47 | [condcomp]: https://wesl-lang.dev/spec/ConditionalTranslation 48 | [generics]: https://github.com/k2d222/wesl-spec/blob/generics/Generics.md 49 | -------------------------------------------------------------------------------- /crates/wgsl-parse/build.rs: -------------------------------------------------------------------------------- 1 | use lalrpop::Configuration; 2 | 3 | fn main() { 4 | println!("cargo::rerun-if-changed=build.rs"); 5 | Configuration::new() 6 | .use_cargo_dir_conventions() 7 | .emit_rerun_directives(true) 8 | .process() 9 | .unwrap(); 10 | } 11 | -------------------------------------------------------------------------------- /crates/wgsl-parse/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::{Debug, Display}, 4 | }; 5 | 6 | use itertools::Itertools; 7 | use thiserror::Error; 8 | 9 | use crate::{lexer::Token, span::Span}; 10 | 11 | /// WGSL parse error kind. 12 | #[derive(Error, Clone, Debug, PartialEq)] 13 | pub enum ErrorKind { 14 | #[error("invalid token")] 15 | InvalidToken, 16 | #[error("use of a reserved word `{0}`")] 17 | ReservedWord(String), 18 | #[error("unexpected token `{token}`, expected `{}`", .expected.iter().format(", "))] 19 | UnexpectedToken { 20 | token: String, 21 | expected: Vec, 22 | }, 23 | #[error("unexpected end of file, expected `{}`", .expected.iter().format(", "))] 24 | UnexpectedEof { expected: Vec }, 25 | #[error("extra token `{0}` at the end of the file")] 26 | ExtraToken(String), 27 | #[error("invalid diagnostic severity")] 28 | DiagnosticSeverity, 29 | #[error("invalid `{0}` attribute, {1}")] 30 | Attribute(&'static str, &'static str), 31 | #[error("invalid `var` template arguments, {0}")] 32 | VarTemplate(&'static str), 33 | } 34 | 35 | #[derive(Default, Clone, Debug, PartialEq)] 36 | pub enum ParseError { 37 | #[default] 38 | LexerError, 39 | ReservedWord(String), 40 | DiagnosticSeverity, 41 | Attribute(&'static str, &'static str), 42 | VarTemplate(&'static str), 43 | } 44 | 45 | type LalrpopError = lalrpop_util::ParseError; 46 | 47 | /// WGSL parse error. 48 | /// 49 | /// This error can be pretty-printed with the source snippet with [`Error::with_source`] 50 | #[derive(Error, Clone, Debug, PartialEq)] 51 | pub struct Error { 52 | pub error: ErrorKind, 53 | pub span: Span, 54 | } 55 | 56 | impl Error { 57 | /// Returns an [`ErrorWithSource`], a wrapper type that implements `Display` and prints 58 | /// a user-friendly error snippet. 59 | pub fn with_source(self, source: Cow<'_, str>) -> ErrorWithSource<'_> { 60 | ErrorWithSource::new(self, source) 61 | } 62 | } 63 | 64 | impl Display for Error { 65 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 66 | write!(f, "chars {:?}: {}", self.span.range(), self.error) 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(err: LalrpopError) -> Self { 72 | match err { 73 | LalrpopError::InvalidToken { location } => { 74 | let span = Span::new(location..location + 1); 75 | let error = ErrorKind::InvalidToken; 76 | Self { span, error } 77 | } 78 | LalrpopError::UnrecognizedEof { location, expected } => { 79 | let span = Span::new(location..location + 1); 80 | let error = ErrorKind::UnexpectedEof { expected }; 81 | Self { span, error } 82 | } 83 | LalrpopError::UnrecognizedToken { 84 | token: (l, token, r), 85 | expected, 86 | } => { 87 | let span = Span::new(l..r); 88 | let error = ErrorKind::UnexpectedToken { 89 | token: token.to_string(), 90 | expected, 91 | }; 92 | Self { span, error } 93 | } 94 | LalrpopError::ExtraToken { 95 | token: (l, token, r), 96 | } => { 97 | let span = Span::new(l..r); 98 | let error = ErrorKind::ExtraToken(token.to_string()); 99 | Self { span, error } 100 | } 101 | LalrpopError::User { 102 | error: (l, error, r), 103 | } => { 104 | let span = Span::new(l..r); 105 | let error = match error { 106 | ParseError::LexerError => ErrorKind::InvalidToken, 107 | ParseError::ReservedWord(word) => ErrorKind::ReservedWord(word), 108 | ParseError::DiagnosticSeverity => ErrorKind::DiagnosticSeverity, 109 | ParseError::Attribute(attr, expected) => ErrorKind::Attribute(attr, expected), 110 | ParseError::VarTemplate(reason) => ErrorKind::VarTemplate(reason), 111 | }; 112 | Self { span, error } 113 | } 114 | } 115 | } 116 | } 117 | 118 | /// A wrapper type that implements `Display` and prints a user-friendly error snippet. 119 | #[derive(Clone, Debug, PartialEq)] 120 | pub struct ErrorWithSource<'s> { 121 | pub error: Error, 122 | pub source: Cow<'s, str>, 123 | } 124 | 125 | impl std::error::Error for ErrorWithSource<'_> {} 126 | 127 | impl<'s> ErrorWithSource<'s> { 128 | pub fn new(error: Error, source: Cow<'s, str>) -> Self { 129 | Self { error, source } 130 | } 131 | } 132 | 133 | impl Display for ErrorWithSource<'_> { 134 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 135 | use annotate_snippets::*; 136 | let text = format!("{}", self.error.error); 137 | 138 | let annot = Level::Info.span(self.error.span.range()); 139 | let snip = Snippet::source(&self.source).fold(true).annotation(annot); 140 | let msg = Level::Error.title(&text).snippet(snip); 141 | 142 | let renderer = Renderer::styled(); 143 | let rendered = renderer.render(msg); 144 | write!(f, "{rendered}") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /crates/wgsl-parse/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod error; 4 | pub mod lexer; 5 | pub mod parser; 6 | pub mod span; 7 | pub mod syntax; 8 | 9 | mod parser_support; 10 | mod syntax_display; 11 | mod syntax_impl; 12 | 13 | #[cfg(feature = "tokrepr")] 14 | pub mod tokrepr; 15 | #[cfg(feature = "tokrepr")] 16 | pub use ::tokrepr::TokRepr; 17 | 18 | pub use error::Error; 19 | pub use parser::{parse_str, recognize_str}; 20 | pub use syntax_impl::Decorated; 21 | -------------------------------------------------------------------------------- /crates/wgsl-parse/src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{ 4 | error::Error, 5 | lexer::{Lexer, TokenIterator}, 6 | syntax::{Expression, GlobalDeclaration, GlobalDirective, Statement, TranslationUnit}, 7 | }; 8 | 9 | use lalrpop_util::lalrpop_mod; 10 | 11 | lalrpop_mod!( 12 | #[allow(clippy::all, reason = "generated code")] 13 | wgsl 14 | ); 15 | lalrpop_mod!( 16 | #[allow(clippy::all, reason = "generated code")] 17 | wgsl_recognize 18 | ); 19 | 20 | pub use wgsl::*; 21 | 22 | /// Parse a string into a syntax tree ([`TranslationUnit`]). 23 | /// 24 | /// Identical to [`TranslationUnit::from_str`]. 25 | pub fn parse_str(source: &str) -> Result { 26 | let lexer = Lexer::new(source); 27 | let parser = TranslationUnitParser::new(); 28 | parser.parse(lexer).map_err(Into::into) 29 | } 30 | 31 | /// Test whether a string represent a valid WGSL module ([`TranslationUnit`]). 32 | /// 33 | /// Warning: it does not take WESL extensions into account. 34 | pub fn recognize_str(source: &str) -> Result<(), Error> { 35 | let lexer = Lexer::new(source); 36 | let parser = wgsl_recognize::TranslationUnitParser::new(); 37 | parser.parse(lexer).map_err(Into::into) 38 | } 39 | 40 | pub fn recognize_template_list(lexer: impl TokenIterator) -> Result<(), Error> { 41 | let parser = TryTemplateListParser::new(); 42 | parser.parse(lexer).map(|_| ()).map_err(Into::into) 43 | } 44 | 45 | impl FromStr for TranslationUnit { 46 | type Err = Error; 47 | 48 | fn from_str(source: &str) -> Result { 49 | let lexer = Lexer::new(source); 50 | let parser = TranslationUnitParser::new(); 51 | parser.parse(lexer).map_err(Into::into) 52 | } 53 | } 54 | impl FromStr for GlobalDirective { 55 | type Err = Error; 56 | 57 | fn from_str(source: &str) -> Result { 58 | let lexer = Lexer::new(source); 59 | let parser = GlobalDirectiveParser::new(); 60 | parser.parse(lexer).map_err(Into::into) 61 | } 62 | } 63 | impl FromStr for GlobalDeclaration { 64 | type Err = Error; 65 | 66 | fn from_str(source: &str) -> Result { 67 | let lexer = Lexer::new(source); 68 | let parser = GlobalDeclParser::new(); 69 | parser.parse(lexer).map_err(Into::into) 70 | } 71 | } 72 | impl FromStr for Statement { 73 | type Err = Error; 74 | 75 | fn from_str(source: &str) -> Result { 76 | let lexer = Lexer::new(source); 77 | let parser = StatementParser::new(); 78 | parser.parse(lexer).map_err(Into::into) 79 | } 80 | } 81 | impl FromStr for Expression { 82 | type Err = Error; 83 | 84 | fn from_str(source: &str) -> Result { 85 | let lexer = Lexer::new(source); 86 | let parser = ExpressionParser::new(); 87 | parser.parse(lexer).map_err(Into::into) 88 | } 89 | } 90 | #[cfg(feature = "imports")] 91 | impl FromStr for crate::syntax::ImportStatement { 92 | type Err = Error; 93 | 94 | fn from_str(source: &str) -> Result { 95 | let lexer = Lexer::new(source); 96 | let parser = ImportStatementParser::new(); 97 | parser.parse(lexer).map_err(Into::into) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/wgsl-parse/src/span.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use derive_more::derive::{AsMut, AsRef, Deref, DerefMut, From}; 4 | 5 | pub type Id = u32; 6 | 7 | #[cfg_attr(feature = "tokrepr", derive(tokrepr::TokRepr))] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] 10 | pub struct Span { 11 | /// The lower bound of the span (inclusive). 12 | pub start: usize, 13 | /// The upper bound of the span (exclusive). 14 | pub end: usize, 15 | } 16 | // Deref, DerefMut, AsRef, AsMut 17 | impl Span { 18 | pub fn new(range: Range) -> Self { 19 | Self { 20 | start: range.start, 21 | end: range.end, 22 | } 23 | } 24 | pub fn range(&self) -> Range { 25 | self.start..self.end 26 | } 27 | pub fn extend(&self, other: Span) -> Self { 28 | Self { 29 | start: self.start, 30 | end: other.end, 31 | } 32 | } 33 | } 34 | 35 | impl From> for Span { 36 | fn from(value: Range) -> Self { 37 | Span::new(value) 38 | } 39 | } 40 | 41 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 42 | #[derive(Default, Clone, Debug, Deref, DerefMut, AsRef, AsMut, From)] 43 | pub struct Spanned { 44 | span: Span, 45 | #[deref(forward)] 46 | #[deref_mut(forward)] 47 | #[as_ref(T)] 48 | #[as_mut(T)] 49 | #[from(T)] 50 | node: Box, 51 | } 52 | 53 | // we ignore the spans for equality comparison 54 | impl PartialEq for Spanned { 55 | fn eq(&self, other: &Self) -> bool { 56 | self.node.eq(&other.node) 57 | } 58 | } 59 | 60 | impl Spanned { 61 | pub fn new(node: T, span: Span) -> Self { 62 | Self::new_boxed(Box::new(node), span) 63 | } 64 | pub fn new_boxed(node: Box, span: Span) -> Self { 65 | Self { span, node } 66 | } 67 | pub fn span(&self) -> Span { 68 | self.span 69 | } 70 | pub fn node(&self) -> &T { 71 | self 72 | } 73 | pub fn node_mut(&mut self) -> &mut T { 74 | self 75 | } 76 | pub fn into_inner(self) -> T { 77 | *self.node 78 | } 79 | } 80 | 81 | impl From for Spanned { 82 | fn from(value: T) -> Self { 83 | Self { 84 | span: Default::default(), 85 | node: Box::new(value), 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/wgsl-parse/src/tokrepr.rs: -------------------------------------------------------------------------------- 1 | //! Turn an instance into a `TokenStream` that represents the instance. 2 | 3 | use tokrepr::TokRepr; 4 | use tokrepr::proc_macro2::TokenStream; 5 | use tokrepr::quote::{format_ident, quote}; 6 | 7 | use crate::syntax::*; 8 | use crate::{span::Spanned, syntax::Ident}; 9 | 10 | /// named nodes are those that have an identifier. We use this trait to conditionally 11 | /// implement TokRepr for Spanned, which allows code injection in `quote_*!` 12 | /// macros from the `wesl` crate. 13 | trait NamedNode { 14 | fn ident(&self) -> Option<&Ident>; 15 | } 16 | 17 | impl NamedNode for GlobalDeclaration { 18 | fn ident(&self) -> Option<&Ident> { 19 | self.ident() 20 | } 21 | } 22 | 23 | impl NamedNode for StructMember { 24 | fn ident(&self) -> Option<&Ident> { 25 | Some(&self.ident) 26 | } 27 | } 28 | 29 | impl NamedNode for Attribute { 30 | fn ident(&self) -> Option<&Ident> { 31 | if let Attribute::Custom(attr) = self { 32 | Some(&attr.ident) 33 | } else { 34 | None 35 | } 36 | } 37 | } 38 | 39 | impl NamedNode for Expression { 40 | fn ident(&self) -> Option<&Ident> { 41 | if let Expression::TypeOrIdentifier(ty) = self { 42 | Some(&ty.ident) 43 | } else { 44 | None 45 | } 46 | } 47 | } 48 | 49 | impl NamedNode for Statement { 50 | fn ident(&self) -> Option<&Ident> { 51 | // COMBAK: this nesting is hell. Hopefully if-let chains will stabilize soon. 52 | if let Statement::Compound(stmt) = self { 53 | if stmt.statements.is_empty() { 54 | if let [attr] = stmt.attributes.as_slice() { 55 | if let Attribute::Custom(attr) = attr.node() { 56 | return Some(&attr.ident); 57 | } 58 | } 59 | } 60 | } 61 | None 62 | } 63 | } 64 | 65 | impl TokRepr for Spanned { 66 | fn tok_repr(&self) -> TokenStream { 67 | let node = self.node().tok_repr(); 68 | let span = self.span().tok_repr(); 69 | 70 | if let Some(ident) = self.ident() { 71 | let name = ident.name(); 72 | if let Some(name) = name.strip_prefix("#") { 73 | let ident = format_ident!("{}", name); 74 | 75 | return quote! { 76 | Spanned::new(#ident.to_owned().into(), #span) 77 | }; 78 | } 79 | } 80 | 81 | quote! { 82 | Spanned::new(#node, #span) 83 | } 84 | } 85 | } 86 | 87 | impl TokRepr for Ident { 88 | fn tok_repr(&self) -> TokenStream { 89 | let name = self.name(); 90 | if let Some(name) = name.strip_prefix("#") { 91 | let ident = format_ident!("{}", name); 92 | quote! { 93 | Ident::from(#ident.to_owned()) 94 | } 95 | } else { 96 | let name = name.as_str(); 97 | quote! { 98 | Ident::new(#name.to_string()) 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | all-features = true 3 | 4 | [advisories] 5 | version = 2 6 | ignore = [ 7 | # See issue #69 8 | "RUSTSEC-2024-0320", 9 | ] 10 | 11 | [licenses] 12 | version = 2 13 | allow = [ 14 | "0BSD", 15 | "Apache-2.0", 16 | "Apache-2.0 WITH LLVM-exception", 17 | "BSD-2-Clause", 18 | "BSD-3-Clause", 19 | "BSL-1.0", 20 | "CC0-1.0", 21 | "ISC", 22 | "MIT", 23 | "MIT-0", 24 | "Unlicense", 25 | "Zlib", 26 | ] 27 | 28 | exceptions = [{ name = "unicode-ident", allow = ["Unicode-3.0"] }] 29 | 30 | [bans] 31 | multiple-versions = "warn" 32 | wildcards = "deny" 33 | 34 | allow-wildcard-paths = true 35 | 36 | # Certain crates that we don't want multiple versions of in the dependency tree 37 | deny = [] 38 | 39 | skip = [] 40 | 41 | [sources] 42 | unknown-registry = "deny" 43 | unknown-git = "deny" 44 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 45 | allow-git = [] 46 | 47 | [[bans.features]] 48 | # thiserror is the preferred way to derive error types 49 | crate = "derive_more" 50 | deny = ["error"] 51 | -------------------------------------------------------------------------------- /examples/random_wgsl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "random_wgsl" 3 | description = "Random utility package for WGSL/WESL" 4 | documentation = "https://docs.rs/random_wgsl" 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | rust-version.workspace = true 11 | 12 | publish = false 13 | 14 | [dependencies] 15 | wesl = { workspace = true } 16 | 17 | [build-dependencies] 18 | wesl = { workspace = true, features = ["package"] } 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /examples/random_wgsl/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | wesl::PkgBuilder::new("random") 3 | .scan_directory("src/shaders/random") 4 | .expect("failed to scan WESL files") 5 | .validate() 6 | .inspect_err(|e| { 7 | eprintln!("{e}"); 8 | panic!(); 9 | }) 10 | .unwrap() 11 | .build_artifact() 12 | .expect("failed to build artifact") 13 | } 14 | -------------------------------------------------------------------------------- /examples/random_wgsl/src/lib.rs: -------------------------------------------------------------------------------- 1 | wesl::wesl_pkg!(random); 2 | -------------------------------------------------------------------------------- /examples/random_wgsl/src/shaders/random.wesl: -------------------------------------------------------------------------------- 1 | // PCG pseudo random generator from vec2u to vec4f 2 | // the random output is in the range from zero to 1 3 | fn pcg_2u_3f(pos: vec2u) -> vec3f { 4 | let seed = mix2to3(pos); 5 | let random = pcg_3u_3u(seed); 6 | let normalized = ldexp(vec3f(random), vec3(-32)); 7 | return vec3f(normalized); 8 | } 9 | 10 | // PCG random generator from vec3u to vec3u 11 | // adapted from http://www.jcgt.org/published/0009/03/02/ 12 | fn pcg_3u_3u(seed: vec3u) -> vec3u { 13 | var v = seed * 1664525u + 1013904223u; 14 | 15 | v = mixing(v); 16 | v ^= v >> vec3(16u); 17 | v = mixing(v); 18 | 19 | return v; 20 | } 21 | 22 | // permuted lcg 23 | fn mixing(v: vec3u) -> vec3u { 24 | var m: vec3u = v; 25 | m.x += v.y * v.z; 26 | m.y += v.z * v.x; 27 | m.z += v.x * v.y; 28 | 29 | return m; 30 | } 31 | 32 | // mix position into a seed as per: https://www.shadertoy.com/view/XlGcRh 33 | fn mix2to3(p: vec2u) -> vec3u { 34 | let seed = vec3u( 35 | p.x, 36 | p.x ^ p.y, 37 | p.x + p.y, 38 | ); 39 | return seed; 40 | } 41 | 42 | // from https://stackoverflow.com/questions/12964279/whats-the-origin-of-this-glsl-rand-one-liner 43 | fn sinRand(co: vec2f) -> f32 { 44 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); 45 | } 46 | -------------------------------------------------------------------------------- /examples/wesl-consumer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wesl-consumer" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | 10 | publish = false 11 | 12 | [dependencies] 13 | random_wgsl = { path = "../random_wgsl" } 14 | wesl = { workspace = true } 15 | 16 | [build-dependencies] 17 | random_wgsl = { path = "../random_wgsl" } 18 | wesl = { workspace = true } 19 | 20 | [features] 21 | # use WESL at build-time (in build.rs) instead of run-time (in main.rs) 22 | build-time = [] 23 | 24 | [lints] 25 | workspace = true 26 | -------------------------------------------------------------------------------- /examples/wesl-consumer/README.md: -------------------------------------------------------------------------------- 1 | # wesl-consumer 2 | 3 | This is an example crate that makes use of an external WESL package `random_wgsl` distributed via Cargo. 4 | 5 | The final shader is assembled at build-time in the `build.rs` file. It is possible to do this at runtime. Move the `random_wgsl` crate from `[build-dependencies]` to `[dependencies]`, and copy-paste the code in `build.rs`. (replace `.build("main.wgsl")` with `.compile("main.wgsl").unwrap().to_string()`) 6 | -------------------------------------------------------------------------------- /examples/wesl-consumer/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "build-time")] 3 | wesl::Wesl::new("src/shaders") 4 | .add_package(&random_wgsl::random::Mod) 5 | .build_artifact("main", "main"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/wesl-consumer/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // run wesl at build-time 3 | #[cfg(feature = "build-time")] 4 | let source = { 5 | use wesl::include_wesl; 6 | include_wesl!("main") 7 | }; 8 | 9 | // run wesl at run-time 10 | #[cfg(not(feature = "build-time"))] 11 | let source = wesl::Wesl::new("src/shaders") 12 | .add_package(&random_wgsl::random::Mod) 13 | .compile("main") 14 | .inspect_err(|e| { 15 | eprintln!("{e}"); 16 | panic!(); 17 | }) 18 | .unwrap() 19 | .to_string(); 20 | 21 | println!("{source}"); 22 | 23 | use wesl::syntax::*; 24 | 25 | let timestamp = std::time::SystemTime::now() 26 | .duration_since(std::time::UNIX_EPOCH) 27 | .unwrap() 28 | .as_secs() as i64; 29 | 30 | // using the procedural macro 31 | let source = wesl::quote_module! { 32 | const timestamp = #timestamp; 33 | }; 34 | println!("quoted: {source}") 35 | } 36 | -------------------------------------------------------------------------------- /examples/wesl-consumer/src/shaders/main.wesl: -------------------------------------------------------------------------------- 1 | import random::pcg_3u_3u; 2 | import super::util::rotate_3u; 3 | 4 | @compute 5 | fn main(@builtin(global_invocation_id) index: vec3u) -> vec3u { 6 | return rotate_3u(pcg_3u_3u(index)); 7 | } 8 | -------------------------------------------------------------------------------- /examples/wesl-consumer/src/shaders/util.wesl: -------------------------------------------------------------------------------- 1 | 2 | fn rotate_3u(input: vec3u) -> vec3u { 3 | return vec3u(input.z, input.x, input.y); 4 | } 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | single_line_if_else_max_width = 0 2 | -------------------------------------------------------------------------------- /samples/binding0.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/binding1.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/eval_shadowing.wesl: -------------------------------------------------------------------------------- 1 | // cargo run -- eval "foo()" --file samples/eval_shadowing.wesl --no-strip --no-lower 2 | 3 | var x = 0; // even if this is not a const declaration... 4 | // const x = 0; // ...this should not be allowed, duplicate declaration 5 | 6 | override s: array; 7 | struct S { x: u32 } 8 | 9 | @const 10 | fn foo() -> i32 { 11 | return x; 12 | } 13 | -------------------------------------------------------------------------------- /samples/imp1.wgsl: -------------------------------------------------------------------------------- 1 | var baz: u32 = 10u; 2 | 3 | fn foo(x: vec3u) { 4 | baz += 1; 5 | } 6 | 7 | 8 | fn fib(n: u32) -> u32 { 9 | var a: u32 = 0; 10 | var b: u32 = 1; 11 | for (var i: u32 = 2; i <= n; i = i + 1) { 12 | b = a + b; 13 | a = b - a; 14 | } 15 | return b; 16 | } 17 | 18 | fn bar() -> bool { 19 | let x = 9 == 9u + baz; 20 | // baz += 1; 21 | return x; 22 | } 23 | -------------------------------------------------------------------------------- /samples/imp2.wgsl: -------------------------------------------------------------------------------- 1 | @if(false && hello) 2 | import imp3/f3; 3 | 4 | fn f2() -> vec4f { 5 | return vec4f(1.0, 0.0, 0.0, 0.0); 6 | } 7 | -------------------------------------------------------------------------------- /samples/main.wgsl: -------------------------------------------------------------------------------- 1 | struct StandardMaterialX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX { 2 | base_color: vec4, 3 | emissive: vec4, 4 | attenuation_color: vec4, 5 | uv_transform: mat3x3, 6 | reflectance: vec3, 7 | perceptual_roughness: f32, 8 | metallic: f32, 9 | diffuse_transmission: f32, 10 | specular_transmission: f32, 11 | thickness: f32, 12 | ior: f32, 13 | attenuation_distance: f32, 14 | clearcoat: f32, 15 | clearcoat_perceptual_roughness: f32, 16 | anisotropy_strength: f32, 17 | anisotropy_rotation: vec2, 18 | flags: u32, 19 | alpha_cutoff: f32, 20 | parallax_depth_scale: f32, 21 | max_parallax_layer_count: f32, 22 | lightmap_exposure: f32, 23 | max_relief_mapping_search_steps: u32, 24 | deferred_lighting_pass_id: u32, 25 | } 26 | 27 | const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUEX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX: u32 = 0u; 28 | 29 | fn standard_material_newX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX() -> StandardMaterialX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX { 30 | var material: StandardMaterialX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX; 31 | 32 | material.base_color = vec4(1f, 1f, 1f, 1f); 33 | material.emissive = vec4(0f, 0f, 0f, 1f); 34 | // material.perceptual_roughness = 0.5f; 35 | // material.metallic = 0f; 36 | // material.reflectance = vec3(0.5f); 37 | // material.diffuse_transmission = 0f; 38 | // material.specular_transmission = 0f; 39 | // material.thickness = 0f; 40 | // material.ior = 1.5f; 41 | // material.attenuation_distance = 1f; 42 | // material.attenuation_color = vec4(1f, 1f, 1f, 1f); 43 | // material.clearcoat = 0f; 44 | // material.clearcoat_perceptual_roughness = 0f; 45 | // material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUEX_naga_oil_mod_XMJSXM6K7OBRHEOR2OBRHEX3UPFYGK4YX; 46 | // material.alpha_cutoff = 0.5f; 47 | // material.parallax_depth_scale = 0.1f; 48 | // material.max_parallax_layer_count = 16f; 49 | // material.max_relief_mapping_search_steps = 5u; 50 | // material.deferred_lighting_pass_id = 1u; 51 | // material.uv_transform = mat3x3(vec3(1f, 0f, 0f), vec3(0f, 1f, 0f), vec3(0f, 0f, 1f)); 52 | let _e66 = material; 53 | return _e66; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | indent_string = " " 3 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | ".git/", 4 | "crates/wesl-test/webgpu-samples/LICENSE.txt", 5 | "crates/wesl-test/", 6 | ] 7 | ignore-hidden = false 8 | 9 | # Corrections take the form of a key/value pair. 10 | # The key is the incorrect word and the value is the correct word. 11 | # If the key and value are the same, the word is treated as always correct. 12 | # If the value is an empty string, the word is treated as always incorrect. 13 | 14 | # Match Whole Word - Case Sensitive 15 | [default.extend-identifiers] 16 | 17 | # Match Inside a Word - Case Insensitive 18 | [default.extend-words] 19 | 20 | [default] 21 | locale = "en-us" 22 | extend-ignore-re = [ 23 | "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", 24 | "(?Rm)^.*$", 25 | # consider raw strings to have correct contents 26 | "r\"[^\"]*\"", 27 | # consider naga_oil function names to be correct 28 | "_mod_[\\dA-Z]+\\b", 29 | ] 30 | extend-ignore-identifiers-re = [] 31 | --------------------------------------------------------------------------------