Skip to content

API Differences: WebGL vs WebGPU

Fresh 🌱

Canvas Handling

javascript
// WebGL: canvas creates the context, state attached to canvas
const gl = canvas.getContext('webgl2');

// WebGPU: device is independent; configure canvas separately
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const context = canvas.getContext('webgpu');
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() });

// WebGPU advantage: one device, MANY canvases
context1.configure({ device, format });
context2.configure({ device, format });
// No canvas limit in WebGPU (vs 16 in Chrome's WebGL)

Antialiasing

javascript
// WebGL: pass antialias as context attribute
const gl = canvas.getContext('webgl2', { antialias: true });

// WebGPU: manual MSAA texture + resolve
const msaa = device.createTexture({
  size: [canvas.width, canvas.height],
  sampleCount: 4,
  format,
  usage: GPUTextureUsage.RENDER_ATTACHMENT
});
// Render to msaa.createView(), resolve to canvas texture

Buffers and Textures: Immutability

javascript
// WebGL: mutable — can resize at any time
gl.bufferData(gl.ARRAY_BUFFER, newData, gl.DYNAMIC_DRAW);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, newWidth, newHeight, ...);

// WebGPU: IMMUTABLE after creation — cannot change size/format/usage
// To "resize": destroy old, create new
const newBuffer = device.createBuffer({ size: newSize, usage: ... });
device.queue.writeBuffer(newBuffer, 0, newData);

Error Handling

javascript
// WebGL: poll synchronously (pipeline stall)
gl.drawArrays(...);
if (gl.getError() !== gl.NO_ERROR) { /* handle */ }

// WebGPU: async scoped errors (no stalls)
device.pushErrorScope('validation'); // or 'out-of-memory', 'internal'
device.createBuffer({ /* bad params */ });
const error = await device.popErrorScope();
if (error) console.error(error.message); // Includes call stack!

// WebGPU also surfaces errors via device.onuncapturederror
device.addEventListener('uncapturederror', (event) => {
  console.error('Uncaptured:', event.error.message);
});

Object Labels

javascript
// WebGPU: every object can have a label for debugging
const buffer = device.createBuffer({
  label: 'MVP uniform buffer', // Shows in DevTools and error messages!
  size: 64,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

const encoder = device.createCommandEncoder({ label: 'Main frame encoder' });

Buffer Upload Patterns

javascript
// WebGL
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, data);

// WebGPU option A: writeBuffer (most common)
device.queue.writeBuffer(buffer, byteOffset, data);

// WebGPU option B: mapped at creation (for initialization)
const buffer = device.createBuffer({
  size: data.byteLength,
  usage: GPUBufferUsage.VERTEX,
  mappedAtCreation: true
});
new Float32Array(buffer.getMappedRange()).set(data);
buffer.unmap(); // must unmap before GPU can use it

Reading GPU Data Back to CPU

javascript
// WebGL: synchronous readPixels (stalls GPU)
const pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

// WebGPU: always async (copyBufferToBuffer + mapAsync)
const staging = device.createBuffer({
  size: bufferToRead.size,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});

const enc = device.createCommandEncoder();
enc.copyBufferToBuffer(bufferToRead, 0, staging, 0, staging.size);
device.queue.submit([enc.finish()]);

await staging.mapAsync(GPUMapMode.READ);
const data = new Float32Array(staging.getMappedRange().slice(0));
staging.unmap();

Based on Chrome for Developers WebGPU Documentation