You should consider each entry point entirely separate. The fact that you can put multiple stages or multiple entry points in the same shader module is just a convenience.
In other words
const module = device.createShaderModule({code: `
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0);
}
@fragment fn fs() -> @location(0) vec4f {
return vec4f(0);
}
`);
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
...
},
...
});
Is functionally no different than
const module1 = device.createShaderModule({code: `
@vertex fn vs() -> @builtin(position) vec4f {
return vec4f(0);
}
`);
const module2 = device.createShaderModule({code: `
@fragment fn fs() -> @location(0) vec4f {
return vec4f(0);
}
`);
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: module1,
},
fragment: {
module: module2,
...
},
...
});
With that in mind it should be clear that happens with constants. They are plugged in separately for each entry point, as though the other entry points didn't exist.
You can basically consider constants passed to createXXXPipeline similar to string substitutions. The shader module will have the constant set to the first value, substituted into the WGSL code string, that entry point will be compiled. Then the shader module will have the constant set to the 2nd value, substituted into the WGSL code string and compiled again. They are not actually string substitutions but they function similarly.
Example:
async function main() {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
device.addEventListener('uncapturederror', e => console.error(e.error.message));
const canvas = document.querySelector('canvas');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat(adapter);
const context = canvas.getContext('webgpu');
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({ code: `
override foo: f32;
struct V {
@builtin(position) p: vec4f,
@location(0) i: f32,
};
@vertex fn vs() -> V {
return V(vec4f(0, 0, 0, 1), foo);
}
@fragment fn fs(v: V) -> @location(0) vec4f {
return vec4f(v.i, foo, 0, 1);
}
`,
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
constants: { foo: 1 },
},
fragment: {
module,
constants: { foo: 0.5 },
targets: [
{format: presentationFormat },
],
},
primitive: { topology: 'point-list' },
});
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 1, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
});
passEncoder.setPipeline(pipeline);
passEncoder.draw(1);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
}
main();
body { background: #000; color: #fff };
<canvas width="1" height="1" style="width: 100px; height: 100px;"></canvas>
<li>The canvas above will be <span style="color:#F80">orange</span>
since foo is 1 in the vertex shader and 0.5 in the fragment shader. (correct)
<li>It would be <span style="color: #FF0">yellow</span> if it was 1 in both.
<li>It would be <span style="color: #880">brown</span> if it was 0.5 in both.</p>