Skip to content

feat: enable custom shader compilers#23204

Draft
videobitva wants to merge 25 commits intobevyengine:mainfrom
videobitva:compiler_plugin
Draft

feat: enable custom shader compilers#23204
videobitva wants to merge 25 commits intobevyengine:mainfrom
videobitva:compiler_plugin

Conversation

@videobitva
Copy link
Copy Markdown

@videobitva videobitva commented Mar 3, 2026

Objective

Pluggable shader compiler & import resolver

Fixes #8342

Solution

TLDR:

how it works on main
---
title: "shader compilation pipeline"
---
flowchart TD
    classDef asset fill:#fff2cc,stroke:#d6b656,color:#000
    classDef bevy fill:#dae8fc,stroke:#6c8ebf,color:#000
    classDef naga fill:#d5e8d4,stroke:#82b366,color:#000
    classDef decision fill:#f8cecc,stroke:#b85450,color:#000
    classDef lang fill:#e1d5e7,stroke:#9673a6,color:#000

    A["Shader Asset<br>(.wgsl / .glsl / .spv / .wesl)"]:::asset
    B["ShaderLoader"]:::bevy
    C["ShaderCache.set_shader()"]:::bevy
    D["<b>ShaderCache.get(id, shader_defs)</b>"]:::bevy
    E{"match shader.source"}:::decision

    A --> B --> C --> D --> E

    %% SpirV branch
    F["Source::SpirV<br><i>pass-through</i>"]:::lang
    E -- "SpirV" --> F

    %% WGSL / GLSL branch
    subgraph hardcoded ["Hardcoded in ShaderCache"]
        G["Source::Wgsl / Glsl"]:::lang
        H["naga_oil::Composer<br>add_composable_module()<br>make_naga_module()"]:::naga
        I["naga::Module"]:::naga
        G --> H --> I
    end
    E -- "Wgsl / Glsl" --> G

    %% WESL branch
    J["Source::Wesl<br>wesl::compile()<br><i>inline in ShaderCache</i>"]:::lang
    E -- "Wesl" --> J

    %% decoupled_naga decision
    K{"decoupled_naga?"}:::decision
    I --> K

    L["WGSL text<br>naga::back::wgsl"]:::naga
    M["Naga IR<br><i>direct</i>"]:::naga
    K -- "yes" --> L
    K -- "no" --> M

    %% Converge to ShaderCacheSource
    N["<b>ShaderCacheSource</b><br>SpirV | Wgsl | Naga"]:::asset
    F --> N
    L --> N
    M --> N
    J --> N

    %% GPU path
    O["load_module()"]:::bevy
    P["wgpu::create_shader_module()"]:::naga
    Q["PipelineCache.process_queue()"]:::bevy
    R["device.create_render_pipeline()<br>"]:::naga

    N --> O --> P --> Q --> R
Loading
how it works in this pr
---
title: "shader compilation pipeline"
---
flowchart TD
    classDef asset fill:#fff2cc,stroke:#d6b656,color:#000
    classDef bevy fill:#dae8fc,stroke:#6c8ebf,color:#000
    classDef naga fill:#d5e8d4,stroke:#82b366,color:#000
    classDef dispatch fill:#f8cecc,stroke:#b85450,color:#000
    classDef plugin fill:#e1d5e7,stroke:#9673a6,color:#000,stroke-dasharray:5 5
    classDef trait fill:#f5f5f5,stroke:#0050ef,color:#000,stroke-width:2px

    A["Shader Asset<br>(.wgsl / .glsl / .spv / .wesl / .custom)"]:::asset
    B["ShaderLoader"]:::bevy
    C["ShaderCache.set_shader()"]:::bevy
    D["<b>ShaderCache.get(id, shader_defs)</b>"]:::bevy
    E["Lookup compiler by ShaderLanguage<br><i>HashMap&lt;ShaderLanguage, Box&lt;dyn ShaderCompiler&gt;&gt;</i>"]:::dispatch

    A --> B --> C --> D --> E

    %% Compiler implementations
    subgraph trait_boundary ["trait ShaderCompiler"]
        direction TB

        subgraph iface [" "]
            direction LR
            T1["fn add_import"]:::trait
            T2["fn remove_import"]:::trait
            T3["fn compile -> Result&lt;CompiledShader&gt;"]:::trait
        end

        F["<b>NagaOilCompiler</b><br><i>Wgsl + Glsl</i><br>───────────────<br>owns naga_oil::Composer<br>add_import -> add_composable_module<br>compile -> make_naga_module()"]:::naga
        G["<b>WeslCompiler</b><br><i>Wesl</i><br>───────────────<br>stores sources by path<br>compile -> wesl::compile()<br>returns WGSL text"]:::naga
        H["<b>SpirVPassthrough Compiler</b><br><i>SpirV</i><br>───────────────<br>compile -> pass bytes through"]:::naga
        I["<b>Custom Compiler</b><br><i>Custom&lpar;&amp;str&rpar;</i><br>───────────────<br>compile -> ???<br><i>user-defined logic</i>"]:::plugin
    end

    E --> F
    E --> G
    E --> H
    E --> I

    %% CompiledShader
    J["<b>CompiledShader</b><br>SpirV | Wgsl | Naga"]:::asset
    F --> J
    G --> J
    H --> J
    I --> J

    %% GPU path
    K["load_module()"]:::bevy
    L["wgpu::create_shader_module()"]:::naga
    M["PipelineCache.process_queue()"]:::bevy
    N["device.create_render_pipeline()<br>"]:::naga

    J --> K --> L --> M --> N

    %% Registration API note
    R["<b>Registration API</b><br>pipeline_cache.register_shader_compiler(<br>  ShaderLanguage::Custom, MyCompiler<br>)"]:::asset

    style trait_boundary fill:none,stroke:#0050ef,stroke-width:2px,stroke-dasharray:5 5
    style iface fill:none,stroke:none

