From 4e0c6a9b8bbc27dbf5df2a07566cfd561df3587d Mon Sep 17 00:00:00 2001
From: Kai Ninomiya <kainino@chromium.org>
Date: Fri, 14 Jun 2024 19:52:31 -0700
Subject: [PATCH] Show a dialog if WebGPU isn't available or there's an error

This won't catch JS exceptions in the middle of the actual sample, but
it at least shows a nice message when WebGPU is not available for some
reason, or there's an error (the browser doesn't support the sample).
---
 sample/a-buffer/main.ts                 |  4 +-
 sample/animometer/main.ts               |  4 +-
 sample/bitonicSort/utils.ts             | 18 +++--
 sample/cameras/main.ts                  |  4 +-
 sample/computeBoids/main.ts             | 11 ++--
 sample/cornell/main.ts                  | 25 +++----
 sample/cubemap/main.ts                  |  4 +-
 sample/deferredRendering/main.ts        |  4 +-
 sample/fractalCube/main.ts              |  4 +-
 sample/gameOfLife/main.ts               |  4 +-
 sample/helloTriangle/main.ts            |  4 +-
 sample/helloTriangleMSAA/main.ts        |  4 +-
 sample/imageBlur/main.ts                |  4 +-
 sample/instancedCube/main.ts            |  4 +-
 sample/multipleCanvases/main.ts         |  4 +-
 sample/normalMap/main.ts                |  4 +-
 sample/particles/main.ts                |  4 +-
 sample/points/main.ts                   |  4 +-
 sample/renderBundles/main.ts            |  4 +-
 sample/resizeCanvas/main.ts             |  4 +-
 sample/resizeObserverHDDPI/main.ts      |  4 +-
 sample/reversedZ/main.ts                |  4 +-
 sample/rotatingCube/main.ts             |  4 +-
 sample/samplerParameters/main.ts        |  4 +-
 sample/shadowMapping/main.ts            |  4 +-
 sample/skinnedMesh/main.ts              |  4 +-
 sample/textRenderingMsdf/main.ts        |  4 +-
 sample/texturedCube/main.ts             |  4 +-
 sample/twoCubes/main.ts                 |  4 +-
 sample/util.ts                          | 87 +++++++++++++++++++++++++
 sample/videoUploading/main.ts           |  6 +-
 sample/videoUploading/video.ts          |  4 +-
 sample/volumeRenderingTexture3D/main.ts |  4 +-
 sample/worker/worker.ts                 |  4 +-
 34 files changed, 177 insertions(+), 86 deletions(-)
 create mode 100644 sample/util.ts

diff --git a/sample/a-buffer/main.ts b/sample/a-buffer/main.ts
index f4713ffa..ed9011ce 100644
--- a/sample/a-buffer/main.ts
+++ b/sample/a-buffer/main.ts
@@ -1,6 +1,7 @@
 import { mat4, vec3 } from 'wgpu-matrix';
 import { GUI } from 'dat.gui';
 
+import { initDeviceAndErrorDialog } from '../util';
 import { mesh } from '../../meshes/teapot';
 
 import opaqueWGSL from './opaque.wgsl';
@@ -12,8 +13,7 @@ function roundUp(n: number, k: number): number {
 }
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
diff --git a/sample/animometer/main.ts b/sample/animometer/main.ts
index 72e814e5..128e84e9 100644
--- a/sample/animometer/main.ts
+++ b/sample/animometer/main.ts
@@ -1,9 +1,9 @@
 import { GUI } from 'dat.gui';
 import animometerWGSL from './animometer.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const perfDisplayContainer = document.createElement('div');
 perfDisplayContainer.style.color = 'white';
diff --git a/sample/bitonicSort/utils.ts b/sample/bitonicSort/utils.ts
index 371401d9..cdc43357 100644
--- a/sample/bitonicSort/utils.ts
+++ b/sample/bitonicSort/utils.ts
@@ -1,5 +1,6 @@
 import type { GUI } from 'dat.gui';
 import fullscreenTexturedQuad from '../../shaders/fullscreenTexturedQuad.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 type BindGroupBindingLayout =
   | GPUBufferBindingLayout
