diff --git a/sample/util.ts b/sample/util.ts index b36fefb4..3fcec9b3 100644 --- a/sample/util.ts +++ b/sample/util.ts @@ -3,11 +3,15 @@ export function quitIfAdapterNotAvailable( adapter: GPUAdapter | null ): asserts adapter { if (!('gpu' in navigator)) { - fail('navigator.gpu is not defined - WebGPU not available in this browser'); + throw fail( + 'navigator.gpu is not defined - WebGPU not available in this browser' + ); } if (!adapter) { - fail("requestAdapter returned null - this sample can't run on this system"); + throw fail( + "requestAdapter returned null - this sample can't run on this system" + ); } } @@ -21,7 +25,7 @@ export function quitIfLimitLessThan( const limitKey = limit as keyof GPUSupportedLimits; const limitValue = adapter.limits[limitKey] as number; if (limitValue < requiredValue) { - fail( + throw fail( `This sample can't run on this system. ${limit} is ${limitValue}, and this sample requires at least ${requiredValue}.` ); } @@ -39,8 +43,7 @@ export function quitIfWebGPUNotAvailable( ): asserts device { if (!device) { quitIfAdapterNotAvailable(adapter); - fail('Unable to get a device for an unknown reason'); - return; + throw fail('Unable to get a device for an unknown reason'); } device.lost.then((reason) => { @@ -51,16 +54,46 @@ export function quitIfWebGPUNotAvailable( }); } -/** Fail by showing a console error, and dialog box if possible. */ -const fail = (() => { +/** + * Create a MessageChannel, and forward messages to fail() to show an error + * dialog. Return a MessagePort for the worker to send messages back on. + */ +export function mainThreadCreateErrorMessagePortForWorker() { + if (typeof window === 'undefined') throw new Error('Called on wrong thread!'); + const mc = new MessageChannel(); + mc.port1.onmessage = (ev: MessageEvent) => { + fail(ev.data); + }; + return mc.port2; +} + +let errorMessagePort: MessagePort | null = null; +export function workerRegisterErrorMessagePort(port: MessagePort) { + if (typeof window !== 'undefined') throw new Error('Called on wrong thread!'); + errorMessagePort = port; +} + +/** + * Fail by showing a console error, and dialog box if possible. + * + * Returns an Error object, which may be thrown if execution should stop here. + * (Throwing the error will generally trigger one of the global listeners, + * 'unhandledrejection' or 'error', but this won't do anything because the + * dialog is already open at that point, and we don't overwrite it.) + */ +const fail: (message: string) => Error = (() => { type ErrorOutput = { show(msg: string): void }; function createErrorOutput() { if (typeof document === 'undefined') { - // Not implemented in workers. return { show(msg: string) { - console.error(msg); + if (errorMessagePort) { + errorMessagePort.postMessage(msg); + } else { + console.warn('workerRegisterErrorMessagePort has not been called!'); + console.error(msg); + } }, }; } @@ -96,6 +129,6 @@ const fail = (() => { if (!output) output = createErrorOutput(); output.show(message); - throw new Error(message); + return new Error(message); }; })(); diff --git a/sample/worker/main.ts b/sample/worker/main.ts index aa416d52..7e25475e 100644 --- a/sample/worker/main.ts +++ b/sample/worker/main.ts @@ -1,3 +1,5 @@ +import { mainThreadCreateErrorMessagePortForWorker } from '../util'; + const canvas = document.querySelector('canvas') as HTMLCanvasElement; // The web worker is created by passing a path to the worker's source file, which will then be @@ -26,8 +28,18 @@ const devicePixelRatio = window.devicePixelRatio; offscreenCanvas.width = canvas.clientWidth * devicePixelRatio; offscreenCanvas.height = canvas.clientHeight * devicePixelRatio; +// Set up a port for any error messages so we can show them to the user. +const errorMessagePort = mainThreadCreateErrorMessagePortForWorker(); + // Send a message to the worker telling it to initialize WebGPU with the OffscreenCanvas. The // array passed as the second argument here indicates that the OffscreenCanvas is to be // transferred to the worker, meaning this main thread will lose access to it and it will be // fully owned by the worker. -worker.postMessage({ type: 'init', offscreenCanvas }, [offscreenCanvas]); +worker.postMessage( + { + type: 'init', + offscreenCanvas, + errorMessagePort, + }, + [offscreenCanvas, errorMessagePort] +); diff --git a/sample/worker/worker.ts b/sample/worker/worker.ts index 0be8298a..d51b96f3 100644 --- a/sample/worker/worker.ts +++ b/sample/worker/worker.ts @@ -10,7 +10,10 @@ import { import basicVertWGSL from '../../shaders/basic.vert.wgsl'; import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl'; -import { quitIfWebGPUNotAvailable } from '../util'; +import { + quitIfWebGPUNotAvailable, + workerRegisterErrorMessagePort, +} 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 @@ -19,6 +22,8 @@ import { quitIfWebGPUNotAvailable } from '../util'; self.addEventListener('message', (ev) => { switch (ev.data.type) { case 'init': { + workerRegisterErrorMessagePort(ev.data.errorMessagePort); + try { init(ev.data.offscreenCanvas); } catch (err) {