Loading

Introduces pluggable ShaderCompiler trait in bevy_shader, registered per ShaderLanguage via PipelineCache::register_shader_compiler.

The existing WGSL, GLSL, WESL, SPIR-V, and paths are refactored into default trait implementations (NagaCompiler, WeslCompiler, SpirVPassthroughCompiler) with no behavioral changes.

A new ShaderLanguage::Custom variant and Shader::from_custom сonstructor allow plugins to define new shader languages.

An end-to-end stub example demonstrates the full workflow.

Testing

.. was done on Windows 10 / Vulkan and macOS M4 / Metal.

  • run the shader_material_hlsl example this one has been cut from the pr for reasons, a separate repo with this example is in progress
  • run shader_material_wesl, shader_material_glsl, and shader_material examples to verify existing paths

Showcase

See the stub example in examples/shader/custom_shader_compiler.rs. The pattern is:

Click to view showcase
  1. Implement ShaderCompiler for your language:
struct Compiler;

impl ShaderCompiler for Compiler{
    fn compile(
        &mut self,
        shader: &Shader,
        _shader_defs: &[ShaderDefVal],
    ) -> Result<CompiledShader, ShaderCompileError> {
        // compile with, for example, shaderc
        Ok(CompiledShader::SpirV(vec![]))
    }
}
  1. Register it with the PipelineCache:
impl Plugin for CustomShaderPlugin {
    fn finish(&self, app: &mut App) {
        let render_app = app.sub_app_mut(RenderApp);
        let mut pipeline_cache = render_app.world_mut().resource_mut::<PipelineCache>();
        pipeline_cache.register_shader_compiler(ShaderLanguage::Custom("custom"), Compiler);
    }
}
  1. Use it in a Material:
