API

WebGPU Breaking Changes

This page describes the breaking changes and differences in behavior between WebGL and WebGPU.

We try to avoid breaking changes in the core library as much as possible, but sometimes we cannot avoid them. With the major change that WebGPU introduces as a new engine, you need to be aware of a number of things when porting existing projects to WebGPU.

readPixels is now asynchronous

Probably the biggest change between WebGPU and WebGL is that reading from a texture is asynchronous in WebGPU. As a result, all methods that read pixels from textures, even in WebGL mode, now return a promise:

To match how WebGL works, a flushFramebuffer call is automatically performed before reading a texture to make sure you get up-to-date data. However, if you know your texture is already up to date when you call a readPixels method, you can avoid this flush, and save a small amount of performance, by passing the appropriate parameter to the function call (flushRenderer = false, see docs). Note that if you are doing the read in engine.onEndFrameObservable, you do not need to flush, because this observer runs after the flush for the current frame has been done.

Note also that readPixels is currently slow if width is not divisible by 64. It is also very slow when reading data from half-float textures, so use full-float textures instead when possible. Improving these cases is on our roadmap.

Creation of the WebGPU engine is asynchronous

Creating the engine is also asynchronous in WebGPU. You can do something like this to create a WebGPU engine if the browser supports it, or a WebGL engine otherwise:

import { Engine } from "@babylonjs/core/Engines/engine";
import { WebGPUEngine } from "@babylonjs/core/Engines/webgpuEngine";
async function createEngine() {
const webGPUSupported = await WebGPUEngine.IsSupportedAsync;
if (webGPUSupported) {
const engine = new WebGPUEngine(document.getElementById("renderCanvas"));
await engine.initAsync();
return engine;
}
return new Engine(document.getElementById("renderCanvas"), true);
}

Or you can use the EngineFactory helper. It first tries to create a WebGPU engine if supported, then a WebGL engine, and finally a null engine:

import { EngineFactory } from "@babylonjs/core/Engines/engineFactory";
async function createEngine() {
return EngineFactory.CreateAsync(document.getElementById("renderCanvas"));
}

WebGPU engines intended for WebXR

WebGPU has no post-creation operation that makes an adapter XR-compatible. If the engine may enter WebXR, pass xrCompatible: true when constructing it:

import { WebGPUEngine } from "@babylonjs/core/Engines/webgpuEngine";
const engine = new WebGPUEngine(canvas, { xrCompatible: true });
await engine.initAsync();

WebGPU-XR additionally requires the experimental XRGPUBinding projection path, XRGPUSubImage.prototype.getViewDescriptor, and the WebXR Layers feature. Babylon does not replace an existing WebGPU engine or scene with WebGL if XR entry fails. Applications that offer a fallback must select WebGL before scene and resource creation, or fully dispose and rebuild/reload the application with WebGL. See WebGPU in WebXR.

Shader code differences

Array of textures

Arrays of textures in shader code cannot be accessed with a varying index; the index must be an immediate value. For example, myTextures[0] and myTextures[1] work, but myTextures[i] does not, where i is a loop variable.

Passing samplers to functions

In shaders, you cannot pass samplers to functions:

vec4 getPixel(sampler2D sampler, vec2 uv) {
return texture2D(sampler, uv);
}

Calling this function will fail with a compilation error in WebGPU.

To simplify porting existing code, we have added a pre-pass shader code inliner that replaces a function call with the code of the function itself. You need to tag the function or functions to be inlined with #define inline for the inliner to process them:

#define inline
vec4 getPixel(sampler2D sampler, vec2 uv) {
return texture2D(sampler, uv);
}

Binding values to samplers

WebGPU is less forgiving than WebGL. All sampler variables declared in a shader must have a bound value, even if you do not use that variable. If you get a warning message like "numBindings mismatch", it probably means you defined a uniform sampler variable in the shader code but did not bind a value to it by calling something like setTexture("myvar", texture) on the shader or material.

ShaderMaterial

If you use a custom attribute in a ShaderMaterial, CustomMaterial, or PBRCustomMaterial, it must be declared in the list of attributes used by that shader. For example, for ShaderMaterial, you must pass its name in the attributes array of the options passed to the constructor. In WebGL, you can omit this declaration and it will still work, but that side effect is not really supported.

In WebGL, you could list the same attribute several times when creating a ShaderMaterial, and it would work as if you had provided the attribute only once. In WebGPU, this fails.

Miscellaneous

Unlike WebGL, the viewport cannot extend outside the framebuffer or texture. So, if you call something like:

import { Viewport } from "@babylonjs/core/Maths/math.viewport";
new Viewport(x, y, w, h);
  • x, y, w, h must be >= 0 and <= 1.
  • x + w must be <= 1
  • y + h must be <= 1

If you still want to use out-of-bounds values, you can use a material plugin like in this PG: Viewport out-of-bounds values

The TEXTUREFORMAT_LUMINANCE format is not supported in WebGPU.