Skip to content

Add native WGSL (genwgsl) shader generator with dedicated data library - #2996

Open
ashwinbhat wants to merge 64 commits into
AcademySoftwareFoundation:mainfrom
autodesk-forks:bhata/wgsl_generator_with_library
Open

ashwinbhat wants to merge 64 commits into
AcademySoftwareFoundation:mainfrom
autodesk-forks:bhata/wgsl_generator_with_library

Conversation

@ashwinbhat

@ashwinbhat ashwinbhat commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

The current glsl to wgsl pathway offered in MaterialX is not suitable for runtime systems.
There is an elaborate pipeline from MaterialX> GLSL -> SPIR-V -> WGSL -> [Browser]. The browser internally might convert WGSL -> SPIRV before creating byte code.

The transplied WGSL is verbose and difficult to read. The GLSL data structures are flattened sometimes due to the SPIR-V path.

This Introduces a C++ MaterialXGenWgsl backend with genwgsl shader target that has a a dedicated WGSL data library.

The WGSL data library is pre-transpiled using a helper script (glsl_to_wgsl.py) that uses Naga to create the shader nodes in WGSL data library

The MaterialX three.js viewer backend now uses WgslShaderGenerator. Unlike GLSLES, three.js does not support direct raw shaders. therefore we have an adapter to create Three.js TSL.

Note about AI use:
The script glsl_to_wgsl.py was authored using AI tools that uses naga and then performs cleanup of the shader code using simple lookup.
Screenshots:
chess-set
materialx-webgpu-collage

WebGPU viewer is host here: https://ashwinbhat.github.io/MaterialX/index-webgpu.html

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for this substantial contribution, @ashwinbhat, and indeed this seems more promising than your previous #2983.

Before moving forward with detailed review, I think it's worthwhile to consider a simplification of this idea:

What if the GLSL-to-WGSL transformation step occurred only in GitHub CI, rather than on the developer's machine? The machine-generated WGSL files would then become a derived artifact rather than a maintained set of code in the repository. The source GLSL files would remain the single source of truth for hardware shading, CI would run the transpiler on each PR and package the results into our build artifacts, and developers not working with WGSL could rely upon CI to validate that they haven't broken the WGSL target with their changes. Since the generated files would no longer be committed, drift between the two libraries becomes impossible by construction, rather than managed through community conventions.

On the build side, we might then default MATERIALX_BUILD_GEN_WGSL to OFF, with GitHub CI providing Naga and setting it to ON, so that ordinary source builds require no Python or Rust toolchain. The hand-written portion of the library (the lib/ helpers, the image and light nodes, and the adapted BSDFs) would remain in the repository as maintained code, analogous to the small libraries of hand-written code in MSL and Slang today.

This direction would preserve what I see as the core contributions of this PR -- the native HwShaderGenerator-derived backend, the resource binding model, and the WebGPU viewer integration -- while shifting the transpiled library from maintained code to build infrastructure. Would you be open to exploring this path?

@ashwinbhat

Copy link
Copy Markdown
Contributor Author

Thanks for this substantial contribution, @ashwinbhat, and indeed this seems more promising than your previous #2983.

Before moving forward with detailed review, I think it's worthwhile to consider a simplification of this idea:

What if the GLSL-to-WGSL transformation step occurred only in GitHub CI, rather than on the developer's machine? The machine-generated WGSL files would then become a derived artifact rather than a maintained set of code in the repository. The source GLSL files would remain the single source of truth for hardware shading, CI would run the transpiler on each PR and package the results into our build artifacts, and developers not working with WGSL could rely upon CI to validate that they haven't broken the WGSL target with their changes. Since the generated files would no longer be committed, drift between the two libraries becomes impossible by construction, rather than managed through community conventions.

On the build side, we might then default MATERIALX_BUILD_GEN_WGSL to OFF, with GitHub CI providing Naga and setting it to ON, so that ordinary source builds require no Python or Rust toolchain. The hand-written portion of the library (the lib/ helpers, the image and light nodes, and the adapted BSDFs) would remain in the repository as maintained code, analogous to the small libraries of hand-written code in MSL and Slang today.

This direction would preserve what I see as the core contributions of this PR -- the native HwShaderGenerator-derived backend, the resource binding model, and the WebGPU viewer integration -- while shifting the transpiled library from maintained code to build infrastructure. Would you be open to exploring this path?

