Try   HackMD

PSA for Chromium / Dawn WebGPU API updates 2021-04-07

Chromium's WebGPU implementation and Dawn's API try to closely follow changes to the WebGPU specification. When the WebGPU IDL changes, Chromium and Dawn will try to support both the deprecated and the new version of the IDL at the same time so prototypes can be updated. In JavaScript, uses of the deprecated path will result in a console warning, while when using Dawn directly, the deprecated path will print a warning to stderr.

Note that all changes to Dawn's API make it closer to webgpu.h that we hope will allow applications to target both Dawn, and wgpu in native before being compiled in WASM. Emscripten will also be updated from the "old" to the "new" API but won't have the smooth transition since developers control which version of emscripten they use.

A couple weeks after an update like this one, the "old" version will be removed. This means that the "old" version of the items below will start being removed from Chromium/Dawn starting on 2021-04-21.

Previous PSAs:

WGSL improvements

The WGSL specification has made a lot of progress with most of the builtin functions specified and nice ergonomics improvements (with more likely coming). The implementation in Chromium is still evolving rapidly to catch up with the specification so we aren't offering a gradual deprecation like for the WebGPU API yet.

Around the last PSA, WGSL code looked like this:

type MyData = [[block]] struct { [[offset 0]] modelViewProjectionMatrix : mat4x4<f32>; [[offset 64]] positionOffset : vec4<f32>; }; [[binding 0, set 0]] var<uniform> u : MyData; [[location 0]] var<in> position : vec4<f32>;] [[builtin position]] var<out> Position : vec4<f32>; [[location 0]] var<out> FragUV : vec2<f32>; fn main() -> void { const fixedPosition : vec4<f32> = position + u.positionOffset; FragUV = position.xy * 0.5; Position = u.modelViewProjectionMatrix * fixedPosition; return; } entry_point vertex as "main" = main;

It now looks like this:

[[block]] struct MyData { modelViewProjectionMatrix : mat4x4<f32>; positionOffset : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> u : MyData; struct Outputs { [[builtin(position)]] position : vec4<f32>; [[location(0)]] fragUV: vec2<f32>; }; [[stage(vertex)]] fn main([[location(0)]] position : vec2<f32>) -> Outputs { let fixedPosition = position + u.positionOffset; var outputs : Outputs; outputs.fragUV = position.xy * 0.5; outputs.position = u.modelViewProjectionMatrix * fixedPosition; return outputs; }

Breaking changes

There are a lot of breaking changes this time, and a couple of them might require large changes (GPUBindGroupLayoutEntry and GPURenderPipelineDescriptor). The changes come both from feedback of WebGPU early adopters as well as some more focus on ergonomics from the WebGPU group.

The group looked at most of the API to see if changes are needed and no breaking changes are in the pipeline. That's why we are hopeful that there will be fewer breaking changed in the future.

OUTPUT_ATTACHMENT -> RENDER_ATTACHMENT

WebGPU PR

Contrary to Vulkan, WebGPU doesn't have "input attachment" so there is not symmetry needed with OUTPUT_ATTACHMENT. For clarity it is renamed to RENDER_ATTACHMENT. In JavaScript the following changes are needed:

const texture = device.createTexture({ dimension: '2d', size: [4, 4], format: 'rgba8unorm', - usage: GPUTextureUsage.OUTPUT_ATTACHMENT, + usage: GPUTextureUsage.RENDER_ATTACHMENT, });

Likewise when using Dawn’s API, changes are needed:

wgpu::TextureDescriptor desc; desc.dimension = wgpu::TextureDimension::e2D; desc.size = {4, 4}; desc.format = wgpu::TextureFormat::RGBA8Unorm; -desc.usage = wgpu::TextureUsage::OutputAttachment; +desc.usage = wgpu::TextureUsage::RenderAttachment; wgpu::Texture texture = device.CreateTexture(&desc);

GPUBindGroupLayoutEntry changes

Main WebGPU PR

In order to keep the complexity of BindGroupLayoutEntry from exploding as more capabilities are added, it was refactored to isolate the properties needed for each type of binding.

An example of the change needed in JavaScript:

const bindGroupLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, - type: 'uniform-buffer' + buffer: {} }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, - type: 'sampler' + sampler: {} }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, - type: 'sampled-texture' + texture: {} }, { binding: 3, visibility: GPUShaderStage.COMPUTE, - type: 'storage-buffer' + buffer: { type: 'storage' } }, { binding: 4, visibility: GPUShaderStage.FRAGMENT, - type: 'sampled-texture' - viewDimension: 'cube' + texture: { viewDimension: 'cube' } }] });

