WebGPU Fallback

Feature-Detecting and Falling Back from WebGPU to WebGL

navigator.gpu can exist with no usable adapter behind it (Power Preference). WebGPUEngine.IsSupportedAsync, a static promise, requests an adapter and resolves to true only if one arrives. Test it before creating any scene, since a scene cannot move to another engine:

Choosing WebGPU when it works and WebGL 2 otherwiseJavaScript
async function createEngine(canvas) {
  if (await BABYLON.WebGPUEngine.IsSupportedAsync) {
    const engine = new BABYLON.WebGPUEngine(canvas, { antialias: true });
    await engine.initAsync();
    return engine;
  }
  return new BABYLON.Engine(canvas, true);              // WebGL 2, or WebGL 1 as a last resort
}
createEngine(view).then((engine) => console.log(`gpu in navigator: ${'gpu' in navigator}, ` +
  `engine: ${engine.description}`));

Run normally, then with Chrome 1 's --disable-gpu switch, which keeps navigator.gpu but removes the adapter:

Output of 5
BJS - [15:36:24]: Babylon.js v9.28.0 - WebGPU1 engine
gpu in navigator: true, engine: WebGPU1
BJS - [15:36:31]: Babylon.js v9.28.0 - WebGL2
gpu in navigator: true, engine: WebGL2

The second run fell back to WebGL 2 on SwiftShader 2,546 (SwiftShader). BABYLON.EngineFactory.CreateAsync(canvas) packages the same logic, ending with a NullEngine. Guard WebGPU-only code with engine.isWebGPU, and test both paths.

Choosing WebGPUEngine when an adapter exists, WebGL 2 otherwise, and showing which wonHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 330px; display: block; touch-action: none; }
  #info { max-width: 600px; padding: 6px 8px; font-family: monospace; white-space: pre; }
</style>
<canvas id="view"></canvas>
<div id="info">Detecting…</div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const canvas = document.getElementById('view');
const info = document.getElementById('info');

async function createEngine(canvas) {
  // navigator.gpu can exist with no adapter behind it; IsSupportedAsync asks for one
  if (await BABYLON.WebGPUEngine.IsSupportedAsync) {
    try {
      const engine = new BABYLON.WebGPUEngine(canvas, { antialias: true });
      await engine.initAsync();
      return engine;
    } catch (error) {
      console.log('WebGPU init failed, falling back: ' + error.message);
    }
  }
  return new BABYLON.Engine(canvas, true);            // WebGL 2, or WebGL 1 as a last resort
}

createEngine(canvas).then((engine) => {
  // Decide the engine BEFORE creating the scene: a scene cannot move to another engine
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = BABYLON.Color4.FromHexString(engine.isWebGPU ? '#e3ecf5ff' : '#f3e7d8ff');
  const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2.5, 1.2, 4, BABYLON.Vector3.Zero(), scene);
  camera.attachControl(canvas, true);
  new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
  const book = BABYLON.MeshBuilder.CreateBox('book', { width: 1, height: 1.5, depth: 0.25 }, scene);
  const cover = new BABYLON.StandardMaterial('cover', scene);
  // Guard backend-specific choices with engine.isWebGPU
  cover.diffuseColor = BABYLON.Color3.FromHexString(engine.isWebGPU ? '#1f5f8b' : '#b5452f');
  book.material = cover;

  info.textContent = `'gpu' in navigator: ${'gpu' in navigator}\n` +
    `engine.isWebGPU:    ${!!engine.isWebGPU}\n` +
    `engine.description: ${engine.description}`;
  engine.runRenderLoop(() => { book.rotation.y += 0.01; scene.render(); });
  addEventListener('resize', () => engine.resize());
});
</script>