Thanks @jstone-lucasfilm for the valuable feedback. I've removed the generated wgsl files and moved the generation to GitHub CI.

The MaterialX WebGPU viewer is hosted here: https://ashwinbhat.github.io/MaterialX/index-webgpu.html

@kwokcb kwokcb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow all the remapping from GLSL to WGSL but assume it's good.

For building:

  • It would still be useful to have a script or set of instructions on how to build locally if you want to change and test anything. e.g. if I modify or add a GLSL file how do you rebuild it to test.

For release a few questions

  • There is a comment about switching between web pages
    • Curious why are there 2 web pages -- seems like it's the simplest way to webpack ?
  • Are there any issues with instantiating both a WebGL or WebGPU generators at the same time if desired on web ?
  • Are all the generated WGSL files installed (visible to look at) in the release ?

* Configure genContext for WebGPU material generation.
* @returns {boolean} Whether the surface is transparent.
*/
function configureWebGPUGenContext(mx, gen, genContext, elem)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is getting large and hard to read. Would it make sense to have a separate file for WebGPU specific functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move it into a separate file as a future refactoring step

// Rendering backend, selected at build time per bundle (see webpack.config.js). The WebGL
// bundle uses classic THREE.WebGLRenderer + RawShaderMaterial (ESSL); the WebGPU bundle
// aliases `three` to three/webgpu and uses WebGPURenderer + NodeMaterial (WGSL via the
// upstream WgslShaderGenerator). A toggle switches between the two HTML pages.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why 2 web pages ? Does it make sense to instead do a run-time switch, or allow for 2 different canvases with different associated renderers ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out how to get the UX and other elements to work seamlessly. We can improve this in future if the core generator is approved.

python python/Scripts/generateshader.py resources/Materials/Examples --target glsl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe
python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target essl --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe
python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target vulkan --validator C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe
python python/Scripts/generateshader.py resources/Materials/Examples/StandardSurface --target wgsl --validator "C:/vcpkg/installed/x64-windows-release/tools/glslang/glslangValidator.exe --target-env vulkan1.3 --quiet"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removed ? If wgsl is available should it not be run still.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current validator uses glsllang that does not support wgsl syntax.
Note that the current generator was generating "glsl" code but the new one will generate "wgsl" code

- name: Create Archive Name
run: echo "MATERIALX_ARCHIVE=MaterialX-${RELEASE_TAG//v}" >> $GITHUB_ENV

- name: Install Python

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if it's possible to call into a shared setup from main and release ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a config that is no Python based, so I kept it separate.

@ashwinbhat

ashwinbhat commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor Author

I don't follow all the remapping from GLSL to WGSL but assume it's good.

For building:

  • It would still be useful to have a script or set of instructions on how to build locally if you want to change and test anything. e.g. if I modify or add a GLSL file how do you rebuild it to test.

I'll a document in the developer docs that adds steps/instructions and details about the remapping.

  • Are there any issues with instantiating both a WebGL or WebGPU generators at the same time if desired on web ?
  • Curious why are there 2 web pages -- seems like it's the simplest way to webpack ?
    WebGL and WebGPU are separate shader generators so it should work. The reason for separate pages is because the workflows are different

WebGL : THREE.WebGLRenderer + RawShaderMaterial (ESSL)
WebGPU :THREE.WebGPURenderer + TSL/NodeMaterial (WGSL)

  • Are all the generated WGSL files installed (visible to look at) in the release ?

Yes they should be part of zip, there should be a way to verify this.

@ashwinbhat

Copy link
Copy Markdown
Contributor Author

@kwokcb I added a developer doc which will be helpful to answer some of your question about local developer workflow

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for turning this around so quickly, @ashwinbhat. Moving the node fragments to CI generation is exactly the direction I was hoping for, and the WebGPU viewer looks great.

One remaining piece I'd like to resolve before diving into detailed review: the BSDF microfacet library still lands in the repository as a committed, hand-written WGSL copy of genglsl/lib/mx_microfacet*.glsl. This hardware shading library is arguably one of the most critical parts of the MaterialX project, and I'd rather not have two references that readers must navigate and keep in agreement.

The goal I'd propose is that the GLSL microfacet library remains the single source of truth, and the WGSL form is a CI-generated artifact that is never committed -- the same treatment the WGSL node fragments now get. From reading glsl_to_wgsl.py, this looks within reach: the main obstacle (WGSL's lack of overloading) is already solved for call sites via CALL_MAP, and the genuinely WGSL-specific residue in the lib is small.

