Unable to use WebGPU sample in Chrome Canary

Hi, I am using the following code in Chrome Canary with WebGPU support enabled. I get this error:

Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules (at babylon-basic.js:5:1)

How to solve this? The code to use WebGPU is from here WebGPU Support | Babylon.js Documentation

// Get the canvas DOM element
var canvas = document.getElementById('renderCanvas');
// Load the 3D engine
var engine = new BABYLON.WebGPUEngine(canvas);
await engine.initAsync();

var createScene = function () {
    var scene = new BABYLON.Scene(engine);

    var camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene);

    camera.setTarget(BABYLON.Vector3.Zero());

    camera.attachControl(canvas, true);

    if (!checkComputeShadersSupported(engine, scene)) {
        return scene;
    }

    const simParams = {
        deltaT: 0.04,
        rule1Distance: 0.1,
        rule2Distance: 0.025,
        rule3Distance: 0.025,
        rule1Scale: 0.02,
        rule2Scale: 0.05,
        rule3Scale: 0.005,
    };

    const gui = getGUI();

    const updateSimParams = () => {
        boid.updateSimParams(simParams);
    };

    Object.keys(simParams).forEach((k) => {
        gui.add(simParams, k).onFinishChange(updateSimParams);
    });

    const boid = new Boid(1500, scene);

    updateSimParams();

    scene.onBeforeRenderObservable.add(() => {
        boid.update();
    });

    return scene;
};

function checkComputeShadersSupported(engine, scene) {
    const supportCS = engine.getCaps().supportComputeShaders;

    if (supportCS) {
        return true;
    }

    var panel = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI", true, scene);

    const textNOk = "**Use WebGPU to watch this demo which requires compute shaders support. To enable WebGPU please use Edge Canary or Chrome canary. Also, select the WebGPU engine from the top right drop down menu.**";

    var info = new BABYLON.GUI.TextBlock();
    info.text = textNOk;
    info.width = "100%";
    info.paddingLeft = "5px";
    info.paddingRight = "5px";
    info.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
    info.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
    info.color = supportCS ? "green" : "red";
    info.fontSize = supportCS ? "18px" : "24px";
    info.fontStyle = supportCS ? "" : "bold";
    info.textWrapping = true;
    panel.addControl(info); 

    return false;
}

class Boid {

