-
Notifications
You must be signed in to change notification settings - Fork 302
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #289 from webgpu/worker
Added a sample that shows WebGPU running from a web worker
- Loading branch information
Showing
5 changed files
with
316 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; |