Here's a proposed set of next steps to consider:

  1. Extend the transpiler to also generate the lib/ helpers, so nothing under genwgsl/lib/ is committed.
  2. Upstream any genuine divergences into the GLSL source, e.g. mx_orthonormal_basis has already drifted (the sign branch moved to fix a tangent-frame artifact), and that fix currently lives only in the WGSL copy.

It's worth noting that generation from GLSL makes drift like the mx_orthonormal_basis case impossible by construction. In the longer term I'd see render-comparison coverage for the WGSL target (building on our nightly MSL/OSL comparisons) as a natural validation gate, though that's future context rather than a request for this PR.

Does this seem like a reasonable path to you?

@ashwinbhat

ashwinbhat commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for turning this around so quickly, @ashwinbhat. Moving the node fragments to CI generation is exactly the direction I was hoping for, and the WebGPU viewer looks great.

One remaining piece I'd like to resolve before diving into detailed review: the BSDF microfacet library still lands in the repository as a committed, hand-written WGSL copy of genglsl/lib/mx_microfacet*.glsl. This hardware shading library is arguably one of the most critical parts of the MaterialX project, and I'd rather not have two references that readers must navigate and keep in agreement.

The goal I'd propose is that the GLSL microfacet library remains the single source of truth, and the WGSL form is a CI-generated artifact that is never committed -- the same treatment the WGSL node fragments now get. From reading glsl_to_wgsl.py, this looks within reach: the main obstacle (WGSL's lack of overloading) is already solved for call sites via CALL_MAP, and the genuinely WGSL-specific residue in the lib is small.

Here's a proposed set of next steps to consider:

  1. Extend the transpiler to also generate the lib/ helpers, so nothing under genwgsl/lib/ is committed.
  2. Upstream any genuine divergences into the GLSL source, e.g. mx_orthonormal_basis has already drifted (the sign branch moved to fix a tangent-frame artifact), and that fix currently lives only in the WGSL copy.

It's worth noting that generation from GLSL makes drift like the mx_orthonormal_basis case impossible by construction. In the longer term I'd see render-comparison coverage for the WGSL target (building on our nightly MSL/OSL comparisons) as a natural validation gate, though that's future context rather than a request for this PR.

Does this seem like a reasonable path to you?

hi @jstone-lucasfilm it seems reasonable but from my findings, lib/ helpers are very difficult to transpile using naga as they are missing a lot of context and other data structures that is emitted by shader generators.

I agree it makes sense update the mx_orthonormal_basis

It might be more valuable to have wgsl specific implementations of some microfacet library functions that are better authored for Web instead of porting the glsl versions as-is. Naga does create very verbose shaders that increases the overall source length.

@ashwinbhat

Copy link
Copy Markdown
Contributor Author

Hi @jstone-lucasfilm
Based on your suggestion, I have modified the WGSL "pre-transpile" pipeline. The subset of libraries for WGSL now are math and fragments that need texture support.
The transpile is done via naga. Naga, produces "correct" but very verbose, hard to read WGSL due to static single assignment (SSA) style code with temporaries, hoisted var declarations, redundant nested blocks, mangled identifier names.
I've introduced a helper that "cleans-up" the naga transpiled code by understanding the WGSL grammar. It doesn't regenerate code, only deletes/renames/reindents etc.
Let me know if this are suitable changes and towards the direction you envision.

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for turning this around, @ashwinbhat. Generating the microfacet library from genglsl is exactly the direction I was hoping for, and the cleanup pass makes the output far more readable than raw Naga output.

Two issues I'd like to resolve before diving into detailed review:

