Skip to content

Forward errors from workers to show a dialog #512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions sample/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}

Expand All @@ -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}.`
);
}
Expand All @@ -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) => {
Expand All @@ -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<string>) => {
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);
}
},
};
}
Expand Down Expand Up @@ -96,6 +129,6 @@ const fail = (() => {
if (!output) output = createErrorOutput();

output.show(message);
throw new Error(message);
return new Error(message);
};
})();
14 changes: 13 additions & 1 deletion sample/worker/main.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]
);
7 changes: 6 additions & 1 deletion sample/worker/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down