WebGPU Fundamentals
repository·main·Indexed 21 days ago
https://github.com/webgpu/webgpufundamentalsA collection of lessons and tutorials designed to teach the WebGPU API, covering topics from basics to advanced 3D math and compute shaders. The documentation includes guides on debugging using error scopes, uncapturederror events, and tools like WebGPU-Inspector and WebGPU-Dev-Extension, as well as instructions for generating WGSL function references and contributing translations.
What's inside WebGPU Fundamentals
- Picking is the process of selecting 3D objects by clicking on the screen and determining which specific objects correspond to the clicked coordinates. This lesson covers techniques for implementing object selection within a 3D editor context.
Use webgpu-utils for common WebGPU tasks
mainThe
webgpu-utilslibrary provides a collection of helper functions for common WebGPU operations, reducing boilerplate for tasks like texture management and mipmap generation.Key capabilities include:
- Texture Importing: Functions like
loadImageBitmap,createTextureFromSource,createTextureFromImage, andcreateTextureFromImagessimplify loading image data into WebGPU textures. - Mipmap Generation:
generateMips(including versions for multiple layers/cubemaps) handles the creation of mipmap levels. - Texture Copying: Utilities like
copySourceToTextureandcopySourcesToTexturemanage data transfers. - Transparency: Includes support for
premultipliedAlphain blending scenarios.
- Texture Importing: Functions like
WebGPU Picking: Overview
mainPicking is the process of selecting objects in a 3D scene by clicking on the screen and identifying which specific objects were clicked. This lesson is part of a series for building 3D editor components. To understand the full context, it is recommended to follow the series in order:
- Highlighting: Visual feedback for selection.
- Camera Controls: Navigating the 3D space.
- Picking: Selecting objects via user input.
Implement GPU-based picking in WebGPU
mainGPU picking works by rendering objects with unique integer IDs instead of colors. By sampling the texel under the pointer from an ID-encoded texture, you can identify which object was clicked.
There are two primary strategies:
- Multi-target rendering: Modify the fragment shader to return both color and ID to a single render pass using multiple
@locationoutputs. This is efficient as it only requires one render pass. - Dual-pass rendering: Render the scene once for visual color and a second time into a specialized ID texture (e.g., using
r32uintformat). This is useful if you need to selectively render objects (for example, to implement 'cycling' through objects by excluding the currently selected one from the picking pass).
// Example of a multi-target fragment shader output struct MyOutput { @location(0) color: vec4f, @location(1) id: vec4u, }; @fragment fn fs(vsOut: VSOutput) -> MyOutput { return MyOutput( vsOut.color * uni.color, uni.id, ); }- Multi-target rendering: Modify the fragment shader to return both color and ID to a single render pass using multiple
Constraints and limitations of storage textures
mainWhen using storage textures, keep the following limitations in mind:
- Read/Write Formats: Only
r32float,r32sint, andr32uintformats supportread_writeaccess. Other formats are restricted to eitherreadorwritewithin a single shader. - Supported Formats: Not all texture formats support storage usage. Supported formats include:
rgba8(unorm/snorm/sint/uint)rgba16(float/sint/uint)rg32(float/sint/uint)rgba32(float/sint/uint)- Note:
bgra8unormis not supported by default and requires a specific feature (see below).
- No Samplers: You cannot use samplers with storage textures. Instead of
textureSample, you must usetextureLoad(to read) andtextureStore(to write) which operate on individual pixels.
- Read/Write Formats: Only
Handle mat3x3f Memory Layout and Padding
mainIn WebGPU, a
mat3x3fin a uniform buffer is treated as threevec3fs. Becausevec3fs are often padded to the size of avec4fin memory, amat3x3factually occupies the space of 12 floats (3 columns * 4 floats per column) rather than 9.To avoid manual slicing and padding when uploading to a
GPUBuffer, you should design your JavaScript matrix functions to return a 12-element array where each row/column is padded with a trailing zero.When using padded matrices, the
multiplyfunction must use a stride of 4 instead of 3 to access elements correctly.// Example of a padded identity matrix identity() { return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ]; } // Example of multiplication with 4-float stride multiply(a, b) { const a00 = a[0 * 4 + 0]; const a01 = a[0 * 4 + 1]; // ... and so on }Generate mipmaps for multi-layer textures
mainWhen generating mipmaps for textures with multiple layers (like cube maps), you must iterate through both the mip levels and the array layers.
Crucially, when creating texture views for the mipmap generation process, you must explicitly set
dimension: '2d'. By default, a multi-layer texture view might resolve to2d-array, which is incompatible with the standard 2D mipmap generation shader. UsebaseArrayLayerandarrayLayerCount: 1to target a specific layer during the render pass.// Inside generateMips loop for (let baseMipLevel = 1; baseMipLevel < texture.mipLevelCount; ++baseMipLevel) { for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: sampler }, { binding: 1, resource: texture.createView({ dimension: '2d', baseMipLevel: baseMipLevel - 1, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1, }), }, ], }); const renderPassDescriptor = { colorAttachments: [{ view: texture.createView({ dimension: '2d', baseMipLevel: baseMipLevel, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1, }), loadOp: 'clear', storeOp: 'store', }], }; // ... perform render pass } }How Bind Group Layouts work in WebGPU
mainBind Group Layouts (
GPUBindGroupLayout) allow WebGPU to efficiently match Bind Groups with compute or render pipelines.- Pipeline Layout: A pipeline (like
GPUComputePipelineorGPURenderPipeline) uses aGPUPipelineLayout, which is an array of one or moreGPUBindGroupLayouts. Each layout is assigned to a specific group index (e.g.,@group(0),@group(1)). - Bind Groups: Bind Groups are created using a specific
GPUBindGroupLayout. - Validation: When calling
drawordispatchWorkgroups, WebGPU performs a simple check to ensure the current Bind Group matches theGPUBindGroupLayoutdefined in the pipeline'sGPUPipelineLayout. Most heavy validation occurs during Bind Group creation, making the actual draw/dispatch calls very fast.
Automatic Layouts: If you use
layout: 'auto'when creating a pipeline, WebGPU automatically generates aGPUPipelineLayoutand populates it with the necessaryGPUBindGroupLayouts based on your WGSL shader code.- Pipeline Layout: A pipeline (like
How WebGPU connects data via indices vs WebGL via names
mainWebGPU requires manual synchronization of data locations between the CPU and the GPU. Unlike WebGL, which uses string-based lookups, WebGPU uses array-like indexing.
WebGL Pattern (Name-based):
function likeWebGL(inputs) { const {position, texcoords, normal, color} = inputs; // ... } // Order doesn't matter, can skip parameters likeWebGL({color, position, normal});WebGPU Pattern (Index-based):
function likeWebGPU(inputs) { const [position, texcoords, normal, color] = inputs; // ... } // Must know exact indices (locations) const inputs = []; inputs[0] = position; inputs[2] = normal; inputs[3] = color; likeWebGPU(inputs);function likeWebGL(inputs) { const {position, texcoords, normal, color} = inputs; ... } function likeWebGPU(inputs) { const [position, texcoords, normal, color] = inputs; ... } // WebGL usage const inputs = {}; inputs.normal = normal; inputs.color = color; inputs.position = position; likeWebGL(inputs); // WebGPU usage const inputs = []; inputs[0] = position; inputs[2] = normal; inputs[3] = color; likeWebGPU(inputs);Efficiently manage mappable buffers with a buffer pool
mainBecause
buffer.mapAsync()is asynchronous, there is a delay between requesting a map and being able to use the buffer. To avoid waiting for this promise in performance-critical loops, you can implement a buffer pool pattern:- Maintain a set (pool) of buffers that are kept in a permanently mapped state.
- When you need a buffer, check the pool for an available mapped buffer.
- If the pool is empty, create a new mapped buffer and add it to the pool.
- Once you have finished using a buffer and submitted the relevant GPU commands, call
buffer.unmap()and immediately callbuffer.mapAsync()again to prepare it for the next use. When the promise resolves, return it to the pool.
Inspect WebGPU adapter limits and features
mainBefore requesting a device, you can inspect the capabilities of a
GPUAdapter.- Limits: Accessed via
adapter.limits, these define the maximum values for various resources (e.g.,maxColorAttachments,maxBufferSize). There are 'minimum limits' that all WebGPU devices must support, and higher limits available on specific hardware. - Features: Accessed via
adapter.features, this is an array of strings representing optional capabilities (e.g.,'texture-compression-astc').
You can use
adapter.limitsto check if a specific threshold is met andadapter.features.has('feature-name')to check for optional capabilities.const adapter = await navigator.gpu?.requestAdapter(); // Check a specific limit console.log(adapter.limits.maxColorAttachments); // Check available features console.log(adapter.features); // Example output: ["texture-compression-astc", "texture-compression-bc"]- Limits: Accessed via
Avoid race conditions in WebGPU compute shaders
mainA race condition occurs when multiple threads run in parallel and attempt to access or modify the same memory location simultaneously. Because compute shader invocations run in parallel, the final value in a shared memory location depends on which thread finishes last, which is non-deterministic.
Example of a race condition in WGSL: In the following shader, 32 parallel threads all attempt to write to the same index
result[0]. The final value ofresult[0]is unpredictable.@group(0) @binding(0) var<storage, read_write> result: array<f32>; @compute @workgroup_size(32) fn computeSomething( @builtin(local_invocation_id) local_invocation_id : vec3<u32>, ) { result[0] = local_invocation_id.x; }WebGPU Execution Guarantees (and lack thereof): When designing compute shaders, do not assume the following:
- Simultaneous Execution: You cannot assume different workgroups execute at the same time.
- Exclusivity: You cannot assume that executing one workgroup prevents others from executing. The implementation may run multiple workgroups concurrently or queue them.
- Ordering: You cannot assume that one specific workgroup starts before another. Workgroups do not start in a guaranteed order.