@@ -111,16 +112,13 @@ export const SampleInitFactoryWebGPU = async (
   callback: SampleInitCallback3D
 ): Promise<SampleInit> => {
   const init = async ({ canvas, gui, stats }) => {
-    const adapter = await navigator.gpu.requestAdapter();
-    const timestampQueryAvailable = adapter.features.has('timestamp-query');
-    let device: GPUDevice;
-    if (timestampQueryAvailable) {
-      device = await adapter.requestDevice({
-        requiredFeatures: ['timestamp-query'],
-      });
-    } else {
-      device = await adapter.requestDevice();
-    }
+    let timestampQueryAvailable;
+    const device = await initDeviceAndErrorDialog({}, (adapter) => {
+      timestampQueryAvailable = adapter.features.has('timestamp-query');
+      return {
+        requiredFeatures: timestampQueryAvailable ? ['timestamp-query'] : [],
+      };
+    });
     const context = canvas.getContext('webgpu') as GPUCanvasContext;
     const devicePixelRatio = window.devicePixelRatio;
     canvas.width = canvas.clientWidth * devicePixelRatio;
diff --git a/sample/cameras/main.ts b/sample/cameras/main.ts
index 648cb61a..9ff195ba 100644
--- a/sample/cameras/main.ts
+++ b/sample/cameras/main.ts
@@ -10,6 +10,7 @@ import {
 import cubeWGSL from './cube.wgsl';
 import { ArcballCamera, WASDCamera } from './camera';
 import { createInputHandler } from './input';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
 
@@ -39,8 +40,7 @@ gui.add(params, 'type', ['arcball', 'WASD']).onChange(() => {
   oldCameraType = newCameraType;
 });
 
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
 const devicePixelRatio = window.devicePixelRatio;
diff --git a/sample/computeBoids/main.ts b/sample/computeBoids/main.ts
index 8a2ea996..02d806f6 100644
--- a/sample/computeBoids/main.ts
+++ b/sample/computeBoids/main.ts
@@ -1,13 +1,16 @@
+import { initDeviceAndErrorDialog } from '../util';
 import spriteWGSL from './sprite.wgsl';
 import updateSpritesWGSL from './updateSprites.wgsl';
 import { GUI } from 'dat.gui';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
 
-const hasTimestampQuery = adapter.features.has('timestamp-query');
-const device = await adapter.requestDevice({
-  requiredFeatures: hasTimestampQuery ? ['timestamp-query'] : [],
+let hasTimestampQuery;
+const device = await initDeviceAndErrorDialog({}, (adapter) => {
+  hasTimestampQuery = adapter.features.has('timestamp-query');
+  return {
+    requiredFeatures: hasTimestampQuery ? ['timestamp-query'] : [],
+  };
 });
 
 const perfDisplayContainer = document.createElement('div');
diff --git a/sample/cornell/main.ts b/sample/cornell/main.ts
index 38a1bcba..39c52b41 100644
--- a/sample/cornell/main.ts
+++ b/sample/cornell/main.ts
@@ -5,21 +5,24 @@ import Radiosity from './radiosity';
 import Rasterizer from './rasterizer';
 import Tonemapper from './tonemapper';
 import Raytracer from './raytracer';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
 
-const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
-const requiredFeatures: GPUFeatureName[] =
-  presentationFormat === 'bgra8unorm' ? ['bgra8unorm-storage'] : [];
-const adapter = await navigator.gpu.requestAdapter();
-for (const feature of requiredFeatures) {
-  if (!adapter.features.has(feature)) {
-    throw new Error(
-      `sample requires ${feature}, but is not supported by the adapter`
-    );
+let presentationFormat;
+const device = await initDeviceAndErrorDialog({}, (adapter) => {
+  presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+  const requiredFeatures: GPUFeatureName[] =
+    presentationFormat === 'bgra8unorm' ? ['bgra8unorm-storage'] : [];
+  for (const feature of requiredFeatures) {
+    if (!adapter.features.has(feature)) {
+      throw new Error(
+        `sample requires ${feature}, but is not supported by the adapter`
+      );
+    }
   }
-}
-const device = await adapter.requestDevice({ requiredFeatures });
+  return { requiredFeatures };
+});
 
 const params: {
   renderer: 'rasterizer' | 'raytracer';
diff --git a/sample/cubemap/main.ts b/sample/cubemap/main.ts
index 7a423ff2..4c207de2 100644
--- a/sample/cubemap/main.ts
+++ b/sample/cubemap/main.ts
@@ -10,10 +10,10 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import sampleCubemapWGSL from './sampleCubemap.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/deferredRendering/main.ts b/sample/deferredRendering/main.ts
index e5d95621..8a535020 100644
--- a/sample/deferredRendering/main.ts
+++ b/sample/deferredRendering/main.ts
@@ -8,14 +8,14 @@ import fragmentWriteGBuffers from './fragmentWriteGBuffers.wgsl';
 import vertexTextureQuad from './vertexTextureQuad.wgsl';
 import fragmentGBuffersDebugView from './fragmentGBuffersDebugView.wgsl';
 import fragmentDeferredRendering from './fragmentDeferredRendering.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const kMaxNumLights = 1024;
 const lightExtentMin = vec3.fromValues(-50, -30, -50);
 const lightExtentMax = vec3.fromValues(50, 50, 50);
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/fractalCube/main.ts b/sample/fractalCube/main.ts
index 35c4fa66..afcc00fa 100644
--- a/sample/fractalCube/main.ts
+++ b/sample/fractalCube/main.ts
@@ -10,10 +10,10 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import sampleSelfWGSL from './sampleSelf.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/gameOfLife/main.ts b/sample/gameOfLife/main.ts
index 3d5589ad..a367159c 100644
--- a/sample/gameOfLife/main.ts
+++ b/sample/gameOfLife/main.ts
@@ -2,10 +2,10 @@ import { GUI } from 'dat.gui';
 import computeWGSL from './compute.wgsl';
 import vertWGSL from './vert.wgsl';
 import fragWGSL from './frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 const devicePixelRatio = window.devicePixelRatio;
diff --git a/sample/helloTriangle/main.ts b/sample/helloTriangle/main.ts
index ab7248f3..683be8f9 100644
--- a/sample/helloTriangle/main.ts
+++ b/sample/helloTriangle/main.ts
@@ -1,9 +1,9 @@
 import triangleVertWGSL from '../../shaders/triangle.vert.wgsl';
 import redFragWGSL from '../../shaders/red.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/helloTriangleMSAA/main.ts b/sample/helloTriangleMSAA/main.ts
index ad34cea5..ae2f24fb 100644
--- a/sample/helloTriangleMSAA/main.ts
+++ b/sample/helloTriangleMSAA/main.ts
@@ -1,9 +1,9 @@
 import triangleVertWGSL from '../../shaders/triangle.vert.wgsl';
 import redFragWGSL from '../../shaders/red.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/imageBlur/main.ts b/sample/imageBlur/main.ts
index b1bab78f..06e1b6f1 100644
--- a/sample/imageBlur/main.ts
+++ b/sample/imageBlur/main.ts
@@ -1,14 +1,14 @@
 import { GUI } from 'dat.gui';
 import blurWGSL from './blur.wgsl';
 import fullscreenTexturedQuadWGSL from '../../shaders/fullscreenTexturedQuad.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 // Contants from the blur.wgsl shader.
 const tileDim = 128;
 const batch = [4, 4];
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/instancedCube/main.ts b/sample/instancedCube/main.ts
index 87265398..f341aafb 100644
--- a/sample/instancedCube/main.ts
+++ b/sample/instancedCube/main.ts
@@ -10,10 +10,10 @@ import {
 
 import instancedVertWGSL from './instanced.vert.wgsl';
 import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/multipleCanvases/main.ts b/sample/multipleCanvases/main.ts
index 45a1f542..10c3b67c 100644
--- a/sample/multipleCanvases/main.ts
+++ b/sample/multipleCanvases/main.ts
@@ -1,6 +1,7 @@
 /* eslint-disable prettier/prettier */
 import { mat4, mat3 } from 'wgpu-matrix';
 import { modelData } from './models';
+import { initDeviceAndErrorDialog } from '../util';
 
 type TypedArrayView = Float32Array | Uint32Array;
 
@@ -46,8 +47,7 @@ function createVertexAndIndexBuffer(
   };
 }
 
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const models = Object.values(modelData).map(data => createVertexAndIndexBuffer(device, data));
 
diff --git a/sample/normalMap/main.ts b/sample/normalMap/main.ts
index fe4f8189..ea929b0e 100644
--- a/sample/normalMap/main.ts
+++ b/sample/normalMap/main.ts
@@ -8,6 +8,7 @@ import {
   create3DRenderPipeline,
   createTextureFromImage,
 } from './utils';
+import { initDeviceAndErrorDialog } from '../util';
 
 const MAT4X4_BYTES = 64;
 enum TextureAtlas {
@@ -17,8 +18,7 @@ enum TextureAtlas {
 }
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 const devicePixelRatio = window.devicePixelRatio;
 canvas.width = canvas.clientWidth * devicePixelRatio;
diff --git a/sample/particles/main.ts b/sample/particles/main.ts
index e718fd91..a25d8819 100644
--- a/sample/particles/main.ts
+++ b/sample/particles/main.ts
@@ -3,6 +3,7 @@ import { GUI } from 'dat.gui';
 
 import particleWGSL from './particle.wgsl';
 import probabilityMapWGSL from './probabilityMap.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const numParticles = 50000;
 const particlePositionOffset = 0;
@@ -16,8 +17,7 @@ const particleInstanceByteSize =
   0;
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/points/main.ts b/sample/points/main.ts
index 6926af7d..323231d9 100644
--- a/sample/points/main.ts
+++ b/sample/points/main.ts
@@ -5,6 +5,7 @@ import distanceSizedPointsVertWGSL from './distance-sized-points.vert.wgsl';
 import fixedSizePointsVertWGSL from './fixed-size-points.vert.wgsl';
 import orangeFragWGSL from './orange.frag.wgsl';
 import texturedFragWGSL from './textured.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 // See: https://www.google.com/search?q=fibonacci+sphere
 function createFibonacciSphereVertices({
@@ -28,8 +29,7 @@ function createFibonacciSphereVertices({
   return new Float32Array(vertices);
 }
 
-const adapter = await navigator.gpu?.requestAdapter();
-const device = await adapter?.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 // Get a WebGPU context from the canvas and configure it
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
diff --git a/sample/renderBundles/main.ts b/sample/renderBundles/main.ts
index 2d50bcfb..197157f0 100644
--- a/sample/renderBundles/main.ts
+++ b/sample/renderBundles/main.ts
@@ -4,6 +4,7 @@ import { createSphereMesh, SphereLayout } from '../../meshes/sphere';
 import Stats from 'stats.js';
 
 import meshWGSL from './mesh.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 interface Renderable {
   vertices: GPUBuffer;
@@ -13,8 +14,7 @@ interface Renderable {
 }
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const settings = {
   useRenderBundles: true,
diff --git a/sample/resizeCanvas/main.ts b/sample/resizeCanvas/main.ts
index bf73f901..364bab61 100644
--- a/sample/resizeCanvas/main.ts
+++ b/sample/resizeCanvas/main.ts
@@ -1,9 +1,9 @@
 import triangleVertWGSL from '../../shaders/triangle.vert.wgsl';
 import redFragWGSL from '../../shaders/red.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/resizeObserverHDDPI/main.ts b/sample/resizeObserverHDDPI/main.ts
index fac614d0..5b089fa4 100644
--- a/sample/resizeObserverHDDPI/main.ts
+++ b/sample/resizeObserverHDDPI/main.ts
@@ -1,9 +1,9 @@
 import { GUI } from 'dat.gui';
 import checkerWGSL from './checker.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/reversedZ/main.ts b/sample/reversedZ/main.ts
index bda1c2d7..bd1922d6 100644
--- a/sample/reversedZ/main.ts
+++ b/sample/reversedZ/main.ts
@@ -8,6 +8,7 @@ import vertexTextureQuadWGSL from './vertexTextureQuad.wgsl';
 import fragmentTextureQuadWGSL from './fragmentTextureQuad.wgsl';
 import vertexPrecisionErrorPassWGSL from './vertexPrecisionErrorPass.wgsl';
 import fragmentPrecisionErrorPassWGSL from './fragmentPrecisionErrorPass.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 // Two planes close to each other for depth precision test
 const geometryVertexSize = 4 * 8; // Byte size of one geometry vertex.
@@ -65,8 +66,7 @@ const depthClearValues = {
 };
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/rotatingCube/main.ts b/sample/rotatingCube/main.ts
index 5dd83191..bf1554f4 100644
--- a/sample/rotatingCube/main.ts
+++ b/sample/rotatingCube/main.ts
@@ -10,10 +10,10 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/samplerParameters/main.ts b/sample/samplerParameters/main.ts
index 4917ecad..6bc8bc4d 100644
--- a/sample/samplerParameters/main.ts
+++ b/sample/samplerParameters/main.ts
@@ -3,6 +3,7 @@ import { GUI } from 'dat.gui';
 
 import texturedSquareWGSL from './texturedSquare.wgsl';
 import showTextureWGSL from './showTexture.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const kMatrices: Readonly<Float32Array> = new Float32Array([
   // Row 1: Scale by 2
@@ -27,8 +28,7 @@ const kMatrices: Readonly<Float32Array> = new Float32Array([
 ]);
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 //
 // GUI controls
diff --git a/sample/shadowMapping/main.ts b/sample/shadowMapping/main.ts
index fd94c6d2..c3873a69 100644
--- a/sample/shadowMapping/main.ts
+++ b/sample/shadowMapping/main.ts
@@ -4,12 +4,12 @@ import { mesh } from '../../meshes/stanfordDragon';
 import vertexShadowWGSL from './vertexShadow.wgsl';
 import vertexWGSL from './vertex.wgsl';
 import fragmentWGSL from './fragment.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const shadowDepthTextureSize = 1024;
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/skinnedMesh/main.ts b/sample/skinnedMesh/main.ts
index 8203889e..d9349d6b 100644
--- a/sample/skinnedMesh/main.ts
+++ b/sample/skinnedMesh/main.ts
@@ -9,6 +9,7 @@ import {
   createSkinnedGridRenderPipeline,
 } from './gridUtils';
 import { gridIndices } from './gridData';
+import { initDeviceAndErrorDialog } from '../util';
 
 const MAT4X4_BYTES = 64;
 
@@ -94,8 +95,7 @@ const getRotation = (mat: Mat4): Quat => {
 
 //Normal setup
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/textRenderingMsdf/main.ts b/sample/textRenderingMsdf/main.ts
index 0e13daab..3f1aaad1 100644
--- a/sample/textRenderingMsdf/main.ts
+++ b/sample/textRenderingMsdf/main.ts
@@ -11,10 +11,10 @@ import { MsdfTextRenderer } from './msdfText';
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/texturedCube/main.ts b/sample/texturedCube/main.ts
index 73442116..c5601d5d 100644
--- a/sample/texturedCube/main.ts
+++ b/sample/texturedCube/main.ts
@@ -10,10 +10,10 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import sampleTextureMixColorWGSL from './sampleTextureMixColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/twoCubes/main.ts b/sample/twoCubes/main.ts
index 4a9f4374..63cc2adc 100644
--- a/sample/twoCubes/main.ts
+++ b/sample/twoCubes/main.ts
@@ -10,10 +10,10 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
diff --git a/sample/util.ts b/sample/util.ts
new file mode 100644
index 00000000..ba1ca92c
--- /dev/null
+++ b/sample/util.ts
@@ -0,0 +1,87 @@
+/** Asserts a condition is truthy; throws an Error if not. */
+export function assert(condition: unknown, msg?: string): asserts condition {
+  if (!condition) {
+    throw new Error(msg);
+  }
+}
+
+/**
+ * Tries to get a GPUDevice. Shows an error dialog if initialization fails, or
+ * if later there is a device loss or uncaptured error.
+ */
+export async function initDeviceAndErrorDialog(
+  requestAdapterOptions: GPURequestAdapterOptions | undefined = undefined,
+  computeDeviceDescriptor: (
+    adapter: GPUAdapter
+  ) => GPUDeviceDescriptor | undefined = () => undefined
+): Promise<GPUDevice> {
+  const dialog = initDialog();
+  try {
+    assert(
+      'gpu' in navigator,
+      'WebGPU is not available in this browser (navigator.gpu is undefined)'
+    );
+
+    const adapter = await navigator.gpu.requestAdapter(requestAdapterOptions);
+    assert(
+      adapter,
+      'WebGPU is not available on this system (requestAdapter() returned null)'
+    );
+
+    const deviceDescriptor = computeDeviceDescriptor(adapter);
+    const device = await adapter.requestDevice(deviceDescriptor);
+    device.lost.then((reason) => {
+      dialog.show(`Device lost ("${reason.reason}"):\n${reason.message}`);
+    });
+    device.onuncapturederror = (error) => {
+      dialog.show(`Uncaptured error:\n${error.error.message}`);
+    };
+
+    dialog.close();
+    return device;
+  } catch (ex) {
+    dialog.show(
+      ex instanceof Error ? (ex.message ? ex.message : ex.stack) : 'error'
+    );
+    throw ex;
+  }
+}
+
+function initDialog() {
+  if (typeof document === 'undefined') {
+    // Not implemented in workers.
+    return {
+      show(msg: string) {
+        console.error(msg);
+      },
+      close() {},
+    };
+  }
+
+  const dialogBox = document.createElement('dialog');
+  document.body.append(dialogBox);
+
+  const dialogText = document.createElement('pre');
+  dialogText.style.whiteSpace = 'pre-wrap';
+  dialogBox.append(dialogText);
+
+  const closeBtn = document.createElement('button');
+  closeBtn.textContent = 'OK';
+  closeBtn.onclick = () => dialogBox.close();
+  dialogBox.append(closeBtn);
+
+  return {
+    show(msg: string) {
+      // Don't overwrite the dialog message while it's still open
+      // (show the first error, not the most recent error).
+      if (!dialogBox.open) {
+        dialogText.textContent = msg;
+        dialogBox.showModal();
+      }
+    },
+    close() {
+      dialogText.textContent = '';
+      dialogBox.close();
+    },
+  };
+}
diff --git a/sample/videoUploading/main.ts b/sample/videoUploading/main.ts
index 35283202..5f991e8a 100644
--- a/sample/videoUploading/main.ts
+++ b/sample/videoUploading/main.ts
@@ -1,6 +1,9 @@
 import { GUI } from 'dat.gui';
 import fullscreenTexturedQuadWGSL from '../../shaders/fullscreenTexturedQuad.wgsl';
 import sampleExternalTextureWGSL from '../../shaders/sampleExternalTexture.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
+
+const device = await initDeviceAndErrorDialog();
 
 // Set video element
 const video = document.createElement('video');
@@ -10,9 +13,6 @@ video.muted = true;
 video.src = '../../assets/video/pano.webm';
 await video.play();
 
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
-
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 const devicePixelRatio = window.devicePixelRatio;
diff --git a/sample/videoUploading/video.ts b/sample/videoUploading/video.ts
index 3c79d578..0a3d6d3b 100644
--- a/sample/videoUploading/video.ts
+++ b/sample/videoUploading/video.ts
@@ -1,6 +1,7 @@
 import { GUI } from 'dat.gui';
 import fullscreenTexturedQuadWGSL from '../../shaders/fullscreenTexturedQuad.wgsl';
 import sampleExternalTextureWGSL from '../../shaders/sampleExternalTexture.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 export default async function ({ useVideoFrame }: { useVideoFrame: boolean }) {
   // Set video element
@@ -11,8 +12,7 @@ export default async function ({ useVideoFrame }: { useVideoFrame: boolean }) {
   video.src = '../../assets/video/pano.webm';
   await video.play();
 
-  const adapter = await navigator.gpu.requestAdapter();
-  const device = await adapter.requestDevice();
+  const device = await initDeviceAndErrorDialog();
 
   const canvas = document.querySelector('canvas') as HTMLCanvasElement;
   const context = canvas.getContext('webgpu') as GPUCanvasContext;
diff --git a/sample/volumeRenderingTexture3D/main.ts b/sample/volumeRenderingTexture3D/main.ts
index 3b36ec74..eee4de0c 100644
--- a/sample/volumeRenderingTexture3D/main.ts
+++ b/sample/volumeRenderingTexture3D/main.ts
@@ -1,6 +1,7 @@
 import { mat4 } from 'wgpu-matrix';
 import { GUI } from 'dat.gui';
 import volumeWGSL from './volume.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 const canvas = document.querySelector('canvas') as HTMLCanvasElement;
 
@@ -17,8 +18,7 @@ gui.add(params, 'rotateCamera', true);
 gui.add(params, 'near', 2.0, 7.0);
 gui.add(params, 'far', 2.0, 7.0);
 
-const adapter = await navigator.gpu.requestAdapter();
-const device = await adapter.requestDevice();
+const device = await initDeviceAndErrorDialog();
 const context = canvas.getContext('webgpu') as GPUCanvasContext;
 
 const sampleCount = 4;
diff --git a/sample/worker/worker.ts b/sample/worker/worker.ts
index 4a3e52f9..d07f6a97 100644
--- a/sample/worker/worker.ts
+++ b/sample/worker/worker.ts
@@ -10,6 +10,7 @@ import {
 
 import basicVertWGSL from '../../shaders/basic.vert.wgsl';
 import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
+import { initDeviceAndErrorDialog } from '../util';
 
 // The worker process can instantiate a WebGPU device immediately, but it still needs an
 // OffscreenCanvas to be able to display anything. Here we listen for an 'init' message from the
@@ -34,8 +35,7 @@ self.addEventListener('message', (ev) => {
 // to the init() method for all the other samples. The remainder of this file is largely identical
 // to the rotatingCube sample.
 async function init(canvas) {
-  const adapter = await navigator.gpu.requestAdapter();
-  const device = await adapter.requestDevice();
+  const device = await initDeviceAndErrorDialog();
   const context = canvas.getContext('webgpu');
 
   const presentationFormat = navigator.gpu.getPreferredCanvasFormat();