Skip to content

Commit

Permalink
Merge pull request #289 from webgpu/worker
Browse files Browse the repository at this point in the history
Added a sample that shows WebGPU running from a web worker
  • Loading branch information
toji authored Aug 10, 2023
2 parents 914e885 + ae3e65f commit 8c86ccf
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 7 deletions.
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"scripts": {
"lint": "eslint --ext .ts,.tsx src/",
"fix": "eslint --fix --ext .ts,.tsx src/",
"start": "next dev",
"build": "next build",
"serve": "next start",
Expand Down Expand Up @@ -44,6 +45,6 @@
"eslint-plugin-react": "^7.31.10",
"prettier": "^2.7.1",
"raw-loader": "^4.0.2",
"typescript": "^4.8.4"
"typescript": "^4.9.5"
}
}
1 change: 1 addition & 0 deletions src/pages/samples/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const pages: PageComponentType = {
cornell: dynamic(() => import('../../sample/cornell/main')),
gameOfLife: dynamic(() => import('../../sample/gameOfLife/main')),
renderBundles: dynamic(() => import('../../sample/renderBundles/main')),
worker: dynamic(() => import('../../sample/worker/main')),
};

function Page({ slug }: Props): JSX.Element {
Expand Down
91 changes: 91 additions & 0 deletions src/sample/worker/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { makeSample, SampleInit } from '../../components/SampleLayout';

const init: SampleInit = async ({ canvas, pageState }) => {
if (!pageState.active) return;

// The web worker is created by passing a path to the worker's source file, which will then be
// executed on a separate thread.
const worker = new Worker(new URL('./worker.ts', import.meta.url));

// The primary way to communicate with the worker is to send and receive messages.
worker.addEventListener('message', (ev) => {
// The format of the message can be whatever you'd like, but it's helpful to decide on a
// consistent convention so that you can tell the message types apart as your apps grow in
// complexity. Here we establish a convention that all messages to and from the worker will
// have a `type` field that we can use to determine the content of the message.
switch (ev.data.type) {
case 'log': {
// Workers don't have a built-in mechanism for logging to the console, so it's useful to
// create a way to echo console messages.
console.log(ev.data.message);
break;
}
default: {
console.error(`Unknown Message Type: ${ev.data.type}`);
}
}
});

try {
// In order for the worker to display anything on the page, an OffscreenCanvas must be used.
// Here we can create one from our normal canvas by calling transferControlToOffscreen().
// Anything drawn to the OffscreenCanvas that call returns will automatically be displayed on
// the source canvas on the page.
const offscreenCanvas = canvas.transferControlToOffscreen();
const devicePixelRatio = window.devicePixelRatio || 1;
offscreenCanvas.width = canvas.clientWidth * devicePixelRatio;
offscreenCanvas.height = canvas.clientHeight * devicePixelRatio;

// 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]);
} catch (err) {
// TODO: This catch is added here because React will call init twice with the same canvas, and
// the second time will fail the transferControlToOffscreen() because it's already been
// transferred. I'd love to know how to get around that.
console.warn(err.message);
worker.terminate();
}
};

const WebGPUWorker: () => JSX.Element = () =>
makeSample({
name: 'WebGPU in a Worker',
description: `This example shows one method of using WebGPU in a web worker and presenting to
the main thread. It uses canvas.transferControlToOffscreen() to produce an offscreen canvas
which is then transferred to the worker where all the WebGPU calls are made.`,
init,
sources: [
{
name: __filename.substring(__dirname.length + 1),
contents: __SOURCE__,
},
{
name: './worker.ts',
// eslint-disable-next-line @typescript-eslint/no-var-requires
contents: require('!!raw-loader!./worker.ts').default,
},
{
name: '../../shaders/basic.vert.wgsl',
// eslint-disable-next-line @typescript-eslint/no-var-requires
contents: require('!!raw-loader!../../shaders/basic.vert.wgsl').default,
},
{
name: '../../shaders/vertexPositionColor.frag.wgsl',
contents:
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('!!raw-loader!../../shaders/vertexPositionColor.frag.wgsl')
.default,
},
{
name: '../../meshes/cube.ts',
// eslint-disable-next-line @typescript-eslint/no-var-requires
contents: require('!!raw-loader!../../meshes/cube.ts').default,
},
],
filename: __filename,
});

export default WebGPUWorker;
215 changes: 215 additions & 0 deletions src/sample/worker/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { mat4, vec3 } from 'wgpu-matrix';

import {
cubeVertexArray,
cubeVertexSize,
cubeUVOffset,
cubePositionOffset,
cubeVertexCount,
} from '../../meshes/cube';

import basicVertWGSL from '../../shaders/basic.vert.wgsl';
import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';

// 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
// main thread that will contain an OffscreenCanvas transferred from the page, and use that as the
// signal to begin WebGPU initialization.
self.addEventListener('message', (ev) => {
switch (ev.data.type) {
case 'init': {
try {
init(ev.data.offscreenCanvas);
} catch (err) {
self.postMessage({
type: 'log',
message: `Error while initializing WebGPU in worker process: ${err.message}`,
});
}
break;
}
}
});

// Once we receive the OffscreenCanvas this init() function is called, which functions similarly
// to the init() method for all the other samples. The remainder of this file is largely identical
// to the rotatingCube sample.
async function init(canvas) {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

context.configure({
device,
format: presentationFormat,
alphaMode: 'premultiplied',
});

// Create a vertex buffer from the cube data.
const verticesBuffer = device.createBuffer({
size: cubeVertexArray.byteLength,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
verticesBuffer.unmap();

const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: basicVertWGSL,
}),
entryPoint: 'main',
buffers: [
{
arrayStride: cubeVertexSize,
attributes: [
{
// position
shaderLocation: 0,
offset: cubePositionOffset,
format: 'float32x4',
},
{
// uv
shaderLocation: 1,
offset: cubeUVOffset,
format: 'float32x2',
},
],
},
],
},
fragment: {
module: device.createShaderModule({
code: vertexPositionColorWGSL,
}),
entryPoint: 'main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',

// Backface culling since the cube is solid piece of geometry.
// Faces pointing away from the camera will be occluded by faces
// pointing toward the camera.
cullMode: 'back',
},

// Enable depth testing so that the fragment closest to the camera
// is rendered in front.
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});

const depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});

const uniformBufferSize = 4 * 16; // 4x4 matrix
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

const uniformBindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
},
},
],
});

const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: undefined, // Assigned later

clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthTexture.createView(),

depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
};

const aspect = canvas.width / canvas.height;
const projectionMatrix = mat4.perspective(
(2 * Math.PI) / 5,
aspect,
1,
100.0
);
const modelViewProjectionMatrix = mat4.create();

function getTransformationMatrix() {
const viewMatrix = mat4.identity();
mat4.translate(viewMatrix, vec3.fromValues(0, 0, -4), viewMatrix);
const now = Date.now() / 1000;
mat4.rotate(
viewMatrix,
vec3.fromValues(Math.sin(now), Math.cos(now), 0),
1,
viewMatrix
);

mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);

return modelViewProjectionMatrix as Float32Array;
}

function frame() {
const transformationMatrix = getTransformationMatrix();
device.queue.writeBuffer(
uniformBuffer,
0,
transformationMatrix.buffer,
transformationMatrix.byteOffset,
transformationMatrix.byteLength
);
renderPassDescriptor.colorAttachments[0].view = context
.getCurrentTexture()
.createView();

const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, uniformBindGroup);
passEncoder.setVertexBuffer(0, verticesBuffer);
passEncoder.draw(cubeVertexCount, 1, 0, 0);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);

requestAnimationFrame(frame);
}

// Note: It is important to return control to the browser regularly in order for the worker to
// process events. You shouldn't simply loop infinitely with while(true) or similar! Using a
// traditional requestAnimationFrame() loop in the worker is one way to ensure that events are
// handled correctly by the worker.
requestAnimationFrame(frame);
}

export {};

0 comments on commit 8c86ccf

Please sign in to comment.