Uniform Buffers

Uniform Buffers Versus WebGL's uniform*() Calls

In WebGL 1 every uniform was a separate call on the current program (gl.uniform3fv(location, eye)), stored by the driver per program and lost when you switched programs. WebGL 2 added uniform blocks; WebGPU keeps only the block model and makes it explicit:

Uniforms in WebGL and WebGPU
Concern WebGL uniform*() WebGPU uniform buffer
Storage Hidden, per program A GPUBuffer you create
Update One call per value One writeBuffer() for a whole struct or many
Sharing Set again for each program One bind group used by many pipelines

The price is that you own the byte layout (Uniform Alignment); the payoff is fewer, larger updates: a camera written once for every pipeline, and per-object data in one large buffer addressed by offset (Dynamic Offsets) or instance index (Instanced Rendering).

One uniform buffer written once per frame and shared by three pipelines, where WebGL would repeat uniform calls per programHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
  .stage { position: relative; width: 100%; max-width: 600px; }
  .stage canvas { display: block; width: 100%; }
  .stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
  <canvas id="view" width="600" height="340"></canvas>
  <canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');

function showMessage(text) {                     // 2D fallback when WebGPU is missing
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
  ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}

// Every pipeline starts with the same frame struct at @group(0) @binding(0).
const FRAME = /* wgsl */ `
struct Frame { time: f32, sway: f32, light: vec2f }
@group(0) @binding(0) var<uniform> frame: Frame;
fn corner(v: u32) -> vec2f { return vec2f(f32(v & 1), f32(v >> 1)); }`;
const shaders = {
  books: FRAME + /* wgsl */ `
    struct Out { @builtin(position) pos: vec4f, @location(0) shade: f32, @location(1) @interpolate(flat) i: u32 }
    @vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
      let q = corner(v);
      let lean = sin(frame.time + f32(i)) * frame.sway * q.y;
      return Out(vec4f(-0.9 + f32(i) * 0.26 + q.x * 0.2 + lean, -0.02 + q.y * (0.55 + f32(i % 3) * 0.1), 0, 1), q.x, i);
    }
    @fragment fn fs(in: Out) -> @location(0) vec4f {
      let tints = array(vec3f(.12, .37, .55), vec3f(.36, .25, .60), vec3f(.88, .60, .06), vec3f(.25, .49, .23), vec3f(.71, .27, .18), vec3f(.16, .62, .56), vec3f(.5, .3, .2));
      return vec4f(tints[in.i] * (0.7 + 0.3 * mix(1 - in.shade, in.shade, frame.light.x)), 1);
    }`,
  plank: FRAME + /* wgsl */ `
    @vertex fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f {
      let q = corner(v);
      return vec4f(-0.97 + q.x * 1.94, -0.09 + q.y * 0.07, 0, 1);
    }
    @fragment fn fs() -> @location(0) vec4f { return vec4f(vec3f(0.55, 0.38, 0.22) * (0.8 + 0.2 * frame.light.x), 1); }`,
  lamp: FRAME + /* wgsl */ `
    @vertex fn vs(@builtin(vertex_index) v: u32) -> @builtin(position) vec4f {
      let q = corner(v) * 2 - 1;
      return vec4f(frame.light.x * 1.6 - 0.8 + q.x * 0.08, 0.8 + q.y * 0.14, 0, 1);
    }
    @fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.82, 0.3, 1); }`};

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  if (!adapter) return showMessage('WebGPU is not available in this browser');
  const device = await adapter.requestDevice();
  const context = canvas.getContext('webgpu');
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({ device, format });

  // One explicit layout for the shared group, so a single bind group fits all three pipelines.
  const frameLayout = device.createBindGroupLayout({ entries: [{ binding: 0,
    visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }] });
  const layout = device.createPipelineLayout({ bindGroupLayouts: [frameLayout] });
  const pipelines = Object.fromEntries(Object.entries(shaders).map(([name, code]) => {
    const module = device.createShaderModule({ code });
    return [name, device.createRenderPipeline({ layout, primitive: { topology: 'triangle-strip' },
      vertex: { module }, fragment: { module, targets: [{ format }] } })];
  }));
  const frameBuffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  const frameGroup = device.createBindGroup({ layout: frameLayout, entries: [{ binding: 0, resource: { buffer: frameBuffer } }] });
  const values = new Float32Array(4);

  function frame(now) {
    values.set([now / 700, 0.04, 0.5 + 0.5 * Math.sin(now / 1300), 0]);
    device.queue.writeBuffer(frameBuffer, 0, values);   // one write per frame, for every pipeline
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
      clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
    pass.setBindGroup(0, frameGroup);                   // bound once; survives setPipeline()
    pass.setPipeline(pipelines.lamp);  pass.draw(4);
    pass.setPipeline(pipelines.plank); pass.draw(4);
    pass.setPipeline(pipelines.books); pass.draw(4, 7);
    pass.end();
    device.queue.submit([encoder.finish()]);
    requestAnimationFrame(frame);
  }
  const rows = [['', 'WebGL uniform*()', 'WebGPU uniform buffer'], ['Storage', 'hidden, per program', 'a GPUBuffer you create'],
    ['Update', 'one call per value', 'one writeBuffer() per struct'], ['Sharing', 'set again per program', 'one bind group, many pipelines'],
    ['This frame', '3 programs x 3 values = 9 calls', '1 writeBuffer(), 1 setBindGroup()']];
  rows.forEach((r, i) => {
    ink.font = `${i === 0 ? 'bold ' : ''}12px system-ui, sans-serif`; ink.fillStyle = i === 4 ? '#1f4f8a' : '#222';
    ink.fillText(r[0], 12, 236 + i * 20); ink.fillText(r[1], 110, 236 + i * 20); ink.fillText(r[2], 330, 236 + i * 20);
  });
  requestAnimationFrame(frame);
}
main();
</script>