Issues initializing WebGPU engine from a web worker

First off, I’m newish to Babylon, but already love the engine and community.
I’ve searched for this with no answers. Additionally Playground example is impossible to provide for this issue.

I am trying to setup an offscreen canvas with WebGPU engine. WebGL2 initializes from my worker without issues. The first hurdle was the assumption WebGPU init makes that its running on the main thread and tries to dynamically resolve glslang module using importScripts unilaterally.

Arriving at this error:

TypeError: Failed to execute 'importScripts' on 'WorkerGlobalScope': Module scripts don't support importScripts().

With importScripts not being available via a worker.

So I try to manually create a glslang module instance (tried both JS & WASM modules) and define these in the engine constructor method using: glslangOptions{…}. This got me a little further but now its trying to resolve twgsl module using importScript and the same error above is being reported.

I see little to no documentation on this. Is offscreen canvas supported with WebGPU engine?
Why is WebGPU engine not already include these modules in the distro? Importing them dynamically from a CDN every time seems wasteful.

Anyone can point in the right direction or provide an example for what I am trying to achieve?

Cheers.

Maybe you are using a module worker? Change worker type to classic to see if it helps.

1 Like

Firs thing I assumed as well, I’ve tried both Module and Classic for the worker. Same result unfortunately. Im sure it would be fine it I saw an example on how to correctly provide glslang & twgsl instances. But for the life of me cannot get this to work. :frowning:

I believe this is being caused by Vite not correctly bundling my worker code as a classic type. I’ll try fix this first and see where I’m at.

Indeed, you will have to use a classic worker, where importScripts does work.

Unfortunately, we currently depend on these external dependencies (glslang, twgsl), but we hope that one day we will be able to get rid of them… Note that you can provide your own urls, so you don’t have to use the default CDNs, you can also host them in your local server.

1 Like

Classic workers are fine, I just need to setup dedicated build and bundling processes for it, although its a much nicer dev experience with vite and vite-comlink-plugin as I don’t have to worry about wrapping or exposing my exported methods in the worker. Get much nicer type inference, and interoperability. This is turning into a time vampire.

Regarding providing glslang and twgsl module instances manually to circumvent importScript dependencies. Do you know or know where I can find a example on how to achieve this? As it seems like this is the intention of those options in webGpuEngine constructor.

From Babylons API docs:

glslang?: any
Defines an existing instance of Glslang (useful in modules who do not access the global instance).

You can use the jsPath and wasmPath properties to provide a (local) path to these files, instead of glslang. By default, these paths are https://preview.babylonjs.com/glslang/glslang.js and https://preview.babylonjs.com/glslang/glslang.wasm respetively.

Same thing for Twgsl, the paths are https://preview.babylonjs.com/twgsl/twgsl.js and https://preview.babylonjs.com/twgsl/twgsl.wasm by default.

I don’t have any example, though, as I don’t think anyone tested WebGPU in a worker thread yet.

[…] Ah sorry, you don’t want to use importScript

Here’s how the glslang object is created when you pass the js and wasm path:

if (IsWindowObjectExist()) {
    return Tools.LoadScriptAsync(glslangOptions.jsPath).then(() => {
        return (self as any).glslang(glslangOptions!.wasmPath);
    });
} else {
    importScripts(glslangOptions.jsPath);
    return (self as any).glslang(glslangOptions!.wasmPath);
}

The returned object is the object you should set to the glslang option property.

And for Twgsl:

if (twgslOptions.jsPath && twgslOptions.wasmPath) {
    if (IsWindowObjectExist()) {
        await Tools.LoadScriptAsync(twgslOptions.jsPath);
    } else {
        importScripts(twgslOptions.jsPath);
    }
}

_twgsl = await (self as any).twgsl(twgslOptions!.wasmPath);

_twgsl is the object you should set to the twgsl option property.

Of course, you will have to replace the LoadScriptAsync / importScripts by any other way to bring the corresponding file/code into the worker.

1 Like

Thanks for the input. I got it working with Vite and as module worker after some trial and error.

For anyone else trying to get WebGPU running in offscreen canvas and bundling under Vite.

My vite.config.js looks like this:

import { defineConfig } from 'vite';
import { comlink } from 'vite-plugin-comlink';
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

export default defineConfig({
    plugins: [
        viteCommonjs(),
        wasm(),
        topLevelAwait(),
        {
            name: "configure-response-headers",
            configureServer: (server) => {
                server.middlewares.use((_req, res, next) => {
                res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
                res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
                next();
                });
            },
        },
        comlink(),
    ],
    worker: {
        plugins: [
            viteCommonjs(),
            wasm(),
            topLevelAwait(),    
            comlink(),
            {
                name: "configure-response-headers",
                configureServer: (server) => {
                    server.middlewares.use((_req, res, next) => {
                    res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
                    res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
                    next();
                    });
                },
            },
        ],
    },
    server: {
        port: 3000,
    },
    build: {
        target: 'esnext',
    },
});

viteCommonjs plugin is needed to deal with the glslang and twgls wasm modules and their js factory wrappers.
All other plugins are required as well. You’ll see I am using comlink + vite-plugin-comlink for auto wrapping and exposing of worker methods with type inferring & easy async handling between main and worker threads.

Worker code for initializing babylon WebGPUEngine within a module worker (no importScript required)

import * as BABYLON from "@babylonjs/core";
import { EngineMessage } from "../Game";
import glslangWasm from '../Libs/glslang.wasm?url';
import glslang from '../Libs/glslang';
import twgslWasm from '../Libs/twgsl.wasm?url';
import twgsl from '../Libs/twgsl';

let _engine: BABYLON.WebGPUEngine | null = null;
let _canvas: HTMLCanvasElement | null = null;
let _callbackState: ((message: EngineMessage) => void) | null = null;
let _glslangModule: any = null;
let _twgslModule: any = null;

export const InitEngine = async (canvas: HTMLCanvasElement) => {
    try {
        if(!_callbackState) throw new Error("State callback not registered");

        const webgpuSupported = await  BABYLON.WebGPUEngine.IsSupportedAsync;

        if(!webgpuSupported) throw new Error("WebGPU not supported");

        _glslangModule = await glslang(glslangWasm);
        _twgslModule = await twgsl(twgslWasm);
        _canvas = canvas;
        _engine = new BABYLON.WebGPUEngine(canvas, {
            twgslOptions: { twgsl: _twgslModule },
            glslangOptions: { glslang: _glslangModule }
        });

        await _engine.initAsync();
        _callbackState(EngineMessage.Init);
    } catch (err) {
        console.error("Engine Init Error", err);
        _callbackState(EngineMessage.Error);
    }
}

You’ll see the wasm modules are imported with ‘?url’ postfix. Afterwards wasm modules are loaded simply like so await glslang(glslangWasm)…

Ignore _callbackState method, just a state update method for reporting to main thread.

4 Likes

I tested listening for events on the main thread and sending them to the child thread, but there was a perceived interaction delay. Do you have a good way for Webworkers to listen to events from the main thread?

Hows does your main thread code look ? Are you creating an OffscreenCanvas with new OffscreenCanvas() etc and passing that through?