graph TD
Nav["navigator.gpu\nEntry point — the Vulkan Instance equivalent"]
Adapter["GPUAdapter\nRepresents a physical GPU (or software rasterizer)\nRequested with powerPreference"]
Device["GPUDevice\nYour logical connection to the GPU\nAll objects are created from here"]
Queue["GPUQueue\nSubmit command buffers here\nAlways exists as device.queue"]
Nav -->|"requestAdapter()"| Adapter
Adapter -->|"requestDevice()"| Device
Device -->|"device.queue"| Queue
Initialization Code
async function initWebGPU() { // 1. Check browser support if (!navigator.gpu) { throw new Error("WebGPU is not supported in this browser."); } // 2. Request an Adapter (Physical GPU) const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance", // "low-power" for integrated GPU }); if (!adapter) throw new Error("No suitable GPU adapter found."); // 3. Inspect adapter capabilities const adapterInfo = await adapter.requestAdapterInfo(); console.log("GPU Vendor:", adapterInfo.vendor); console.log("GPU Architecture:", adapterInfo.architecture); console.log("Max Texture Dimension:", adapter.limits.maxTextureDimension2D); console.log("Max Buffer Size:", adapter.limits.maxBufferSize / (1024*1024), "MB"); // 4. Request a Device (Logical GPU Connection) const device = await adapter.requestDevice({ label: "My WebGPU Device", requiredLimits: { maxBufferSize: 512 * 1024 * 1024, // Request up to 512 MB buffers maxStorageBufferBindingSize: 512 * 1024 * 1024, }, requiredFeatures: [ // "texture-compression-bc", // BC compressed textures (Desktop) // "texture-compression-astc", // ASTC compressed textures (Mobile) // "rg11b10ufloat-renderable", // HDR render targets // "timestamp-query", // GPU timing ] }); // 5. Handle device loss (GPU reset, driver update, etc.) device.lost.then((info) => { console.error("WebGPU device was lost:", info.message); if (info.reason !== "destroyed") { initWebGPU(); // Attempt to reinitialize } }); return { adapter, device };}
Canvas Configuration
Connecting WebGPU to a Canvas
const canvas = document.querySelector("canvas");const context = canvas.getContext("webgpu");// Choose the best format for the screen (usually 'bgra8unorm' on desktop)const preferredFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({ device: device, format: preferredFormat, // Pixel format alphaMode: "opaque", // No window transparency usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,});// Each frame: get the texture to render into (equivalent of swapchain image acquire)const currentTexture = context.getCurrentTexture();const currentView = currentTexture.createView();
WGSL Shader Language
WGSL — WebGPU Shading Language
WGSL is statically typed, Rust-like, and cross-compiles to SPIR-V (Linux), HLSL (Windows), and MSL (Mac) internally by the browser.
WGSL Type
Equivalent in GLSL
Description
f32
float
32-bit float
i32
int
32-bit signed int
u32
uint
32-bit unsigned int
vec2<f32>
vec2
2-component float vector
vec3<f32>
vec3
3-component float vector
vec4<f32>
vec4
4-component float vector
mat4x4<f32>
mat4
4x4 float matrix
array<f32, N>
float arr[N]
Fixed-size array
array<Vertex>
SSBO array
Dynamic-size array (in storage buffer)
bool
bool
Boolean
The Triangle Shader (WGSL)
// Uniforms (Struct + binding)struct Uniforms { modelMatrix : mat4x4<f32>, viewProjMatrix : mat4x4<f32>, cameraPosition : vec3<f32>, time : f32,};@group(0) @binding(0) var<uniform> uniforms : Uniforms;@group(0) @binding(1) var albedoTexture : texture_2d<f32>;@group(0) @binding(2) var texSampler : sampler;// Vertex → Fragment structstruct VertexOutput { @builtin(position) position : vec4<f32>, // Required: clip-space position @location(0) worldPos : vec3<f32>, @location(1) normal : vec3<f32>, @location(2) uv : vec2<f32>,};// Vertex shader — @vertex marks entry point@vertexfn vs_main( @location(0) position : vec3<f32>, // From vertex buffer attribute 0 @location(1) normal : vec3<f32>, // From vertex buffer attribute 1 @location(2) uv : vec2<f32>, // From vertex buffer attribute 2) -> VertexOutput { var out : VertexOutput; let worldPos = uniforms.modelMatrix * vec4<f32>(position, 1.0); out.position = uniforms.viewProjMatrix * worldPos; out.worldPos = worldPos.xyz; out.normal = (uniforms.modelMatrix * vec4<f32>(normal, 0.0)).xyz; out.uv = uv; return out;}// Fragment shader — @fragment marks entry point@fragmentfn fs_main(in : VertexOutput) -> @location(0) vec4<f32> { let albedo = textureSample(albedoTexture, texSampler, in.uv); let normal = normalize(in.normal); let lightDir = normalize(vec3<f32>(1.0, 2.0, -1.0)); let ndotl = max(dot(normal, lightDir), 0.0); let result = albedo.rgb * (ndotl + 0.1); return vec4<f32>(result, albedo.a);}
Compute Shader (WGSL)
// WGSL Compute Shader — particle simulationstruct Particle { position : vec2<f32>, velocity : vec2<f32>, color : vec4<f32>, lifetime : f32, _pad : vec3<f32>, // 16-byte alignment padding};// Storage buffers@group(0) @binding(0) var<storage, read> particlesIn : array<Particle>;@group(0) @binding(1) var<storage, read_write> particlesOut : array<Particle>;// Uniform for time delta@group(0) @binding(2) var<uniform> deltaTime : f32;// @compute marks this as a compute shader// @workgroup_size(x, y, z) — 64 threads per workgroup@compute @workgroup_size(64, 1, 1)fn cs_main( @builtin(global_invocation_id) globalID : vec3<u32> // Thread position in grid) { let index = globalID.x; let total = arrayLength(&particlesIn); if (index >= total) { return; } // Guard: don't run on extra threads var p = particlesIn[index]; // Integrate velocity → position p.velocity += vec2<f32>(0.0, -9.8) * deltaTime; p.position += p.velocity * deltaTime; p.lifetime -= deltaTime; // Respawn when lifetime expires if (p.lifetime <= 0.0) { p.position = vec2<f32>(0.0); p.velocity = vec2<f32>(0.0, 5.0); p.lifetime = 2.0; } particlesOut[index] = p;}
Buffers
Buffer Types and Usages
GPUBufferUsage flag
Purpose
VERTEX
Vertex attribute data
INDEX
Triangle index data
UNIFORM
Small, per-frame constant data read in shaders
STORAGE
Large read/write GPU arrays (compute)
COPY_SRC
Can be copied FROM (staging source)
COPY_DST
Can be copied TO (staging destination, GPU target)
MAP_READ
CPU can map this after GPU writes to read results back
MAP_WRITE
CPU can map this to write data in, then copy to GPU
INDIRECT
Used as argument buffer for indirect draw/dispatch
Creating Buffers
// ---- Create a Vertex Buffer ----const vertices = new Float32Array([// x, y, z, nx, ny, nz, u, v -0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 0.5, 1.0,]);const vertexBuffer = device.createBuffer({ label: "Vertex Buffer", size: vertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, mappedAtCreation: false,});// Upload data via device.queue.writeBuffer (the easy way)device.queue.writeBuffer(vertexBuffer, 0, vertices);// ---- Create a Uniform Buffer (mapped persistently) ----const uniformBuffer = device.createBuffer({ label: "Per-Frame Uniforms", size: 256, // Always multiple of 256 for uniforms usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,});// Update every framefunction updateUniforms(modelMatrix, viewProjMatrix) { const data = new Float32Array([ ...modelMatrix, // 16 floats ...viewProjMatrix, // 16 floats ]); device.queue.writeBuffer(uniformBuffer, 0, data);}// ---- Create a Storage Buffer for Compute ----const PARTICLE_COUNT = 100000;const PARTICLE_STRIDE = 32; // bytes per particleconst particleBufferA = device.createBuffer({ label: "Particle Buffer A", size: PARTICLE_COUNT * PARTICLE_STRIDE, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,});const particleBufferB = device.createBuffer({ label: "Particle Buffer B", size: PARTICLE_COUNT * PARTICLE_STRIDE, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX, // Render from compute output!});
Reading Data Back from GPU (Readback)
// Create a buffer the CPU can read fromconst readbackBuffer = device.createBuffer({ size: computeResultSize, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,});// After dispatch — copy compute output to readback bufferconst encoder = device.createCommandEncoder();encoder.copyBufferToBuffer(computeOutputBuffer, 0, readbackBuffer, 0, computeResultSize);device.queue.submit([encoder.finish()]);// Map the buffer for CPU reading (async — waits for GPU to finish)await readbackBuffer.mapAsync(GPUMapMode.READ);const data = new Float32Array(readbackBuffer.getMappedRange());console.log("GPU Result:", data[0], data[1], data[2]);readbackBuffer.unmap(); // Unmap before GPU can use it again
In WebGPU, you do not bind resources one by one. You group them into a GPUBindGroup — a snapshot of exactly which buffers, textures, and samplers are bound at specific slots.