-
Notifications
You must be signed in to change notification settings - Fork 345
Add reference Web GPU Spinning Cube Demo and glMatrix #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9f828d6
5bc139f
6501f8d
b5238a0
949df91
de6bbf4
0e41a76
461166c
6c7aee2
fdb5ebd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,318 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <meta name="viewport" content="width=1000"> | ||
| <title>Web GPU Cube demo</title> | ||
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script> | ||
| </head> | ||
| <body> | ||
| <canvas height=1000 width=1000></canvas> | ||
| <script> | ||
| /******* | ||
| * This preview of Web GPU uses WHLSL as its shading language. | ||
| * The choice of shading language(s) that will be ingested by the API is still under deliberation. | ||
JusSn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| *******/ | ||
| const positionAttributeNum = 0; | ||
| const colorAttributeNum = 1; | ||
| const transformBindingNum = 0; | ||
| const bindGroupIndex = 0; | ||
|
|
||
| const shader = ` | ||
kainino0x marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| struct FragmentData { | ||
| float4 position : SV_Position; | ||
| float4 color : attribute(${colorAttributeNum}); | ||
| } | ||
|
|
||
| vertex FragmentData vertex_main( | ||
| float4 position : attribute(${positionAttributeNum}), | ||
| float4 color : attribute(${colorAttributeNum}), | ||
| constant float4x4[] modelViewProjectionMatrix : register(b${transformBindingNum})) | ||
| { | ||
| FragmentData out; | ||
| out.position = mul(modelViewProjectionMatrix[0], position); | ||
| out.color = color; | ||
|
|
||
| return out; | ||
| } | ||
|
|
||
| fragment float4 fragment_main(float4 color : attribute(${colorAttributeNum})) : SV_Target 0 | ||
| { | ||
| return color; | ||
| } | ||
| `; | ||
|
|
||
| let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor; | ||
| let projectionMatrix = mat4.create(); | ||
| let mappedGroups = []; | ||
|
|
||
| const colorOffset = 4 * 4; | ||
| const vertexSize = 4 * 8; | ||
| const verticesArray = new Float32Array([ | ||
| // float4 position, float4 color | ||
| 1, -1, 1, 1, 1, 0, 1, 1, | ||
| -1, -1, 1, 1, 0, 0, 1, 1, | ||
| -1, -1, -1, 1, 0, 0, 0, 1, | ||
| 1, -1, -1, 1, 1, 0, 0, 1, | ||
| 1, -1, 1, 1, 1, 0, 1, 1, | ||
| -1, -1, -1, 1, 0, 0, 0, 1, | ||
|
|
||
| 1, 1, 1, 1, 1, 1, 1, 1, | ||
| 1, -1, 1, 1, 1, 0, 1, 1, | ||
| 1, -1, -1, 1, 1, 0, 0, 1, | ||
| 1, 1, -1, 1, 1, 1, 0, 1, | ||
| 1, 1, 1, 1, 1, 1, 1, 1, | ||
| 1, -1, -1, 1, 1, 0, 0, 1, | ||
|
|
||
| -1, 1, 1, 1, 0, 1, 1, 1, | ||
| 1, 1, 1, 1, 1, 1, 1, 1, | ||
| 1, 1, -1, 1, 1, 1, 0, 1, | ||
| -1, 1, -1, 1, 0, 1, 0, 1, | ||
| -1, 1, 1, 1, 0, 1, 1, 1, | ||
| 1, 1, -1, 1, 1, 1, 0, 1, | ||
|
|
||
| -1, -1, 1, 1, 0, 0, 1, 1, | ||
| -1, 1, 1, 1, 0, 1, 1, 1, | ||
| -1, 1, -1, 1, 0, 1, 0, 1, | ||
| -1, -1, -1, 1, 0, 0, 0, 1, | ||
| -1, -1, 1, 1, 0, 0, 1, 1, | ||
| -1, 1, -1, 1, 0, 1, 0, 1, | ||
|
|
||
| 1, 1, 1, 1, 1, 1, 1, 1, | ||
| -1, 1, 1, 1, 0, 1, 1, 1, | ||
| -1, -1, 1, 1, 0, 0, 1, 1, | ||
| -1, -1, 1, 1, 0, 0, 1, 1, | ||
| 1, -1, 1, 1, 1, 0, 1, 1, | ||
| 1, 1, 1, 1, 1, 1, 1, 1, | ||
|
|
||
| 1, -1, -1, 1, 1, 0, 0, 1, | ||
| -1, -1, -1, 1, 0, 0, 0, 1, | ||
| -1, 1, -1, 1, 0, 1, 0, 1, | ||
| 1, 1, -1, 1, 1, 1, 0, 1, | ||
| 1, -1, -1, 1, 1, 0, 0, 1, | ||
| -1, 1, -1, 1, 0, 1, 0, 1, | ||
| ]); | ||
|
|
||
| async function init() { | ||
| const adapter = await navigator.gpu.requestAdapter(); | ||
| device = await adapter.requestDevice(); | ||
|
|
||
| const canvas = document.querySelector('canvas'); | ||
| let canvasSize = canvas.getBoundingClientRect(); | ||
| canvas.width = canvasSize.width; | ||
| canvas.height = canvasSize.height; | ||
|
|
||
| const aspect = Math.abs(canvas.width / canvas.height); | ||
| mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0); | ||
|
|
||
| const context = canvas.getContext('gpu'); | ||
|
|
||
| const swapChainDescriptor = { | ||
| device: device, | ||
| format: "bgra8unorm" | ||
| }; | ||
| swapChain = context.configureSwapChain(swapChainDescriptor); | ||
|
|
||
| const shaderModuleDescriptor = { code: shader }; | ||
| const shaderModule = device.createShaderModule(shaderModuleDescriptor); | ||
|
|
||
| const verticesBufferDescriptor = { size: verticesArray.byteLength, usage: GPUBufferUsage.VERTEX }; | ||
| let verticesArrayBuffer; | ||
| [verticesBuffer, verticesArrayBuffer] = device.createBufferMapped(verticesBufferDescriptor); | ||
|
|
||
| const verticesWriteArray = new Float32Array(verticesArrayBuffer); | ||
| verticesWriteArray.set(verticesArray); | ||
| verticesBuffer.unmap(); | ||
|
|
||
| // Vertex Input | ||
| const positionAttributeDescriptor = { | ||
| shaderLocation: positionAttributeNum, // [[attribute(0)]] | ||
| offset: 0, | ||
| format: "float4" | ||
| }; | ||
| const colorAttributeDescriptor = { | ||
| shaderLocation: colorAttributeNum, | ||
| offset: colorOffset, | ||
| format: "float4" | ||
| } | ||
| const vertexBufferDescriptor = { | ||
| attributeSet: [positionAttributeDescriptor, colorAttributeDescriptor], | ||
| stride: vertexSize, | ||
| stepMode: "vertex" | ||
| }; | ||
| const vertexInputDescriptor = { vertexBuffers: [vertexBufferDescriptor] }; | ||
|
|
||
| // Bind group binding layout | ||
| const transformBufferBindGroupLayoutBinding = { | ||
| binding: transformBindingNum, // id[[(0)]] | ||
| visibility: GPUShaderStageBit.VERTEX, | ||
| type: "uniform-buffer" | ||
| }; | ||
|
|
||
| const bindGroupLayoutDescriptor = { bindings: [transformBufferBindGroupLayoutBinding] }; | ||
| bindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDescriptor); | ||
|
|
||
| // Pipeline | ||
| const depthStateDescriptor = { | ||
| depthWriteEnabled: true, | ||
| depthCompare: "less" | ||
| }; | ||
|
|
||
| const pipelineLayoutDescriptor = { bindGroupLayouts: [bindGroupLayout] }; | ||
| const pipelineLayout = device.createPipelineLayout(pipelineLayoutDescriptor); | ||
| const vertexStageDescriptor = { | ||
| module: shaderModule, | ||
| entryPoint: "vertex_main" | ||
| }; | ||
| const fragmentStageDescriptor = { | ||
| module: shaderModule, | ||
| entryPoint: "fragment_main" | ||
| }; | ||
| const colorState = { | ||
| format: "bgra8unorm", | ||
| alphaBlend: { | ||
| srcFactor: "src-alpha", | ||
| dstFactor: "one-minus-src-alpha", | ||
| operation: "add" | ||
| }, | ||
| colorBlend: { | ||
| srcFactor: "src-alpha", | ||
| dstFactor: "one-minus-src-alpha", | ||
| operation: "add" | ||
| }, | ||
| writeMask: GPUColorWriteBits.ALL | ||
| }; | ||
| const pipelineDescriptor = { | ||
| layout: pipelineLayout, | ||
|
|
||
| vertexStage: vertexStageDescriptor, | ||
| fragmentStage: fragmentStageDescriptor, | ||
|
|
||
| primitiveTopology: "triangle-list", | ||
| colorStates: [colorState], | ||
| depthStencilState: depthStateDescriptor, | ||
| vertexInput: vertexInputDescriptor | ||
| }; | ||
| pipeline = device.createRenderPipeline(pipelineDescriptor); | ||
|
|
||
| let colorAttachment = { | ||
| // attachment is acquired in render loop. | ||
| loadOp: "clear", | ||
| storeOp: "store", | ||
| clearColor: { r: 0.5, g: 1.0, b: 1.0, a: 1.0 } // GPUColor | ||
| }; | ||
|
|
||
| // Depth stencil texture | ||
|
|
||
| // GPUExtent3D | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wish we could do something more type-safe here. It seems silly to attach a 3D size to a 2D texture.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, maybe we need GPUSize2D and GPUSize3D (I prefer both of them over "Extent") |
||
| const depthSize = { | ||
| width: canvas.width, | ||
| height: canvas.height, | ||
| depth: 1 | ||
| }; | ||
|
|
||
| const depthTextureDescriptor = { | ||
| size: depthSize, | ||
| arrayLayerCount: 1, | ||
| mipLevelCount: 1, | ||
| sampleCount: 1, | ||
| dimension: "2d", | ||
| format: "depth32float-stencil8", | ||
| usage: GPUTextureUsage.OUTPUT_ATTACHMENT | ||
| }; | ||
|
|
||
| const depthTexture = device.createTexture(depthTextureDescriptor); | ||
|
|
||
| // GPURenderPassDepthStencilAttachmentDescriptor | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this comment. |
||
| const depthAttachment = { | ||
| attachment: depthTexture.createDefaultView(), | ||
| depthLoadOp: "clear", | ||
| depthStoreOp: "store", | ||
| clearDepth: 1.0 | ||
| }; | ||
|
|
||
| renderPassDescriptor = { | ||
| colorAttachments: [colorAttachment], | ||
| depthStencilAttachment: depthAttachment | ||
| }; | ||
|
|
||
| render(); | ||
| } | ||
|
|
||
| // Transform Buffers and Bindings | ||
|
|
||
| const transformSize = 4 * 16; | ||
| const transformBufferDescriptor = { | ||
| size: transformSize, | ||
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.MAP_WRITE | ||
| }; | ||
|
|
||
| function render() { | ||
| if (mappedGroups.length === 0) { | ||
| const [buffer, arrayBuffer] = device.createBufferMapped(transformBufferDescriptor); | ||
| const group = device.createBindGroup(createBindGroupDescriptor(buffer)); | ||
| let mappedGroup = { buffer: buffer, arrayBuffer: arrayBuffer, bindGroup: group }; | ||
| drawCommands(mappedGroup); | ||
| } else | ||
| drawCommands(mappedGroups.shift()); | ||
| } | ||
|
|
||
| function createBindGroupDescriptor(transformBuffer) { | ||
| const transformBufferBinding = { | ||
| buffer: transformBuffer, | ||
| offset: 0, | ||
| size: transformSize | ||
| }; | ||
| const transformBufferBindGroupBinding = { | ||
| binding: transformBindingNum, | ||
| resource: transformBufferBinding | ||
| }; | ||
| return { | ||
| layout: bindGroupLayout, | ||
| bindings: [transformBufferBindGroupBinding] | ||
| }; | ||
| } | ||
|
|
||
| function drawCommands(mappedGroup) { | ||
| updateTransformArray(new Float32Array(mappedGroup.arrayBuffer)); | ||
| mappedGroup.buffer.unmap(); | ||
|
|
||
| const commandEncoder = device.createCommandEncoder(); | ||
| renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView(); | ||
| const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); | ||
|
|
||
| // Encode drawing commands | ||
|
|
||
| passEncoder.setPipeline(pipeline); | ||
| // Vertex attributes | ||
| passEncoder.setVertexBuffers(0, [verticesBuffer], [0]); | ||
| // Bind groups | ||
| passEncoder.setBindGroup(bindGroupIndex, mappedGroup.bindGroup); | ||
| // 36 vertices, 1 instance, 0th vertex, 0th instance. | ||
| passEncoder.draw(36, 1, 0, 0); | ||
JusSn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| passEncoder.endPass(); | ||
|
|
||
| device.getQueue().submit([commandEncoder.finish()]); | ||
|
|
||
| // Ready the current buffer for update after GPU is done with it. | ||
| mappedGroup.buffer.mapWriteAsync().then((arrayBuffer) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The right thing will happen here, but we might not wants apps to rely on the specific timeline that the mapWriteAsync promise resolves. It may be better to explicitly handle the dependency of "the mapping should happen after the command buffer finishes" by using fences. I'm not sure which behavior we want to promote.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this does the right thing, it's probably more readable than barriers. The thing that I think is a bit weird is that mappedGroups could grow infinitely (if some other part of the code appends to it outside this loop). It's a shame we can't attach the group to the queue or something.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have this example working with my mapWriteAsync() prototype. I could use a class to control access to the mappedGroups queue, if that's what you mean?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I understand what you're saying now. Shouldn't mapWriteAsync only resolve after the GPU is done with the buffer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but it means that we don't have any pipelining here based on how I understand it. We cannot upload the data for the second frame until the GPU is done with the data for the first. Our example should support double-buffering. |
||
| mappedGroup.arrayBuffer = arrayBuffer; | ||
| mappedGroups.push(mappedGroup); | ||
| }); | ||
|
|
||
| requestAnimationFrame(render); | ||
| } | ||
|
|
||
| function updateTransformArray(array) { | ||
| let viewMatrix = mat4.create(); | ||
| mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -5)); | ||
| let now = Date.now() / 1000; | ||
| mat4.rotate(viewMatrix, viewMatrix, 1, vec3.fromValues(Math.sin(now), Math.cos(now), 0)); | ||
| let modelViewProjectionMatrix = mat4.create(); | ||
| mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix); | ||
| mat4.copy(array, modelViewProjectionMatrix); | ||
| } | ||
|
|
||
| window.addEventListener("load", init); | ||
| </script> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You set a 600px viewport above, so 1000x1000 would be mostly offscreen.