Skip to content

Module parameters proposal, take 2#164

Draft
k2d222 wants to merge 8 commits into
mainfrom
k2d222-patch-1
Draft

Module parameters proposal, take 2#164
k2d222 wants to merge 8 commits into
mainfrom
k2d222-patch-1

Conversation

@k2d222
Copy link
Copy Markdown
Contributor

@k2d222 k2d222 commented Nov 13, 2025

supersedes #146.
closed #117.

Greatly simplified as commented in #146 (comment).

  • @param const create overridable module parameters
  • can be set only once, by the linker only (for now? what about packages? imports?)
  • can be used in @if (replaces conditional features)

I also added @elif and @else in the spec (#117)

Rendered

@k2d222 k2d222 requested review from mighdoll and stefnotch November 13, 2025 20:51
@stefnotch
Copy link
Copy Markdown
Collaborator

Should we make it less verbose by changing it to param instead of @param const? Or are there good reasons for keeping it as a const?

I took a quick look, and my initial comments are

We should also disallow the following cases, or have explicit rules for them

  • @if together with @param.
    • Bonus: What about param that is inside of a module, which is only conditionally imported?
  • Params that are inside of a function. fn foo() { param x = 3; }

Not a problem yet:

  • A param that does not have a default value, and is not exported.
  • A param that gets re-exported, and can thus be set via two names.

Copy link
Copy Markdown
Contributor

@mighdoll mighdoll left a comment

Choose a reason for hiding this comment

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

Perhaps we should also add numeric @param const values in this revision? even w/o using them in conditions, that gets us out of the nonstandard constants:: we're doing now..

@mighdoll
Copy link
Copy Markdown
Contributor

I think it might be worth making a table or a flow diagram about override vs. param vs. const...

rough table idea:

feature where allowed shaderModule @if statements array sizes
param module, fn before yes yes yes
override module only after no yes no

(later we'd a column if param is settable from import statements.)

rough diagram idea:
set params -> transpile -> shaderModule -> set overrides -> create pipeline

@mighdoll
Copy link
Copy Markdown
Contributor

I note that param isn't currently a reserved word in WGSL, which would make param syntax harder to adopt upstream..

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Nov 17, 2025

@stefnotch
I think I @param const because there is already a lot of declaration keywords – var, var<>, let, const, override and it's confusing. Also I would prefer if all consts could be used in @if, but the implementation cost of const eval doesn't justify it yet.

Thinking about it, I would like to amend the proposal: "all consts initialized with a simple literal can be used in @if, while @param just makes the const host-visible. Or even, we ditch @param and use @public instead.

@mighdoll
love the table and diagram! makes things clear right away.

Ok to add numeric params in this revision.

@mighdoll
Copy link
Copy Markdown
Contributor

Presumably, @param const is allowable in libraries? Lygia GLSL makes extensive use of #define'd constants that the user might override. e.g. #define GAMMA 2.2. I think that's naturally translated as @param const GAMMA = 2.2;.

And presumably, GAMMA isn't host visible unless the root app shader also does a publish.

@stefnotch
Copy link
Copy Markdown
Collaborator

Very relevant here: Section "4.2.6 Global Generic Type Parameters" from the Slang paper https://kilthub.cmu.edu/articles/thesis/Slang_A_Shader_Compilation_System_for_Extensible_Real-Time_Shading/16826602?file=31117213

I do encourage reading that section. A rough summary of it is:

Slang has generic entry points. They let you specialize a shader.

float3 entryPoint<M : IMaterial, L : ILightingEnv>(
  Camera camera,
  M material,
  L lights,
  SurfaceGeometry geometry)
{
  float3 viewDir = normalize(camera.P - geometry.P);
  M.Pattern bxdf = material.evalPattern(geometry);
  return lights.illuminate(bxdf, viewDir);
}

but there's a lot of existing HLSL code.

It is common practice in current HLSL and GLSL codebases to declare some of the parameters of a shader entry point at global scope, rather than as explicit parameters of an entry-point function

Some of it was hard to convert. So they added a mechanism for that.
That mechanism is essentially our param const, except it's for types instead of values.

type_param M : IMaterial;
type_param L : ILightingEnv;
ParameterBlock<M> gMaterial;
ParameterBlock<L> gLights;
ParameterBlock<Camera> gCamera;
float4 main(SurfaceGeometry geom)
{
  float3 viewDir = normalize(gCamera.P - geometry.P);
  M.BxDF bxdf = gMaterial.evalPattern(geometry);
  return gLights.illuminate(surface, viewDir);
}

Conceptually they behave as if the whole shader library is nested in a generic declaration, with the given parameters

And then they link to section 7.2.3 where they explain why the type_param design is worse. Based on their experience in shipping production shader codebases.

In practice, global generic type parameters have created many problems and should be avoided if possible.

The problem there is that one runs into "Confusing circular dependency scenarios" when one starts doing simple kinds of reflection.

To create a shader with type_param M, you have to supply a type. Often times, you'll want to use some type that is defined in your shaders. But, that type is not allowed to depend on M in any way. M doesn't exist yet after all.

The param const equivalent is

param const X: u32;
const Y = X + 1;

and then trying to read the value of Y via reflection. You first have to pass in the param const values to be able to create the shader.

In HLSL/Slang, this problem is much worse, since they have methods directly in their struct definitions. So here, I couldn't use reflection to read D until after I've passed in something for T.

type_param T;
struct D
{
  float doSomething()
  {
    T t; ...
  }
};

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 13, 2025

@stefnotch it took me a while to understanding but your slang feedback is interesting. It doesn't affect the current proposal, since param consts are currently not overridable from shader code. But it's something that we'll maybe want to do in the future 🤔

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 13, 2025

param const in dependencies seem to be trickier than I thought.

TL;DR: we need at least re-exports to finish this proposal. Possibly we need to allow overriding param consts from shader code too.

  1. from the root package, there is no fully-qualified path that can access deep dependencies declarations. And that's for a good reason.

    • example: if bevy depends on random which has a param SEED, you can access random::SEED from bevy, but from outside bevy, the fact that bevy depends on random should be an unobservable implementation detail.
    • so, the root package can't override SEED.
    • so, packages need a way to override param consts. (from wesl.toml?)
  2. packages need to be able to re-export param consts from their dependencies. to allow passing control of SEED outside of bevy.

    • or alternatively, bevy creates a new param const and overrides SEED to that. Chaining param const this way may become tedious. And we still need a syntax for that in the toml.
  3. dependency unification. We need to make sure that unified dependencies override params at most once. And we must avoid "action at a distance".

    • in rust, features unification is easy, because features are just bool: it is enabled if any dependent package enables the feature. But how to unify if one sets SEED to 3 and the other to 5?
    • or do we always duplicate dependencies with different overrides? If I have two random with a different SEED override in the deps graph, do I get two versions of the random package?

@mighdoll
Copy link
Copy Markdown
Contributor

  1. packages need to be able to re-export param consts from their dependencies. to allow passing control of SEED outside of bevy.

I agree that packages will need a way to control what's published to the host from their imported libraries. I'd thought perhaps publish (#65) would do this. WDYT? I like that better than trying to control from wesl.toml. (I suppose param const works just not for libraries w/o this, but probably makes sense to do both together)

Possibly we need to allow overriding param consts from shader code too.

I think we had earlier discussed having the ability to allow importers to re-set const values. I agree that we want app shaders to have that ability to set param const values from library declared param consts. Potentially we could use that same syntax to allow alternate root shaders in the app package to set param consts delared on another app package module.

  1. from the root package, there is no fully-qualified path that can access deep dependencies declarations.

Do we need to expose module paths in the host interface to address param consts at all? We could require that param consts be published by the app root module, making them visible w/o any module path addressing. It seems clean to have the host api clearly controlled by the app root module. (We could extend that later if we want to have module path addressing)

But how to unify if one sets SEED to 3 and the other to 5?

How 'bout if we start by declaring this an error. We can consider relaxing that restriction later (without causing any breakage if an error case now becomes a new feature).

Confusing circular dependency scenarios" when one starts doing simple kinds of reflection.

param const X: u32;
const Y = X + 1;

In this case perhaps we'd reflect Y as a const of type u32 with a value of 'runtime-injected'. I note that we already have a variant of this problem due to conditions, for the same reason. For sure, flexible runtime control makes reflection more complicated.

@stefnotch
Copy link
Copy Markdown
Collaborator

stefnotch commented Dec 19, 2025

In this case perhaps we'd reflect Y as a const of type u32 with a value of 'runtime-injected'. I note that we already have a variant of this problem due to conditions, for the same reason. For sure, flexible runtime control makes reflection more complicated.

That absolutely does work, even if it has a few annoying implications. (Using a param const is a breaking change from a semver perspective despite the const having the same type, ...) It's probably also the best we can do, given the current design of param const.

If however, we went back to what existing programming languages have, we don't have such problems.

  • ML-style Module constructors: Syntax and semantics for using it from a shader becomes obvious, none of that "setting it twice is an error", reflection becomes clearer since constants inside of a module constructor clearly don't have a value. Meanwhile constants from a constructed module can be reflected.
  • Generics: WGSL has const generics. Nearly all cases where overrides are not allowed are in datatypes, which makes generics a quite natural tool to use. Similar upsides in terms of everything having well defined semantics.

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 19, 2025

For this example @stefnotch:

param const X: u32;
const Y = X + 1;

I don't see how that's an issue. If Y is part of a host-visible interface, any change to its value, or type, is a breaking change. So just changing just the value of X is also a breaking change for users of Y.

I guess if you have an array<u32, Y> and by reflection you can no longer compute the size of the array in host code, it's arguably a bigger change than just changing the size of the array. But is that not predictable and intended behavior? The programmer chose to make Y depend on X.

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 19, 2025

re @mighdoll

But how to unify if one sets SEED to 3 and the other to 5?

How 'bout if we start by declaring this an error. We can consider relaxing that restriction later (without causing any breakage if an error case now becomes a new feature).

I'm not super satisfied with the error. I think duplicating the package might be a better solution. We haven't closed the question of dependency unification yet (#21). Apparently we settled on that a while ago in #21 (comment). Note, we only have to effectively duplicate packages, i.e. it should behave as if it was duplicated, but identical declarations can be unified to reduce code generated.
wesl-rs currently duplicates all dependendencies.


Do we need to expose module paths in the host interface to address param consts at all? We could require that param consts be published by the app root module, making them visible w/o any module path addressing.

yes, this is my preferred approach too. I can update the proposal to reflect that.

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 19, 2025

re @stefnotch

Do you think we should postpone this proposal until we settle on a design for generics? Maybe, a good generics design would fulfill most/all use-cases of conditional compilation. And cond-comp is error-prone and hard to reason about.

Param consts do look and feel like ML module constructors, and it was the design I had in mind in the original proposal (#34), but now there are 2 important differences:

  • (1) ML modules are first-class citizens, they can be passed around and even created at runtime.
  • (2) There can be only one "instance" of a param const / parameterized module with the current proposal.

@stefnotch
Copy link
Copy Markdown
Collaborator

I think we can defer it until we have some generics prototypes. Unless there are some pressing use-cases that this would unblock?

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 19, 2025

It would be nice to have it before bevy adoption of WESL. Because

  • this proposal is a breaking change for conditional compilation
  • bevy uses numeric conditions

Otherwise I'm happy to defer.

@mighdoll
Copy link
Copy Markdown
Contributor

Unless there are some pressing use-cases that this would unblock?

const injection is pretty valuable. It's used extensively in Lygia, and I think will be important for wgsl-test too (nearly ready!). Anecdotally, it's usually the first thing people ask about when I mention enhancing wgsl, and it was quite common when we looked at wgsl shaders on github.

Our current workaround is to use the constants:: virtual module, but that doesn't work for many cases. Host code has to get involved to provide the constants so basically it only works in cases where the shader author is willing to require host code changes to use the shader. You can wrap constants in conditions.. but then that gets complicated for the user of the shader.

So I think it would be valuable if we can land a param const design. Though not if we'd want to obsolete it a few months later. I don't want to fall into the trap of having to fully design all the things before we can make progress on any of them, but let's look ahead so we don't zig zag too much. I think when last we discussed, the sense was that param const a useful addition to typeclasses/context classes/generics, and conceptually compatible with ml style solutions (though syntax might change)..

@stefnotch
Copy link
Copy Markdown
Collaborator

Okay, if we need it, then let's see how to unblock this:

  • Reflection: Marked as runtime-injected.
  • ML modules: Syntax and semantics of param const may evolve, but we can find a way of making it evolve towards something more powerful with a reasonably clear migration path
  • Const generics: Might end up being preferred over param consts in many cases, but we should wait for user feedback. So having both is fine.
  • Root module: Param consts are host visible, so the root module controls them. When we add "publishing/public", then we'll extend the permitted usages from "only param consts in the root module are visible" to "root module and public items are visible"
  • Setting from shader code: Do we have users that immediately need this, or can we defer that can of worms?

@k2d222
Copy link
Copy Markdown
Contributor Author

k2d222 commented Dec 19, 2025

Root module: Param consts are host visible, so the root module controls them. When we add "publishing/public", then we'll extend the permitted usages from "only param consts in the root module are visible" to "root module and public items are visible"

Does that mean that param consts in non-root modules are effectively consts for the time being? Should we just forbid them?

Setting from shader code: Do we have users that immediately need this, or can we defer that can of worms?

can defer I believe.

@mighdoll
Copy link
Copy Markdown
Contributor

Makes sense to me to do it in 3 steps.

  1. (this PR) start to unify conditions/consts, replaces non-standard constants:: virtual module, allows app shader to set conditions
  2. future publish or alt PR) enables injectable constants in libraries, host set only
  3. future import with or alt PR) enables setting constants from shader code

Should we just forbid them [non-root module param consts]?

I think so, until we get to step 2.

For shader libraries like Lygia that use conditions, there's benefit in allowing the conditions to be set from shader code, not just host code as now. That only works if condition variables are global, across modules and packages. Condition variables are global now, so I guess we can stay with that for now and then tighten up scoping with publishing/public.

For wesl-test or wesl-play, having param consts for injectable values only at the root level is pretty ok. Those are intended to be short self contained shaders. So step 1 is still useful.

Setting from shader code: Do we have users that immediately need this

Libraries like Lygia will want injectable consts, both injecting from host code and from app shaders. Here's a Lygia example:

// Smaller = nicer blur, larger = faster
#define SAMPLEDOF_RAD_SCALE .5

I think we are pretty close on a step 2 design too, so hopefully we can get that in relatively soon. With step 3, the story will be nice and orthogonal: with conditions and consts unified, usable in apps and libraries, and settable from both host and shader code.

@mighdoll mighdoll mentioned this pull request May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

@else into spec

3 participants