    constructor(numParticles, scene) {
        const engine = scene.getEngine();

        this.numParticles = numParticles;

        // Create boid mesh
        const boidMesh = BABYLON.MeshBuilder.CreatePlane("plane", { size: 1 }, scene);

        this.mesh = boidMesh;

        boidMesh.forcedInstanceCount = numParticles;

        //const mesh = new BABYLON.Mesh("boid", scene);
        //new BABYLON.Geometry(BABYLON.Geometry.RandomId(), scene, null, false, mesh);

        const mat = new BABYLON.ShaderMaterial("mat", scene, { 
            vertexSource: boidVertexShader,
            fragmentSource: boidFragmentShader,
        }, {
            attributes: ["a_pos", "a_particlePos", "a_particleVel"]
        });

        boidMesh.material = mat;

        const buffSpriteVertex = new BABYLON.VertexBuffer(engine, [-0.01, -0.02, 0.01, -0.02, 0.0, 0.02], "a_pos", false, false, 2, false);

        boidMesh.setIndices([0, 1, 2]);
        boidMesh.setVerticesBuffer(buffSpriteVertex);

        // Create uniform / storage / vertex buffers
        this.simParams = new BABYLON.UniformBuffer(engine, undefined, undefined, "simParams");

        this.simParams.addUniform("deltaT", 1);
        this.simParams.addUniform("rule1Distance", 1);
        this.simParams.addUniform("rule2Distance", 1);
        this.simParams.addUniform("rule3Distance", 1);
        this.simParams.addUniform("rule1Scale", 1);
        this.simParams.addUniform("rule2Scale", 1);
        this.simParams.addUniform("rule3Scale", 1);
        this.simParams.addUniform("numParticles", 1);

        const initialParticleData = new Float32Array(numParticles * 4);
        for (let i = 0; i < numParticles; ++i) {
            initialParticleData[4 * i + 0] = 2 * (Math.random() - 0.5);
            initialParticleData[4 * i + 1] = 2 * (Math.random() - 0.5);
            initialParticleData[4 * i + 2] = 2 * (Math.random() - 0.5) * 0.1;
            initialParticleData[4 * i + 3] = 2 * (Math.random() - 0.5) * 0.1;
        }

        this.particleBuffers = [
            new BABYLON.StorageBuffer(engine, initialParticleData.byteLength, BABYLON.Constants.BUFFER_CREATIONFLAG_VERTEX | BABYLON.Constants.BUFFER_CREATIONFLAG_WRITE),
            new BABYLON.StorageBuffer(engine, initialParticleData.byteLength, BABYLON.Constants.BUFFER_CREATIONFLAG_VERTEX | BABYLON.Constants.BUFFER_CREATIONFLAG_WRITE),
        ];

        this.particleBuffers[0].update(initialParticleData);
        this.particleBuffers[1].update(initialParticleData);

        this.vertexBuffers = [
            [
                new BABYLON.VertexBuffer(engine, this.particleBuffers[0].getBuffer(), "a_particlePos", false, false, 4, true, 0, 2),
                new BABYLON.VertexBuffer(engine, this.particleBuffers[0].getBuffer(), "a_particleVel", false, false, 4, true, 2, 2)
            ],
            [
                new BABYLON.VertexBuffer(engine, this.particleBuffers[1].getBuffer(), "a_particlePos", false, false, 4, true, 0, 2),
                new BABYLON.VertexBuffer(engine, this.particleBuffers[1].getBuffer(), "a_particleVel", false, false, 4, true, 2, 2)
            ]
        ];

        // Create compute shaders
        this.cs1 = new BABYLON.ComputeShader("compute1", engine, { computeSource: boidComputeShader }, {
            bindingsMapping: {
                "params": { group: 0, binding: 0 },
                "particlesA": { group: 0, binding: 1 },
                "particlesB": { group: 0, binding: 2 },
            }
        });
        this.cs1.setUniformBuffer("params", this.simParams);
        this.cs1.setStorageBuffer("particlesA", this.particleBuffers[0]);
        this.cs1.setStorageBuffer("particlesB", this.particleBuffers[1]);

        this.cs2 = new BABYLON.ComputeShader("compute2", engine, { computeSource: boidComputeShader }, {
            bindingsMapping: {
                "params": { group: 0, binding: 0 },
                "particlesA": { group: 0, binding: 1 },
                "particlesB": { group: 0, binding: 2 },
            }
        });
        this.cs2.setUniformBuffer("params", this.simParams);
        this.cs2.setStorageBuffer("particlesA", this.particleBuffers[1]);
        this.cs2.setStorageBuffer("particlesB", this.particleBuffers[0]);

        this.cs = [this.cs1, this.cs2];
        this.t = 0;
    }

    dispose() {
        this.simParams.dispose();
        this.particleBuffers[0].dispose();
        this.particleBuffers[1].dispose();
        this.cs1.dispose();
        this.cs2.dispose();
    }

    updateSimParams(simParams) {
        this.simParams.updateFloat("deltaT", simParams.deltaT);
        this.simParams.updateFloat("rule1Distance", simParams.rule1Distance);
        this.simParams.updateFloat("rule2Distance", simParams.rule2Distance);
        this.simParams.updateFloat("rule3Distance", simParams.rule3Distance);
        this.simParams.updateFloat("rule1Scale", simParams.rule1Scale);
        this.simParams.updateFloat("rule2Scale", simParams.rule2Scale);
        this.simParams.updateFloat("rule3Scale", simParams.rule3Scale);
        this.simParams.updateInt("numParticles", this.numParticles);
        this.simParams.update();
    }

    update() {
        this.cs[this.t].dispatch(Math.ceil(this.numParticles / 64));

        this.mesh.setVerticesBuffer(this.vertexBuffers[this.t][0], false);
        this.mesh.setVerticesBuffer(this.vertexBuffers[this.t][1], false);

        this.t = (this.t + 1) % 2;
    }
}

function getGUI() {
    var oldgui = document.getElementById("datGUI");
    if (oldgui != null) {
        oldgui.remove();
    }

    var gui = new dat.GUI();
    gui.domElement.style.marginTop = "100px";
    gui.domElement.id = "datGUI";

    return gui;
}

const boidVertexShader = `
    attribute vec2 a_pos;
    attribute vec2 a_particlePos;
    attribute vec2 a_particleVel;
    
    void main() {
        float angle = -atan(a_particleVel.x, a_particleVel.y);
        vec2 pos = vec2(
            a_pos.x * cos(angle) - a_pos.y * sin(angle),
            a_pos.x * sin(angle) + a_pos.y * cos(angle)
        );
        gl_Position = vec4(pos + a_particlePos, 0.0, 1.0);
    }
`;

const boidFragmentShader = `
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
`;

