Choosing a WebGPU color-attachment format: rgba8unorm-srgb vs bgra8unorm-srgb
Pick a WebGPU color-attachment format the supported way: use getPreferredCanvasFormat(), understand what the -srgb variants actually do, and verify with a known-color read-back test.
03 Sept 2025, 09:40 UTC

The decision and the short answer
Every WebGPU swap chain and offscreen render target needs a color-attachment format, and the two realistic candidates for standard dynamic-range output are rgba8unorm-srgb and bgra8unorm-srgb. The choice affects whether the GPU performs automatic linear-to-sRGB conversion on write, how your WGSL shaders see channel order, and how well the format maps to the native presentation surface on each platform.
Short answer: call navigator.gpu.getPreferredCanvasFormat() and use whatever it returns for your canvas. On most desktop systems that is bgra8unorm-srgb (or bgra8unorm); on some Android devices it is rgba8unorm. Hard-coding either format is the common mistake; the preferred-format query exists precisely because the right answer is per-device.
Constraints that drive the choice
- Canvas presentation: a canvas context must be configured with a format the compositor accepts without an extra copy. The preferred-format query tells you which one that is.
- sRGB handling: the
-srgbvariants make the GPU convert linear shader output to sRGB automatically on write, and convert sRGB to linear on texture reads. If you render lighting in linear space, this removes manual gamma code. - Offscreen targets: intermediate render targets are not presented, so byte order matters less; pick whichever keeps your pipeline formats consistent to avoid extra conversions.
- Fallbacks: if you also ship a WebGL2 path, neither WebGPU format name exists there; you need a parallel code path regardless.
Format comparison
| Property | rgba8unorm-srgb | bgra8unorm-srgb |
|---|---|---|
| Bytes per pixel | 4 | 4 |
| Automatic linear↔sRGB conversion | Yes | Yes |
| Typical preferred canvas format | Some Android / older drivers | Most Windows, macOS, ChromeOS desktop |
| Channel order as seen in WGSL | vec4 is (r, g, b, a) | vec4 is still (r, g, b, a); swizzle is handled by the API |
| Extra compositor copy risk | On platforms where BGRA is native | On platforms where RGBA is native |
One clarification that trips people up: in WGSL you always read and write vec4<f32> in (r, g, b, a) order. The difference between the formats is memory layout, not shader-visible order, so you do not write manual swizzles in WGSL for either one. The performance difference comes from whether the presentation engine can consume the buffer directly or must convert it.
Concrete implementation
The supported pattern is: ask for the preferred format, configure the canvas with it, and build your render pipeline against context.getCurrentTexture().format so the pipeline always matches the surface. Run this in a browser page or worker with WebGPU access (Chrome 113+, Edge 113+, Firefox 141+ on Windows; check navigator.gpu exists first). No special permissions are needed beyond a secure context (HTTPS or localhost).
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('WebGPU not available');
const device = await adapter.requestDevice();
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
// Per-device correct answer instead of hard-coding a format:
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: shaderModule, entryPoint: 'vs' },
fragment: {
module: shaderModule,
entryPoint: 'fs',
targets: [{ format }] // must match the configured context format
},
primitive: { topology: 'triangle-list' }
});Placeholders: shaderModule is your compiled WGSL module; vs/fs are your entry points. Expected check: context.getCurrentTexture().format equals the format you built the pipeline with; a mismatch raises a validation error at draw time, which is the most common real-world failure here.
If you need an sRGB-correct offscreen target (for example, a post-processing chain), create it explicitly and sample it as a texture so reads are linearized automatically:
const offscreen = device.createTexture({
size: [canvas.width, canvas.height],
format: 'rgba8unorm-srgb',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
});Risk: bgra8unorm-srgb and rgba8unorm-srgb are both core formats in the WebGPU spec, so support is not the issue; the issue is assuming one is the preferred canvas format everywhere. Also, recreating the device (after device loss) invalidates textures and pipelines; rebuild them against the new device's preferred format rather than caching the old choice.
How to verify the result
- Log
navigator.gpu.getPreferredCanvasFormat()on each target device and browser; confirm your code uses that value rather than a constant. - Render a triangle outputting a known linear color, e.g.
vec4(0.5, 0.0, 0.0, 1.0), into an-srgbattachment. Screenshot or read back the pixel: an sRGB attachment should store approximately 188/255 red (0.5 linear ≈ 0.735 sRGB), not 128/255. That confirms automatic conversion is active. - Render the same triangle into a non-srgb attachment; the read-back should be ~128/255. The contrast between the two runs proves the format flag is doing what you expect.
- Test on at least one desktop browser and one Android device, since preferred formats differ across them.
Limitations
- Preferred canvas format varies by OS, GPU, and browser version; never assume from a single test machine.
- Read-back of a canvas texture requires copying to a buffer with
copyTextureToBuffer; canvas textures are not directly mappable. - WebGL2 fallback paths cannot share these format names; plan a separate format table if you support both APIs.
- Behavior described matches the WebGPU spec as implemented in current stable browsers; verify on your exact target versions before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.