WebGPU has a bunch of optional features and limits. Let’s go over how to check them and request them.
When you request an adapter with
const adapter = await navigator.gpu?.requestAdapter();
The adapter will have a list of limits on adapter.limits
and array of feature names
on adapter.features
. For example
const adapter = await navigator.gpu?.requestAdapter(); console.log(adapter.limits.maxColorAttachments);
Might print 8
to the console meaning the adapter supports a maximum
of 8 color attachments.
Here is a list of all the limits, including the limits of your default adapter as well as the minimum required limits.
The minimum limits are the limits you can count on all devices that support WebGPU to have.
There is also a list of optional features. For example, you could view them like this
const adapter = await navigator.gpu?.requestAdapter(); console.log(adapter.features);
which might print something like ["texture-compression-astc", "texture-compression-bc"]
telling
you those features are available if you request them.
Here is the list of features available on your default adapter.
Note: You can check all of your system’s adapter’s features and limits at webgpureport.org.
By default, when you request a device, you get the minimum limits (the right column above) and you get no optional features. The hope is, if you stay under the minimum limits, then your app will run on all devices that support WebGPU.
But, given the available limits and features listed on the adapter,
you can request them when you call requestDevice
by
passing your desired limits as requiredLimits
and your desired features as requiredFeatures
. For example
const k1Gig = 1024 * 1024 * 1024; const adapter = await navigator.gpu?.requestAdapter(); const device = adapter?.requestDevice({ requiredLimits: { maxBufferSize: k1Gig }, requiredFeatures: [ 'float32-filterable' ], });
Above we’re requesting to be able to use buffers of up to 1gig and to be able to use filterable float32
textures (for example 'rgba32float'
with minFilter set to 'linear'
which by default can only be used with 'nearest'
)
If either of those requests can not be met requestDevice
will fail (reject the promise).
It might be temping to ask for all the limits and features and then check for the ones you need.
Example:
function objLikeToObj(src) { const dst = {}; for (const key in src) { dst[key] = src[key]; } return dst; } // // BAD!!! ? // async function main() { const adapter = await navigator?.gpu.requestAdapter(); const device = await adapter?.requestDevice({ requiredLimits: objLikeToObj(adapter.limits), requiredFeatures: adapter.features, }); if (!device) { fail('need webgpu'); return; } const canUse128KUniformsBuffers = device.limits.maxUniformBufferBindingSize >= 128 * 1024; const canStoreToBGRA8Unorm = device.features.has('bgra8unorm-storage'); const canIndirectFirstInstance = device.features.has('indirect-first-instance'); }
This seems like a simple and clear way to check for limits and features[1]. The
problem with this pattern is you might be accidentally exceeding limits and not
know it. For example lets say you created an 'rgba32float'
texture and filtered it
with 'linear'
filtering.
It would magically just work on your desktop machine because you happened to have
enabled it.
On the user’s phone, your program fails mysteriously because the 'float32-filterable'
feature didn’t exist and you happened to be using it without realizing that it’s
an optional feature.
Or you might allocate a buffer larger the minimum maxBufferSize
and again
not be aware you went over the limit. You ship and a bunch of users can’t run
your page.
The recommended way to use features and limits is to decide on what you absolutely must have and only request those limits.
For example
const adapter = await navigator?.gpu.requestAdapter(); const canUse128KUniformsBuffers = adapter?.limits.maxUniformBufferBindingSize >= 128 * 1024; const canStoreToBGRA8Unorm = adapter?.features.has('bgra8unorm-storage'); const canIndirectFirstInstance = adapter?.features.has('indirect-first-instance'); // if we absolutely need these one or more of these features then fail now if they are not // available if (!canUse128kUniformBuffers) { alert('Sorry, your device is probably too old or underpowered'); return; } // Request the available features and limits we need const device = adapter?.requestDevice({ requiredFeatures: [ ...(canStorageBGRA8Unorm ? ['bgra8unorm'] : []), ...(canIndirectFirstInstance) ? ['indirect-first-instance']), ], requiredLimits: [ maxUniformBufferBindingSize: 128 * 1024, ] });
Doing it this way, if you happen to ask for a Uniform buffer larger than 128k you’ll get an error. Similarly if you happen to try to use a feature you didn’t request you’ll get an error. You can then make a conscience decision if you want to increase your required limits (and therefore refuse to run on more devices) or if you want to keep the limits, or if you want to structure your code to do different things if the features or limits are or are not available.
What is this objLikeToObj
and why do I needed
it? It’s an esoteric Web spec issue. The spec lists requiredLimits
as
record<DOMString, GPUSize64>
. The Web IDL spec says, when converting
an object from something to record<DOMString, GPUSize64>
copy
only the properties that are actually the object’s own properties.
The limits
object on the adapter is listed as an interface
. The
things that appear to be properties there are not properties, they’re
getters that exist on the object prototype, they are not actually the
object’s own properties. So, they aren’t copied
when converted to record<DOMString, GPUSize64>
and so you have
copy them yourself. ↩︎