const boidComputeShader = `
struct Particle {
  pos : vec2<f32>,
  vel : vec2<f32>,
};
struct SimParams {
  deltaT : f32,
  rule1Distance : f32,
  rule2Distance : f32,
  rule3Distance : f32,
  rule1Scale : f32,
  rule2Scale : f32,
  rule3Scale : f32,
  numParticles: u32,
};
struct Particles {
  particles : array<Particle>,
};
@binding(0) @group(0) var<uniform> params : SimParams;
@binding(1) @group(0) var<storage, read> particlesA : Particles;
@binding(2) @group(0) var<storage, read_write> particlesB : Particles;

// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp
@stage(compute) @workgroup_size(64)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
  var index : u32 = GlobalInvocationID.x;

  if (index >= params.numParticles) {
      return;
  }

  var vPos : vec2<f32> = particlesA.particles[index].pos;
  var vVel : vec2<f32> = particlesA.particles[index].vel;
  var cMass : vec2<f32> = vec2<f32>(0.0, 0.0);
  var cVel : vec2<f32> = vec2<f32>(0.0, 0.0);
  var colVel : vec2<f32> = vec2<f32>(0.0, 0.0);
  var cMassCount : u32 = 0u;
  var cVelCount : u32 = 0u;
  var pos : vec2<f32>;
  var vel : vec2<f32>;

  for (var i : u32 = 0u; i < arrayLength(&particlesA.particles); i = i + 1u) {
    if (i == index) {
      continue;
    }

    pos = particlesA.particles[i].pos.xy;
    vel = particlesA.particles[i].vel.xy;
    if (distance(pos, vPos) < params.rule1Distance) {
      cMass = cMass + pos;
      cMassCount = cMassCount + 1u;
    }
    if (distance(pos, vPos) < params.rule2Distance) {
      colVel = colVel - (pos - vPos);
    }
    if (distance(pos, vPos) < params.rule3Distance) {
      cVel = cVel + vel;
      cVelCount = cVelCount + 1u;
    }
  }
  if (cMassCount > 0u) {
    var temp : f32 = f32(cMassCount);
    cMass = (cMass / vec2<f32>(temp, temp)) - vPos;
  }
  if (cVelCount > 0u) {
    var temp : f32 = f32(cVelCount);
    cVel = cVel / vec2<f32>(temp, temp);
  }
  vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) +
      (cVel * params.rule3Scale);

  // clamp velocity for a more pleasing simulation
  vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
  // kinematic update
  vPos = vPos + (vVel * params.deltaT);
  // Wrap around boundary
  if (vPos.x < -1.0) {
    vPos.x = 1.0;
  }
  if (vPos.x > 1.0) {
    vPos.x = -1.0;
  }
  if (vPos.y < -1.0) {
    vPos.y = 1.0;
  }
  if (vPos.y > 1.0) {
    vPos.y = -1.0;
  }
  // Write back
  particlesB.particles[index].pos = vPos;
  particlesB.particles[index].vel = vVel;
}
`;

This is the same for any Javascript here, not only Babylon. You can not have an await in top level bodies of modules meaning an await in a none async function.

I would advice to either:

  1. put most of the code in engine.initAsync().then(() => {...}) but this won t be sustainable long if you have more async code.
  2. simply wrap all your code in (async () => { ... })() to create a top level async IIFE in your code

Thank you, I wrapped my code in async and no errors anymore but I still have an empty canvas. What could be the error here?

Plenty of things could go wrong I d suggest to make the code work in a playground first.

Also you could wrap your code in a try catch to see any error which would stay stuck in the enclosing promise

I used a try-catch and have no errors. My machines fan is goung up, I think somethings happening but still no animation in my canvas. Any other suggestions? Thank you in advance.

Is your code working in a playground ?

Yes. I copied and pasted this code: https://playground.babylonjs.com/?webgpu#3URR7V#171

I added
var canvas = document.getElementById('renderCanvas'); var engine = new BABYLON.WebGPUEngine(canvas);

to my JS file on top because those were missing. Of course, I made all the code async like you wrote before. I don’t see anything in my canvas. No errors, no babylon sample in canvas.

Can you share your file ?

Of course. I uploaded my HTML sheet, stylesheet and my script to pastebin.

You are currently not initializing the engine or even rendering.

you need to add smthg similar in your script:

  await engine.initAsync();
  var scene = createScene();
  engine.runRenderLoop(() => {
   scene.render();
 })
1 Like

Thank you very much. I had this in my code and removed it due to certain errors. I went through the code again and now it’s working!