impl Material for CustomMaterial {
    fn vertex_shader() -> ShaderRef { "shaders/my_shader.vert".into() }
    fn fragment_shader() -> ShaderRef { "shaders/my_shader.frag".into() }
}

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 3, 2026

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen A-Assets Load files from disk to use for things like images, models, and sounds S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Shaders This code uses GPU shader languages labels Mar 4, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Mar 4, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Assets Mar 4, 2026
@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Mar 4, 2026
Comment thread Cargo.toml Outdated
[dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.19.0-dev", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
shaderc = { version = "0.10", optional = true }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this shouldn't be added as a top level dependency

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@mockersf resolved in c2cb526

Comment thread Cargo.toml Outdated
shader_format_wesl = ["bevy_internal/shader_format_wesl"]

# Enable the shaderc compiler (used by the shader_material_hlsl example)
shader_compiler_shaderc = ["dep:shaderc"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this isn't a feature of Bevy

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this isn't a feature of Bevy

yeah, but otherwise i don't know how to add this dependency as an optional, so that it is to be compiled only for this specific example...

Copy link
Copy Markdown
Author

@videobitva videobitva Mar 5, 2026

Choose a reason for hiding this comment

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

hmm, can probably move the shaderc dep to dev-dependecies with bring-your-own shaderc lib feature flag. this way it won't be much of a burden for running other examples, though for the one I added the user will need to have shaderc installed on their system

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@mockersf resolved in c2cb526

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

re-resolved in efcae67

use shaderc-dyn (lightweight interface dyn-loading shaderc) insetad of
heavy shaderc (which compiles/links to shaderc c++ lib).

move the shaderc/shaderc-dyn dependency from bevy-level to
dev-dependencies.
Comment thread Cargo.toml Outdated
serde_json = "1.0.140"
bytemuck = "1"
# Needed for shader_material_hlsl example
shaderc-dyn = "0.10.1"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer to not compile this by default for all examples. Its a small dep, and libloading does insulate us from non-existence of the library. But it is a dependency managed by a single person who is a new contributor to the bevy project, and for a number of reasons (trust, attack surface area, build breakage, increased dependency counts, etc) I think making this a dependency that every bevy developer consumes would be a mistake.

Additionally, including the HLSL shader example alongside our others implies a degree of support that we are not ready for.

I'd prefer it if this was reframed as a "custom shader compiler" example with a stub implementation, and a link to a repo that contains the full HLSL impl with this dependency.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i've though a bit before about just pointing to a crate with example impl, but did not know whether this is fine for you or not. from your response it seems that this is ok, so will do that 👍

1. merge import resolver and shader compilers trait into one, as it made
   no sense in the end to keep them separated
2. remove intermediate types representing shader state
3. consistent feature flags for different shading langs
4. a bit better public interface for registering compilers
Comment on lines +111 to +115
let render_app = app.sub_app_mut(bevy::render::RenderApp);
let mut pipeline_cache = render_app.world_mut().resource_mut::<PipelineCache>();

pipeline_cache
.register_shader_compiler(ShaderLanguage::Custom("custom"), CustomShaderCompiler);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i dont really like how the compiler registration looks rn, would be nice for it to be done the same way the AssetLoader does it, i.e. just a method on App. but need to look into how to do that

Utf8(#[from] std::string::FromUtf8Error),
}

impl AssetLoader for CustomShaderLoader {
Copy link
Copy Markdown
Author

@videobitva videobitva Mar 6, 2026

Choose a reason for hiding this comment

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

for any non-bevy-native shader lang you'll need to impl an asset loader. hypothetically this can be bypassed by integrating the info about a shader into the ShaderCompiler trait and next somehow pass it through to the already existing ShaderLoader impl

Comment on lines +164 to +176
compilers.insert(ShaderLanguage::Wgsl, naga_oil.clone());
#[cfg(feature = "shader_format_glsl")]
compilers.insert(ShaderLanguage::Glsl, naga_oil.clone());
#[cfg(feature = "shader_format_wesl")]
compilers.insert(
ShaderLanguage::Wesl,
Arc::new(Mutex::new(WeslCompiler::new())),
);
#[cfg(feature = "shader_format_spirv")]
compilers.insert(
ShaderLanguage::SpirV,
Arc::new(Mutex::new(SpirVPassthroughCompiler)),
);
Copy link
Copy Markdown
Author

@videobitva videobitva Mar 6, 2026

Choose a reason for hiding this comment

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

thinking about a possibility to move the default compilers registration out from the ShaderCache::new to a some more user-friendly place, so that adding new compilers/overriding existing could be more sound

(guess this is the same issue as in #23204 (comment))

@videobitva videobitva marked this pull request as draft March 12, 2026 02:04
@videobitva
Copy link
Copy Markdown
Author

ok, i kinda found a way to tackle the issues i've mentioned in the comments above, but it will take some time to refine the impl. so to not waste maintainers time and not distract them from other pull requests i'll mark this one as draft for now

@videobitva
Copy link
Copy Markdown
Author

note to myself: should probably ask what is the plan for wgsl to wesl migration, as with every change in this branch the bevy_shader itself and its interactions with bevy_render get refactored further and further

@ncthbrt
Copy link
Copy Markdown

ncthbrt commented Mar 17, 2026

Is there a way to add more information to materials or customise material compilation behaviour beyond just the path to the shader? For my experimental mew compiler for example, for generics you need to be able to specify the entry point to compile the shader from mew to wigs. For example: MySurface3d<f16>::vertex could be an entry point on mobile, and MySurface3d<f32>::vertex could be the entry point on desktop.

In a way it's part of specialisation, except the function exposed in the material for that is way too late in the pipeline for that to work correctly.

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

Labels

A-Assets Load files from disk to use for things like images, models, and sounds A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Shaders This code uses GPU shader languages S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Needs SME Triage
Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

Allow customization of shader compilation

5 participants