An example of the changes needed when using Dawn's API (note that at least one of buffer.type, sampler.type, texture.sampleType or storageTexture.access+format must be set):

wgpu::BindGroupLayoutEntry entries[5]; entries[0].binding = 0; entries[0].visibility = wgpu::ShaderStage::Vertex; -entries[0].bindingType = wgpu::BindingType::UniformBuffer; +entries[0].buffer.type = wgpu::BufferBindingType::Uniform; entries[1].binding = 1; entries[1].visibility = wgpu::ShaderStage::Fragment; -entries[1].bindingType = wgpu::BindingType::Sampler; +entries[1].sampler.type = wgpu::SamplerBindingType::Filtering; entries[2].binding = 2; entries[2].visibility = wgpu::ShaderStage::Fragment; -entries[2].bindingType = wgpu::BindingType::SampledTexture; +entries[2].texture.sampleType = wgpu::TextureSampleType::Float; entries[3].binding = 3; entries[3].visibility = wgpu::ShaderStage::Compute; -entries[3].bindingType = wgpu::BindingType::StorageBuffer; +entries[0].buffer.type = wgpu::BufferBindingType::Storage; entries[4].binding = 4; entries[4].visibility = wgpu::ShaderStage::Compute; -entries[4].bindingType = wgpu::BindingType::StorageTexture; -entries[4].format = wgpu::TextureFormat::RGBA8Unorm; +entries[4].storageTexture.access = wgpu::StorageTextureAccess::ReadOnly; +entries[4].storageTexture.format = wgpu::TextureFormat::RGBA8Unorm;

Render pipeline changes

Main WebGPU PR

The GPURenderPipelineDescriptor format has been re-organized for clarity. See the table below for converting deprecated GPURenderPipelineDescriptors to the new format.

Dawn Tracking Bug

Old key New Key
vertexStage.module vertex.module
vertexStage.entryPoint vertex.entryPoint
vertexState.vertexBuffers vertex.buffers
vertexState.indexFormat primitive.stripIndexFormat
primitiveTopology primitive.topology
rasterizationState.frontFace primitive.frontFace
rasterizationState.cullMode primitive.cullMode
rasterizationState.depthBias depthStencil.depthBias
rasterizationState.depthBiasSlopeScale depthStencil.depthBiasSlopeScale
rasterizationState.depthBiasClamp depthStencil.depthBiasClamp
depthStencilState.format depthStencil.format
depthStencilState.depthWriteEnabled depthStencil.depthWriteEnabled
depthStencilState.depthCompare depthStencil.depthCompare
depthStencilState.stencilFront depthStencil.stencilFront
depthStencilState.stencilBack depthStencil.stencilBack
depthStencilState.stencilReadMask depthStencil.stencilReadMask
depthStencilState.stencilWriteMask depthStencil.stencilWriteMask
sampleCount multisample.count
sampleMask multisample.mask
alphaToCoverageEnabled multisample.alphaToCoverageEnabled
fragmentStage.module fragment.module
fragmentStage.entryPoint fragment.entryPoint
colorStates[i].format fragment.targets[i].format
colorStates[i].writeMask fragment.targets[i].writeMask
colorStates[i].colorBlend fragment.targets[i].blend.color
colorStates[i].alphaBlend fragment.targets[i].blend.alpha