The first is the hand-written library remaining under stdlib/genwgsl/lib, still around 1,200 lines with mx_noise alone at 716. The skip list justifies these as "whole dir maintained by hand (was HANDWRITTEN_LIB_DIRS)", which reads as a record of the tool's earlier state rather than a technical constraint, and differs in character from the pbrlib and node entries above it. Looking through the sources, only mx_math appears genuinely blocked and only in part -- the mx_mod overloads and mx_isinf need hand-writing, while mx_matrix_mul, mx_square and mx_srgb_encode are ordinary GLSL, so perhaps an mx_math_platform.wgsl split mirroring mx_shadow_platform. mx_noise, mx_flake, mx_hsv and mx_geometry have no samplers, no $-tokens and no derivatives, and mxgenwgsl.py already carries a LIB_USE_SIBLING_BODIES entry for mx_flake and mx_noise that transpileLibs can never reach, since isHandWrittenLib short-circuits first. mx_hextile I'd set aside for now. Its dFdx and dFdy calls are only a spelling difference -- MslShaderGenerator and SlangShaderGenerator already map those tokens, and we could do the same for WGSL or wrap them in mx_math alongside mx_mod and mx_isinf. But WGSL enforces uniform control flow for derivatives as a hard error rather than leaving it undefined, so that one deserves its own look.

Was the stdlib pass attempted after the lib refactor, or is this the original setting carried forward? I'd suggest dropping those four from skip_transpile.txt and running the tool: if they transpile, close to a thousand lines leave the repository, and if they don't, we learn the reason the skip list is currently missing. My concern is the one we discussed for the microfacet library -- mx_noise has 58 function definitions whose numerics must match genglsl exactly, and coverage matches today just as it did for the microfacet library right up until mx_orthonormal_basis drifted.

The second is mx_orthonormal_basis itself, where I think you've found a real bug that isn't WGSL-specific. mx_generate_prefilter_env builds its normal from the latlong projection, where N.z is cos(latitude) * cos(longitude). That crosses zero at longitude +/-90 degrees, so the N.z < 0 branch runs down two vertical lines of the environment map, with the tangent frame flipping across each one. That flip affects GLSL, ESSL, MSL and Slang today, and is latent only because FIS is our default environment method. Moving the branch to -0.9999999 hides it on the map's wrap seam rather than removing it, and it costs precision: the original sign choice exists to keep sign + N.z away from zero, and the new threshold lets it cancel in f32, so the basis stops being orthonormal near that seam. Since mx_generate_prefilter_env already has uv in hand, I'd suggest building the frame from the latlong parameterization itself -- a tangent along the longitude direction, with the bitangent from the cross product -- which is continuous across the whole map and fixes the seam for every hardware target at once.

Could we take that as its own PR against main, with before-and-after images? The same for the tf_thickness / tf_ior rename -- I'm not opposed to the names, but it's a change to a public data library and I'd rather it stood on its own than rode along as a transpiler simplification. Together those would let this PR stop modifying pbrlib/genglsl entirely.

@ashwinbhat

Copy link
Copy Markdown
Contributor Author

Thanks @jstone-lucasfilm for the detailed feedback on the stdlib genwgsl/lib helpers.

Experiment results — all four transpile cleanly from genglsl

Removed mx_noise, mx_flake, mx_hsv, and mx_geometry from skip_transpile.txt and ran mxgenwgsl.py. All four transpile with zero failures (naga 30.0.0). The sibling-body path for mx_noise/mx_flake works as intended now that isHandWrittenLib no longer short-circuits them.

Committed hand-written copies of those files (~1,000 lines) have been deleted; CI/local transpile regenerates them from genglsl.

mx_math split (mx_math_platform.wgsl)

