Frank Yellin
Frank Yellin

Reputation: 11297

Using override constants in WebGPU

In the WebGPU spec, both GPUVertexState and GPUFragmentState implement GPUProgrammableStage. The latter contains the constants field where one specifies the values of pipeline-overrideable constants.

That means that there are two different places within the GPURenderPipelineDescriptor where you can specify the pipeline-overrideable constants when creating a pipeline.
It is unclear how these two different locations interact. The specification makes it clear that one can override the value of a variable that is not statically used by that stage's entrypoint.

What does it mean if a constant is overridden with different values in each of the stages? Will anything called from the vertex stage see one value and anything called from the fragment stage see the other? (This seems wrong!). Does one value override the other?

Why was the constants field placed in GPUProgrammableStage rather than in GPURenderPipelineDescriptor and GPUComputePipelineDescriptor?

Upvotes: 0

Views: 141

Answers (1)

user128511
user128511

Reputation:

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>

Upvotes: 1

Related Questions