Note that Chromium/Dawn will now validate that fragment.targets[i].blend is undefined / nullptr if fragment.targets[i].format is a non-filterable format (i.e. integer or float32).

Some types part of the GPUPipelineDescriptor have been renamed. No changes are needed in JavaScript, but the following updates are required when using Dawn's API:

Previous type New type
wgpu::BlendDescriptor wgpu::BlendComponent
wgpu::StencilFaceStateDescriptor wgpu::StencilFaceState
wgpu::VertexAttributeDescriptor wgpu::VertexAttributes
wgpu::VertexBufferLayoutDescriptor wgpu::VertexBufferLayout

Also when using Dawn's API the new render pipeline descriptor is called wgpu::RenderPipelineDescriptor2 and passed to wgpu::Device::CreateRenderPipeline2. Of course this is only temporary and after the deprecation period, the "2" variant will be deprecated in favor of the old names (but with the new structure).

GPUDevice.defaultQueue -> GPUDevice.queue

WebGPU PR

Since WebGPU does not yet support multiple queues, and many applications will only ever need one queue, the GPUDevice's default queue has been renamed to simply queue to reduce the amount of typing needed.

An example of the change needed in JavaScript:

-device.defaultQueue.submit([commandEncoder.finish()]); +device.queue.submit([commandEncoder.finish()]);

Likewise when using Dawn’s API, changes are needed:

-wgpu::Queue queue = device.GetDefaultQueue(); +wgpu::Queue queue = device.GetQueue();

GPUFence -> GPUQueue.onSubmittedWorkDone

WebGPU PR

Since WebGPU does not yet support multiple queues, the GPUFence could only be used to watch the completion of work submitted to queues. Because of this extremely limited use, they are replaced with GPUQueue.onSubmittedWorkDone which is a Promise that resolves when the queue is done processing work submitted until this point.

In JavaScript the following changes are needed:

-const fence = queue.createFence(); -queue.signal(fence, 1); -await fence.onCompletion(1); +await queue.onSubmittedWorkDone();

Likewise when using Dawn’s API, changes are needed:

-wgpu::Fence fence = queue.CreateFence(); -queue.Signal(fence, 1); -fence.OnCompletion(CompletionCallback, userdata); +queue.OnSubmittedWorkDone(CompletionCallback, userdata);

VertexFormat rename.

WebGPU PR

WebGPU's vertex formats were using "char", "short" and similar terminology which wasn't used anywhere else (and isn't very descriptive if you don't know C/C++). To make it more clear it has been changed to use names like "uint8", "sint16" and "float32". See the table below for correspondances in the JavaScript API and Dawn's API.

Old Javascript New Javascript Old Dawn New Dawn
"uchar2" "uint8x2" UChar2 Uint8x2
"uchar4" "uint8x4" UChar4 Uint8x4
"char2" "sint8x2" Char2 Sint8x2
"char4" "sint8x4" Char4 Sint8x4
"uchar2norm" "unorm8x2" UChar2Norm Unorm8x2
"uchar4norm" "unorm8x4" UChar4Norm Unorm8x4
"char2norm" "snorm8x2" Char2Norm Snorm8x2
"char4norm" "snorm8x4" Char4Norm Snorm8x4
"ushort2" "uint16x2" UShort2 Uint16x2
"ushort4" "uint16x4" UShort4 Uint16x4
"short2" "sint16x2" Short2 Sint16x2
"short4" "sint16x4" Short4 Sint16x4
"ushort2norm" "unorm16x2" UShort2Norm Unorm16x2
"ushort4norm" "unorm16x4" UShort4Norm Unorm16x4
"short2norm" "snorm16x2" Short2Norm Snorm16x2
"short4norm" "snorm16x4" Short4Norm Snorm16x4
"half2" "float16x2" Half2 Float16x2
"half4" "float16x4" Half4 Float16x4
"float" "float32" Float Float32
"float2" "float32x2" Float2 Float32x2
"float3" "float32x3" Float3 Float32x3
"float4" "float32x4" Float4 Float32x4
"uint" "uint32" UInt Uint32
"uint2" "uint32x2" UInt2 Uint32x2
"uint3" "uint32x3" UInt3 Uint32x3
"uint4" "uint32x4" UInt4 Uint32x4
"int" "sint32" SInt Uint32
"int2" "sint32x2" Int2 Sint32x2
"int3" "sint32x3" Int3 Sint32x3
"int4" "sint32x4" Int4 Sint32x4

CopyView type renames

WebGPU PR

To avoid confusion with the concept of TextureViews, the types BufferCopyView, TextureCopyView, and ImageBitmapCopyView have been renamed to ImageCopyBuffer, ImageCopyTexture, and ImageCopyImageBitmap respectively.

Since these types are dictionaries this rename has no effect on JavaScript code, other than to make the documentation a bit clearer.

When using Dawn's API, changes are needed:

-wgpu::BufferCopyView bufferSrc; +wgpu::ImageCopyBuffer bufferSrc; bufferSrc.buffer = buffer; bufferSrc.layout.offset = 0; bufferSrc.layout.bytesPerRow = 1024; bufferSrc.layout.rowsPerImage = wgpu::kCopyStrideUndefined; -wgpu::TextureCopyView textureDst; +wgpu::ImageCopyTexture textureDst; textureDst.texture = texture; textureDst.mipLevel = 0; textureDst.origin = {0, 0, 0}; textureDst.aspect = wgpu::TextureAspect::All; wgpu::Extent3D copySize = {1024, 1024, 1}; wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); encoder.CopyBufferToTexture(&bufferSrc, &textureDst, &copySize);

Dawn API bytesPerRow and rowsPerImage changes

Previously when doing 1D copies, validation ignored the value bytesPerRow so it was valid to set it to 0, or another value too small. This will now cause a validation error. Instead set bytesPerRow to wgpu::kCopyStrideUndefined (the default struct member value) or a value larger than the copy's size in bytes.

Likewise previously when doing 2D copies, validation ignored the value rowsPerImage. To avoid a validation error it must now be set to wgpu::kCopyStrideUndefined (the default struct member value) or a value larger than the copy's height.