Following your suggestion, WGSL-specific helpers now live in a small hand-written platform file:

  • mx_mod_* overloads (GLSL #define mx_mod mod has no WGSL equivalent)
  • mx_isinf (naga rejects GLSL isinf())
  • unsuffixed mx_matrix_mul(mat4x4f, vec4f) for Hw geometric nodes

The rest of mx_math.glsl (mx_matrix_mul overloads, mx_square, mx_srgb_encode) transpiles from genglsl; generated mx_math.wgsl #includes the platform file.

Also moved to generated

mx_transform_uv and mx_transform_uv_vflip (trivial one-liners) — same treatment.

Intentionally still hand-written in stdlib/lib

  • mx_hextile.wgsl — deferred (dFdx/dFdy uniform-control-flow; as discussed)
  • mx_math_platform.wgsl — platform gaps only (~45 lines)

Validation

  • Full mxgenwgsl.py run: 14 lib + 98 node files, 0 failures
  • test_mxwgslcleanup.py: all tests pass
  • JsMaterialXGenShader builds with MATERIALX_BUILD_GEN_WGSL=ON

Ready for detailed review. The mx_orthonormal_basis / tf_* items remain scoped for separate PRs as you suggested.

@ashwinbhat

ashwinbhat commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor Author

hi @jstone-lucasfilm PR 3023 tackles the mx_generate_prefilter_env issue. We don't need the tf_* renames, I had added them for clarity but can skip them.

Introduce a native WebGPU/WGSL shader generator as a new "genwgsl" target,
replacing the old GLSL-rewriting WgslShaderGenerator in MaterialXGenGlsl.

- MaterialXGenWgsl is HwShaderGenerator based generator that emits complete
  standalone WGSL so that there is no transpiling required.
- MATERIALX_BUILD_GEN_WGSL option to enable this project
- Add js and python binding.

The main addition is a helper python script that uses naga to convert glsl data library to wgsl data library. The script is not a general purpose converter.
It run in few phases
1. wraps a glsl fragment into a shader and uses naga to generate wgsl.
2. the generated wgsl is cleaned up to keep doc comments, and maintian similar code style as glsl
3. Add a banner to alert future authors to not edit the wgsl directly.
Note: only stright forward simple conversion is done. there are cases with glsl overloads in math and lib that have hand written wgsl counterparts.
see source\MaterialXGenWgsl\tools\glsl_to_wgsl.py and associated readme.md
Added genwgsl library what was transpiled offline.
Add an opt-in `MATERIALX_GENERATE_WGSL_LIBRARY` build option that regenerates and validates the library at build time.
This needs Cargo the official build tool and package manager for Rust.
MATERIALX_CARGO_PATH can be set to define path to Cargo.
See https://doc.rust-lang.org/cargo/getting-started/installation.html
Render MaterialX materials over WebGPU by using the native WgslShaderGenerator
The main change here to support Three.js wgslFn: WGSL function node, a TSL function.
https://threejs.org/docs/#FunctionNode

- mxtsladapter.js: converts the complete WGSL module to TSL-portable format
- wgslmanifest.js: reconstruct the reflection manifest in JS from the generated WGSL +
  Shader uniform ports
- viewer.js/index.js: dual WebGL (ESSL) + WebGPU (WGSL) backends selected per bundle;
  WebGPU path uses WebGPURenderer + the TSL bridge.
We might look into naga-cli
These will be generated as part of the CI.
Note: there are still a few wgsl files that are hand written since their glsl counterparts use overloads, defines and storage qualifiers
- Add generate_wgsl rule that installs naga-cli and updates data library with wgsl
TODO: check if cargo is availble on CI runners
@ashwinbhat ashwinbhat self-assigned this Sep 10, 2026
ashwinbhat and others added 8 commits September 11, 2026 10:04
These .wgsl files are not generated by mxgenwgsl.py
Update the base shader gen to support some tokens needed for WGSL and add helpers syntax that differ
- HwConstants: add closure data token
- HwSurfaceNode/WgslSurfaceNode: refactor surface node emission
- Syntax: add type mapping helpers
- WgslSyntax: add WGSL-specific syntax extensions
- WgslShaderGenerator: token substitution updates

Transpiler refactoring:
- Replace/refactor regex for texture rewrites and make it more data-driven
- Use dict caches with @functools.lru_cache to avoid globals
- Reorganize the script to move code in relavent sections
- Update skiplists
When tree-sitter-language-pack is unavailable (Python < 3.10),
mxgenwgsl.py skips readability cleanup.
Restore $texSamplerSignature GLSL tokens
@ashwinbhat

Copy link
Copy Markdown
Contributor Author

Hi @jstone-lucasfilm thanks for the valuable feedback. I've incorporated your feedback and ideas. Hopefully the new changes are keeping the changes closer and minimized. Let me know your thoughts.

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for the quick turnaround on the September items, @ashwinbhat, and this is looking very close to the mark. I've had a chance to read through the latest PR, and I have a few notes and suggestions:

  1. The most significant issue is that the texture restore rules currently discard the LOD and gradient arguments. In mxgenwgsl.py, TEXTURE_REWRITE_RULES maps textureLod(sampler, uv, lod) to mtlx_tex_lookup_rgb(uv, lod) and both textureGrad forms to mtlx_tex_lookup_rgb(uv, 0.0) or mtlx_tex_lookup_rgba(uv), and _TEX_LOOKUP_RULES then restores every placeholder to a plain textureSample, keeping only the coordinate. The textureLod call in mx_latlong_map_lookup is the only texture read in the environment path, so mx_environment_fis computes mx_latlong_compute_lod and then samples with implicit derivatives, and mx_latlong_alpha_to_lod in mx_environment_prefilter has no effect at all. The textureGrad calls in the two hextile nodes likewise lose the per-tile derivatives that the hex-tiling method depends on for continuous mip selection across tile boundaries. The hand-written files this round replaced used textureSampleLevel and textureSampleGrad, so this is a regression from the previous state rather than a pre-existing issue.

    The fix should be relatively small. I'd suggest giving the placeholders distinct names for the three GLSL forms, so that textureLod becomes a mtlx_tex_lookup_level placeholder carrying its LOD, textureGrad becomes a mtlx_tex_lookup_grad placeholder carrying both derivatives, and plain texture keeps the current form, with the restore rules emitting textureSampleLevel, textureSampleGrad, and textureSample respectively. Since the placeholders are ordinary GLSL functions in TEXTURE_EXPANSION_PREAMBLE, naga should carry the extra arguments through without any special handling, and a case in test_mxgenwgsl.py asserting that the arguments round-trip would keep this from recurring. The explicit-level form also sidesteps WGSL's uniform-control-flow requirement for textureSample, which is a nice property to have inside the FIS loop.

  2. On the binding model, WgslResourceBindingContext::emitResourceBindings emits each value uniform as its own var<uniform> binding, and with the complete shader interface that the viewer requests, a Standard Surface material produces on the order of fifty of them. WebGPU's default maxUniformBuffersPerShaderStage is 12, so the standalone shader that WgslShaderGenerator produces won't create a pipeline on a default device without the consumer restructuring it. The viewer handles this in mxtsladapter.js by removing the @group/@binding declarations and rebinding through TSL, and a good portion of that file and wgslmanifest.js exists to undo and then reconstruct the layout the generator emitted.

    The approach I'd suggest is to pack each uniform block into a single struct bound once, so that PublicUniforms and PrivateUniforms each become one var<uniform> of a generated struct type, with textures and samplers keeping their individual bindings. This mirrors what the Vulkan and MSL targets already do through emitStructuredResourceBindings, and I'd expect it to let the TSL adapter shrink considerably. I'd welcome your thoughts on whether this fits within the current PR or is better deferred to a follow-up, since the binding layout is what downstream WebGPU integrators will build against, and I'd like us to have a shared view of where it's headed before 1.39.6.

A few small cleanup notes:

  • The documentation has drifted from the latest code. WGSLShaderGeneration.md and both READMEs still reference EXPECTED_FALLBACK, which no longer exists in mxgenwgsl.py, and source/MaterialXGenWgsl/README.md still describes mx_chiang_hair_bsdf and the image nodes as hand-written. The tools README asks for naga v29+ while CI pins 30.0.0, and the comment on generatedBanner says the // @mxgenwgsl marker is stripped during shader assembly, though I don't see generator code that does so. The PR description would also be worth refreshing to describe the design that will actually get merged.
  • WgslEmittedFunctions in WgslShaderGenerator.cpp collects function names in emitBlock, but nothing reads them, so it looks like a leftover from an earlier deduplication approach.
  • test_mxgenwgsl.py isn't run in CI, which currently invokes only test_mxwgslcleanup.py, and it would be valuable to add this to the same step.

Let me know if this all sounds reasonable, and thanks again for your persistence on this one!

ashwinbhat and others added 5 commits September 21, 2026 09:31
- Pack uniforms into struct UBOs
    - Update wgslmanifest.js to parse struct-packed bindings and expand
  members into per-entry flat lists for the TSL adapter.
- preserve texture LOD/grad args
    - Add placeholder names so textureLod and textureGrad are restored after Naga
- add test_mxgenwgsl to CI
- remove dead code
…rm bindings with struct-packed UBOs (u_prv/u_pub),

This needs token substitutions for private-uniform struct access and port renaming for public-uniform struct access.
Should be done in base generator.
@ashwinbhat

Copy link
Copy Markdown
Contributor Author

Hi @jstone-lucasfilm thanks for your feedback and patience. One of the changes to wgsl shader generator emits structure based UBO binding.
As a follow-up I would like to propose a UBO based structured output for glsl as well. I'll put a separate proposal for this. Thank you.

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for this latest set of improvements, @ashwinbhat. The struct-packed uniform blocks, the LOD and gradient round-trip, and the consolidation of the hand-written library down to the three light shaders and mx_math_platform.wgsl all look right to me, and I think the architecture is now in a good place.

For this round I'd like to focus on a single theme, which is validation coverage. CI currently runs naga over the StandardSurface examples with default options, and the new WgslShaderGeneratorTester generates the TestSuite without compiling it. To see what lies outside that slice, I asked Claude to take the generated library from the latest CI build and compile the generator's output with wgpu-native's naga across the Examples, the TestSuite, and a handful of GenOptions settings. The default path is in good shape, with 131 of 132 Examples stages compiling, but the rest turned up a few issues:

  1. With hwSpecularEnvironmentMethod set to SPECULAR_ENVIRONMENT_PREFILTER, every fragment shader fails to compile. The generated mx_environment_prefilter.wgsl calls mx_latlong_map_lookup(L, $envMatrix, mx_latlong_alpha_to_lod(avgAlpha), $envRadiance) without its sampler argument, since the [^)]* pattern in patchWgslEnvLatlongCalls can't span the nested call. The FIS path is unaffected only because its LOD argument happens to be a plain identifier.

  2. With hwShadowMap or hwAmbientOcclusion enabled, every fragment shader fails as well. Those two branches of HwSurfaceNode still emit GLSL, passing a combined u_shadowMap to mx_shadow_occlusion and calling texture(u_ambOccMap, ambOccUv), so they'd need the same syntax hooks that the rest of the node now uses.

  3. In the TestSuite, 61 of 1,586 stages fail to compile under default options. The largest group is nodegraphs that read geometry outside of a closure, where WgslCompoundNode threads vd only into closure functions, and this is also the one Examples failure (the normal map graph in gltf_pbr_boombox). The remainder are smaller cases:

    • Integer outputs are written as vec4f(i32, i32, i32, 1.0) in toVec4Wgsl.
    • The matrix variants of the conditional nodes use select, which WGSL doesn't define for matrices.
    • stdlib_genwgsl_impl.mtlx references mx_inverse, which isn't defined in the library.
    • HwTransformNormalNode emits &out = normalize(out).
    • Boolean uniforms stored as u32 reach the inline logic nodes without the bool() conversion.
    • mx_blackbody.wgsl loses the file-scope XYZ_to_RGB constant, since the node pass emits function bodies only.
    • The boolean geompropvalue node references its uniform as u_geomprop_geompropvalue_bool, without the u_prv. qualifier of the struct it's now declared in.
  4. Two cases compile but silently diverge from GLSL, which is the category we've been working to rule out. TOKEN_EXPANSIONS maps $refractionTwoSided to false and never restores it, so the generated mx_surface_transmission contains if false { ... } and the u_refractionTwoSided uniform has no effect. Likewise LIB_PREAMBLE pins DIRECTIONAL_ALBEDO_METHOD and AIRY_FRESNEL_ITERATIONS at transpile time, so hwDirectionalAlbedoMethod and hwAiryFresnelIterations are ignored. I'd suggest restoring the first as a reversible token like the others, and for the second, either generating the variants or having the generator raise an error for settings that the WGSL library doesn't support.

  5. On packaging, the Python sdist and the Linux wheels from the latest CI run ship PyMaterialXGenWgsl with only the four hand-written .wgsl files, while the Windows and macOS wheels contain the full library. The sdist job does run the transpiler, so my guess is that the new .gitignore rule is excluding the generated files from the sdist, but it's worth confirming. Relatedly, the wheels that do carry the library ship the verbose form on every Python version, since the tree-sitter cleanup isn't installed in the wheel builds, and that form is roughly 11,800 lines against 4,500 with the cleanup applied. Which library a user receives therefore depends on the environment that built it, and I'd suggest making the cleanup either required or absent, so that there's a single generated artifact.

Since the items above all pass CI today, the fix I'd most like to see is to the gate itself: running naga over the shaders that WgslShaderGeneratorTester already generates, and over a small matrix of GenOptions for the StandardSurface examples. That would have caught items 1 through 3 directly, and it gives us a foundation for the render comparisons we discussed earlier in the thread.

I have some additional notes on the build integration and the viewer's TSL adapter, but I'll save those for a later round once these issues are resolved.

I'd be interested in your thoughts on this proposed path forward, and I appreciate the steady progress you've made on this PR!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants