Skip to content

Commit 0d9da4a

Browse files
committed
Forward errors from workers to show a dialog
1 parent 2fa0137 commit 0d9da4a

File tree

3 files changed

+62
-12
lines changed

3 files changed

+62
-12
lines changed

sample/util.ts

+43-10
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ export function quitIfAdapterNotAvailable(
33
adapter: GPUAdapter | null
44
): asserts adapter {
55
if (!('gpu' in navigator)) {
6-
fail('navigator.gpu is not defined - WebGPU not available in this browser');
6+
throw fail(
7+
'navigator.gpu is not defined - WebGPU not available in this browser'
8+
);
79
}
810

911
if (!adapter) {
10-
fail("requestAdapter returned null - this sample can't run on this system");
12+
throw fail(
13+
"requestAdapter returned null - this sample can't run on this system"
14+
);
1115
}
1216
}
1317

@@ -21,7 +25,7 @@ export function quitIfLimitLessThan(
2125
const limitKey = limit as keyof GPUSupportedLimits;
2226
const limitValue = adapter.limits[limitKey] as number;
2327
if (limitValue < requiredValue) {
24-
fail(
28+
throw fail(
2529
`This sample can't run on this system. ${limit} is ${limitValue}, and this sample requires at least ${requiredValue}.`
2630
);
2731
}
@@ -39,8 +43,7 @@ export function quitIfWebGPUNotAvailable(
3943
): asserts device {
4044
if (!device) {
4145
quitIfAdapterNotAvailable(adapter);
42-
fail('Unable to get a device for an unknown reason');
43-
return;
46+
throw fail('Unable to get a device for an unknown reason');
4447
}
4548

4649
device.lost.then((reason) => {
@@ -51,16 +54,46 @@ export function quitIfWebGPUNotAvailable(
5154
});
5255
}
5356

54-
/** Fail by showing a console error, and dialog box if possible. */
55-
const fail = (() => {
57+
/**
58+
* Create a MessageChannel, and forward messages to fail() to show an error
59+
* dialog. Return a MessagePort for the worker to send messages back on.
60+
*/
61+
export function mainThreadCreateErrorMessagePortForWorker() {
62+
if (typeof window === 'undefined') throw new Error('Called on wrong thread!');
63+
const mc = new MessageChannel();
64+
mc.port1.onmessage = (ev: MessageEvent<string>) => {
65+
fail(ev.data);
66+
};
67+
return mc.port2;
68+
}
69+
70+
let errorMessagePort: MessagePort | null = null;
71+
export function workerRegisterErrorMessagePort(port: MessagePort) {
72+
if (typeof window !== 'undefined') throw new Error('Called on wrong thread!');
73+
errorMessagePort = port;
74+
}
75+
76+
/**
77+
* Fail by showing a console error, and dialog box if possible.
78+
*
79+
* Returns an Error object, which may be thrown if execution should stop here.
80+
* (Throwing the error will generally trigger one of the global listeners,
81+
* 'unhandledrejection' or 'error', but this won't do anything because the
82+
* dialog is already open at that point, and we don't overwrite it.)
83+
*/
84+
const fail: (message: string) => Error = (() => {
5685
type ErrorOutput = { show(msg: string): void };
5786

5887
function createErrorOutput() {
5988
if (typeof document === 'undefined') {
60-
// Not implemented in workers.
6189
return {
6290
show(msg: string) {
63-
console.error(msg);
91+
if (errorMessagePort) {
92+
errorMessagePort.postMessage(msg);
93+
} else {
94+
console.warn('workerRegisterErrorMessagePort has not been called!');
95+
console.error(msg);
96+
}
6497
},
6598
};
6699
}
@@ -96,6 +129,6 @@ const fail = (() => {
96129
if (!output) output = createErrorOutput();
97130

98131
output.show(message);
99-
throw new Error(message);
132+
return new Error(message);
100133
};
101134
})();

sample/worker/main.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { mainThreadCreateErrorMessagePortForWorker } from '../util';
2+
13
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
24

35
// 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;
2628
offscreenCanvas.width = canvas.clientWidth * devicePixelRatio;
2729
offscreenCanvas.height = canvas.clientHeight * devicePixelRatio;
2830

31+
// Set up a port for any error messages so we can show them to the user.
32+
const errorMessagePort = mainThreadCreateErrorMessagePortForWorker();
33+
2934
// Send a message to the worker telling it to initialize WebGPU with the OffscreenCanvas. The
3035
// array passed as the second argument here indicates that the OffscreenCanvas is to be
3136
// transferred to the worker, meaning this main thread will lose access to it and it will be
3237
// fully owned by the worker.
33-
worker.postMessage({ type: 'init', offscreenCanvas }, [offscreenCanvas]);
38+
worker.postMessage(
39+
{
40+
type: 'init',
41+
offscreenCanvas,
42+
errorMessagePort,
43+
},
44+
[offscreenCanvas, errorMessagePort]
45+
);

sample/worker/worker.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010

1111
import basicVertWGSL from '../../shaders/basic.vert.wgsl';
1212
import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
13-
import { quitIfWebGPUNotAvailable } from '../util';
13+
import {
14+
quitIfWebGPUNotAvailable,
15+
workerRegisterErrorMessagePort,
16+
} from '../util';
1417

1518
// The worker process can instantiate a WebGPU device immediately, but it still needs an
1619
// OffscreenCanvas to be able to display anything. Here we listen for an 'init' message from the
@@ -19,6 +22,8 @@ import { quitIfWebGPUNotAvailable } from '../util';
1922
self.addEventListener('message', (ev) => {
2023
switch (ev.data.type) {
2124
case 'init': {
25+
workerRegisterErrorMessagePort(ev.data.errorMessagePort);
26+
2227
try {
2328
init(ev.data.offscreenCanvas);
2429
} catch (err) {

0 commit comments

Comments
 (0)