Finally when doing 3D copies, a 0 value for rowsPerImage caused it to default to the copy height. This will now produce a validation error and you should use wgpu::kCopyStrideUndefined instead (the default struct member value

These changes are made to be closer to the Javascript API where undefined and 0 have different behaviors, and makes it easier to implement the Javascript API using Dawn's API.

GPUExtent3DDict depth -> depthOrArrayLayers

WebGPU PR

Depending on context, the 3rd element of a GPUExtent3DDict may represent either a depth, as in the case of a 3D texture, or the number of layers, as in the case of a 2D array texture. To clarify this dual purpose, the depth attribute has been renamed to depthOrArrayLayers.

An example of the change needed in JavaScript:

const texture = device.createTexture({ - size: { width: 16, height: 16, depth: 16 }, + size: { width: 16, height: 16, depthOrArrayLayers: 16 }, dimension: '3d'; format: 'rgba8unorm', usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED });

Likewise when using Dawn’s API, changes are needed:

wgpu::TextureDescriptor descriptor; descriptor.size.width = 16; descriptor.size.height = 16; -descriptor.size.depth = 16; +descriptor.size.depthOrArrayLayers = 16; descriptor.dimension = wgpu::TextureDimension::e3D; descriptor.format = wgpu::TextureFormat::RGBA8Unorm; descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::Sampled; wgpu::Texture texture = device.CreateTexture(&descriptor);

Note that both Javascript and Dawn's API support shorthand declarations with unspecified values defaulting to one.

In Javascript:

[w, h] // Same as {width: w, height: h, depthOrArrayLayers: 1} 

Using Dawn's API:

wgpu::Extent3D extent = {w, h}; // Is the same as wgpu::Extent3D extent; extent.width = w; extent.height = h; extent.depthOrArrayLayers = 1;

getSwapChainPreferredFormat

It was determined that getSwapChainPreferredFormat didn't need to be asynchronous, and that it was more appropriate for it to accept a GPUAdapter than a GPUDevice.

An example of the change needed in JavaScript:

const context = canvas.getContext('gpupresent'); -const swapChainFormat = await context.getSwapChainPreferredFormat(device); +const swapChainFormat = context.getSwapChainPreferredFormat(adapter); const swapChain = context.configureSwapChain({ device: device, format: swapChainFormat });

The Dawn API does not currently have an equivalent call, so no changes are needed.

SetIndexBufferWithFormat -> SetIndexBuffer

In Dawn's API SetIndexBuffer was temporarily renamed to SetIndexBufferWithFormat while its signature was changed. Now that the signature is changed, SetIndexBufferWithFormat is deprecated in favor of SetIndexBuffer (with the updated signature).

extensions->features rename

WebGPU PR

The WebGPU specification has started referring to what was previously called "Extensions" as "Features". This is because they include functionality that is present in the base spec but is not universally supported, and as such needs to be enabled explicitly (such as compressed texture formats).

In service of this change the extensions attribute on both GPUAdapter and GPUDevice has been renamed to features. Also, the extensions key of the GPUDeviceDescriptor dictionary has beed renamed to nonGuaranteedFeatures. The name nonGuaranteedFeatures was chosen to emphasize that the features requested in this array are not guaranteed to be supported on all platforms and devices, and as such should be verified against the GPUAdapter.features list prior to requesting. As was the case prior to the rename, specifying a feature in the nonGuaranteedFeatures that is not supported by the GPUAdapter will cause requestDevice() to fail.

An example of the change needed in JavaScript:

let deviceDescriptor = {}; -const hasBCCompression = adapter.extensions.indexOf('texture-compression-bc') != -1; +const hasBCCompression = adapter.features.indexOf('texture-compression-bc') != -1; if (hasBCCompression) { - deviceDescriptor.extensions = ['texture-compression-bc']; + deviceDescriptor.nonGuaranteedFeatures = ['texture-compression-bc']; } let device = await adapter.requestDevice(deviceDescriptor);

This change has not yet been made in Dawn, so no changes are required yet.

New features and improvements

Most of the improvements to Dawn / Chromium for WebGPU are happening under the hood. Recently our focus has been on massively improving the security of our implementation in preparation for a future WebGPU Origin Trial. This includes reworking large parts of our cross-process remoting architecture, vastly expanded test coverage (and fixing all the tiny issues found in the spec etc), automatic fuzzing, etc.

Another huge effort is expanding usage of Tint, our WGSL compiler. Previously it directly converted WGSL to SPIR-V and we were using SPIRV-Cross for translation to MSL / HLSL / GLSL and reflection. SPIRV-Cross is a mature library but is not meant to handle untrusted/malicious inputs like you can find on the Web. Tint has been expanded to the point where Dawn can use it for shader reflection and WGSL -> HLSL translation.

OffscreenCanvas support

OffscreenCanvas now supports the "gpupresent" context type! It can currently be used to detach the content of the canvas into and ImageBitmap with transferToImageBitmap(). Additional features like using WebGPU on an OffscreenCanvas received from transferToOffscreenCanvas() and OffscreenCanvas.convertToBlob() will follow shortly.

Create*PipelineAsync

When a GPURenderPipeline or GPUComputePipeline is created with GPUDevice.createRenderPipeline() or GPUDevice.createComputePipeline() respectively, they return immediately. On the GPU process side it means that the pipelines have to be compiled immediately and stall the execution of further commands (because they could be used for drawing immediately). This can easily cause frame hitching and at worst compiling a lot of pipelines can trigger the "GPU watchdog" that will lose the WebGPU devices.

New methods for creating pipelines asynchronously, GPUDevice.createRenderPipelineAsync() and GPUDevice.createComputePipelineAsync(), have been added to address this. These methods accept the same descriptors as their synchronous counterparts but asynchronously return the pipeline once it is fully ready to be used. Using pipelines returned from the create*PipelineAsync() methods can help reduce hitches on first use compared to those returned from create*Pipeline(). This is a similar idea to WebGL's KHR_parallel_shader_compile but more Javacript-idiomatic.

Note that currently these methods aren't completely asynchronous internally but work is ongoing to improve their performance.

Using the feature in Javascript is done like below:

const computePipeline = await device.createComputePipelineAsync({ compute: { module: device.createShaderModule({ code }), entryPoint: 'main' } }); const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(computePipeline); passEncoder.dispatch(1024, 1024); passEncoder.endPass(); device.queue.submit([commandEncoder.finish()]);

Using the feature in Dawn's API is done like below:

wgpu::ComputePipelineDescriptor desc; // Fill `desc` as usual. device->CreateComputePipeline(&desc, [](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline pipeline, const char* message, void* userdata) { if (status == WGPUCreatePipelineAsyncStatus::WGPUCreatePipelineAsyncStatus_Success) { // pipeline can now be used as usual. } }, nullptr // userdata passed to the callback. );

Depth clamping

"depth-clamping" is a new optional WebGPU feature that allows controlling the behavior of fragments with a depth that fall outside of the [0, 1] range. By default without this extension they are discarded, instead when setting GPUPrimitiveState.clampDepth = true the fragments will be kept and their depth clamped to [0, 1]. This is particularly useful when rendering shadow maps so that geometry outside of the light's frustum still occludes geometry correctly (otherwise it would just "disappear" from the shadow).

Using the feature in Javascript is done like below:

// Check that our adapter has the feature available. const adapter = await navigator.gpu.requestAdapter(); if (!adapter.features.includes("depth-clamping")) { alert("Need GPUAdapter with depth clamping support!"); return; } // Create the device, requesting the feature to be enabled. const device = await adapter.requestDevice({ nonGuaranteedFeatures: ["depth-clamping"] }); // Create a pipeline that uses the feature. const pipeline = device.createRenderPipeline({ primitive: { clampDepth: true, // ... }, // ... });

Using the feature in Dawn's API, when a wgpu::Device with the feature has been created is done like below. Extensibility of descriptors in C++ is done with "chained structures" a bit like Vulkan:

wgpu::RenderPipelineDescriptor desc; // Fill `desc` as usual. // Chain our depth clamping state in desc.primitive. wgpu::PrimitiveDepthClampingState depthClampState; depthClampState.clampDepth = true; desc.primitive.nextInChain = &depthClampState; wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&desc);

maxAnisotropy

It is now possible to specify a GPUSampler's maximum anisotropy for anisotropic filtering. For historical reasons this features is not guaranteed to be everywhere so the WebGPU specification doesn't require that values higher than 1 do something, but that should be the case everywhere in practice.

Using the feature is done by adding maxAnisotropy to the descriptor passed to GPUDevice.createSampler:

const sampler = device.createSampler({ magFilter: "linear", minFilter: "linear", mipmapFilter: "linear", maxAnisotropy: 4, });

When using Dawn's API maxAnisotropy can be set in a similar way:

wgpu::SamplerDescriptor desc; desc.magFilter = wgpu::FilterMode::Linear; desc.minFilter = wgpu::FilterMode::Linear; desc.mipmapFilter = wgpu::FilterMode::Linear; desc.maxAnisotropy = 4; wgpu::Sampler sampler = device.CreateSampler(&desc);