b09c3a4b351bdabb885321f93fc21aeee3870816
[WebKit-https.git] / Websites / webkit.org / demos / webgpu / hello-cube.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta name="viewport" content="width=1000">
5 <title>WebGPU Cube demo</title>
6 <script src="scripts/gl-matrix-min.js"></script>
7 <link rel="stylesheet" href="css/style.css"/>
8 </head>
9 <body>
10 <canvas></canvas>
11 <script>
12 const positionAttributeNum  = 0;
13 const texCoordsAttributeNum = 1;
14
15 const transformBindingNum   = 0;
16 const textureBindingNum     = 1;
17 const samplerBindingNum     = 2;
18
19 const vertexBufferIndex     = 0;
20 const bindGroupIndex        = 0;
21
22 const shader = `
23 #include <metal_stdlib>
24
25 using namespace metal;
26
27 struct Vertex
28 {
29     float4 position [[attribute(${positionAttributeNum})]];
30     float2 texCoords [[attribute(${texCoordsAttributeNum})]];
31 };
32
33 struct FragmentData
34 {
35     float4 position [[position]];
36     float2 texCoords;
37 };
38
39 struct Uniform
40 {
41     device float4x4* modelViewProjectionMatrix [[id(${transformBindingNum})]];
42 };
43
44 struct SampledTexture
45 {
46     texture2d<float> faceTexture [[id(${textureBindingNum})]];
47     sampler faceSampler [[id(${samplerBindingNum})]];
48 };
49
50 vertex FragmentData vertex_main(Vertex vertexIn [[stage_in]],
51                             const device Uniform& uniforms [[buffer(${bindGroupIndex})]])
52 {
53     FragmentData output;
54     output.position = uniforms.modelViewProjectionMatrix[0] * vertexIn.position;
55     output.texCoords = vertexIn.texCoords;
56
57     return output;
58 }
59
60 fragment float4 fragment_main(FragmentData data [[stage_in]],
61                             const device SampledTexture& args [[buffer(${bindGroupIndex})]])
62 {
63     float4 color = args.faceTexture.sample(args.faceSampler, data.texCoords);
64     if (color.a < 1)
65         discard_fragment();
66
67     return color;
68 }
69 `
70
71 let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor, queue, textureViewBinding, samplerBinding;
72 let projectionMatrix = mat4.create();
73
74 const texCoordsOffset = 4 * 4;
75 const vertexSize = 4 * 6;
76 function createVerticesArray() {
77     return new Float32Array([
78         // float4 position, float2 texCoords
79         1, -1, 1, 1, 1, 0, 
80         -1, -1, 1, 1, 0, 0, 
81         -1, -1, -1, 1, 0, 1, 
82         1, -1, -1, 1, 1, 1,
83         1, -1, 1, 1, 1, 0,
84         -1, -1, -1, 1, 0, 1,
85
86         1, 1, 1, 1, 0, 0,
87         1, -1, 1, 1, 0, 1,
88         1, -1, -1, 1, 1, 1,
89         1, 1, -1, 1, 1, 0,
90         1, 1, 1, 1, 0, 0,
91         1, -1, -1, 1, 1, 1,
92
93         -1, 1, 1, 1, 0, 1,
94         1, 1, 1, 1, 1, 1,
95         1, 1, -1, 1, 1, 0,
96         -1, 1, -1, 1, 0, 0,
97         -1, 1, 1, 1, 0, 1,
98         1, 1, -1, 1, 1, 0,
99
100         -1, -1, 1, 1, 1, 1,
101         -1, 1, 1, 1, 1, 0,
102         -1, 1, -1, 1, 0, 0,
103         -1, -1, -1, 1, 0, 1,
104         -1, -1, 1, 1, 1, 1,
105         -1, 1, -1, 1, 0, 0,
106
107         1, 1, 1, 1, 1, 0,
108         -1, 1, 1, 1, 0, 0,
109         -1, -1, 1, 1, 0, 1,
110         -1, -1, 1, 1, 0, 1,
111         1, -1, 1, 1, 1, 1,
112         1, 1, 1, 1, 1, 0,
113
114         1, -1, -1, 1, 0, 1,
115         -1, -1, -1, 1, 1, 1,
116         -1, 1, -1, 1, 1, 0,
117         1, 1, -1, 1, 0, 0,
118         1, -1, -1, 1, 0, 1,
119         -1, 1, -1, 1, 1, 0,
120     ]);
121 }
122
123 async function init() {
124     const adapter = await navigator.gpu.requestAdapter();
125     device = await adapter.requestDevice();
126
127     const canvas = document.querySelector('canvas');
128     let canvasSize = canvas.getBoundingClientRect();
129     canvas.width = canvasSize.width;
130     canvas.height = canvasSize.height;
131
132     const aspect = Math.abs(canvas.width / canvas.height);
133     mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0);
134
135     const context = canvas.getContext('gpu');
136
137     const swapChainDescriptor = { 
138         device: device,
139         format: "bgra8unorm"
140     };
141     swapChain = context.configureSwapChain(swapChainDescriptor);
142
143     // WebKit WebGPU accepts only MSL for now.
144     const shaderModuleDescriptor = { code: shader };
145     const shaderModule = device.createShaderModule(shaderModuleDescriptor);
146
147     const verticesArray = createVerticesArray();
148     const verticesBufferDescriptor = { 
149         size: verticesArray.byteLength, 
150         usage: GPUBufferUsage.VERTEX | GPUBufferUsage.TRANSFER_DST
151     };
152     verticesBuffer = device.createBuffer(verticesBufferDescriptor);
153     verticesBuffer.setSubData(0, verticesArray.buffer);
154
155     // Input state. Model will change soon to adopt one of https://github.com/kainino0x/gpuweb/pull/2/'s ideas.
156     const positionAttributeDescriptor = {
157         shaderLocation: positionAttributeNum,  // [[attribute(0)]].
158         inputSlot: vertexBufferIndex,       // Used as vertex buffer index in Metal.
159         offset: 0,
160         format: "float4"
161     };
162     const texCoordsAttributeDescriptor = {
163         shaderLocation: texCoordsAttributeNum,
164         inputSlot: vertexBufferIndex,
165         offset: texCoordsOffset,
166         format: "float2"
167     }
168     const vertexBufferDescriptor = {
169         inputSlot: vertexBufferIndex,
170         stride: vertexSize,
171         stepMode: "vertex"
172     };
173     const inputStateDescriptor = {
174         indexFormat: "uint32",
175         attributes: [positionAttributeDescriptor, texCoordsAttributeDescriptor], 
176         inputs: [vertexBufferDescriptor]
177     };
178
179     // Texture
180
181     // Load texture image
182     const image = new Image();
183     const imageLoadPromise = new Promise(resolve => { 
184         image.onload = () => resolve(); 
185         image.src = "resources/safari-alpha.png"
186     });
187     await Promise.resolve(imageLoadPromise);
188
189     const textureSize = {
190         width: image.width,
191         height: image.height,
192         depth: 1
193     };
194
195     const textureDescriptor = {
196         size: textureSize,
197         arrayLayerCount: 1,
198         mipLevelCount: 1,
199         sampleCount: 1,
200         dimension: "2d",
201         format: "rgba8unorm",
202         usage: GPUTextureUsage.TRANSFER_DST | GPUTextureUsage.SAMPLED
203     };
204     const texture = device.createTexture(textureDescriptor);
205
206     // Texture data 
207     const canvas2d = document.createElement('canvas');
208     canvas2d.width = image.width;
209     canvas2d.height = image.height;
210     const context2d = canvas2d.getContext('2d');
211     context2d.drawImage(image, 0, 0);
212
213     const imageData = context2d.getImageData(0, 0, image.width, image.height);
214
215     const textureDataBufferDescriptor = {
216         size: imageData.data.length,
217         usage: GPUBufferUsage.TRANSFER_SRC | GPUBufferUsage.TRANSFER_DST
218     };
219     const textureDataBuffer = device.createBuffer(textureDataBufferDescriptor);
220     textureDataBuffer.setSubData(0, imageData.data.buffer);
221
222     const dataCopyView = {
223         buffer: textureDataBuffer,
224         offset: 0,
225         rowPitch: image.width * 4,
226         imageHeight: 0
227     };
228     const textureCopyView = {
229         texture: texture,
230         mipLevel: 0,
231         arrayLayer: 0,
232         origin: { x: 0, y: 0, z: 0 }
233     };
234
235     const blitCommandEncoder = device.createCommandEncoder();
236     blitCommandEncoder.copyBufferToTexture(dataCopyView, textureCopyView, textureSize);
237
238     queue = device.getQueue();
239
240     queue.submit([blitCommandEncoder.finish()]);
241
242     // Bind group binding layout
243     const transformBufferBindGroupLayoutBinding = {
244         binding: transformBindingNum, // id[[(0)]]
245         visibility: GPUShaderStageBit.VERTEX,
246         type: "uniform-buffer"
247     };
248
249     const textureBindGroupLayoutBinding = {
250         binding: textureBindingNum,
251         visibility: GPUShaderStageBit.FRAGMENT,
252         type: "sampled-texture"
253     };
254     textureViewBinding = {
255         binding: textureBindingNum,
256         resource: texture.createDefaultView()
257     };
258
259     const samplerBindGroupLayoutBinding = {
260         binding: samplerBindingNum,
261         visibility: GPUShaderStageBit.FRAGMENT,
262         type: "sampler"
263     };
264     samplerBinding = {
265         binding: samplerBindingNum,
266         resource: device.createSampler({})
267     };
268
269     const bindGroupLayoutDescriptor = { 
270         bindings: [transformBufferBindGroupLayoutBinding, textureBindGroupLayoutBinding, samplerBindGroupLayoutBinding] 
271     };
272     bindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDescriptor);
273
274     // Pipeline
275     const depthStateDescriptor = {
276         depthWriteEnabled: true,
277         depthCompare: "less"
278     };
279
280     const pipelineLayoutDescriptor = { bindGroupLayouts: [bindGroupLayout] };
281     const pipelineLayout = device.createPipelineLayout(pipelineLayoutDescriptor);
282     const vertexStageDescriptor = {
283         module: shaderModule,
284         entryPoint: "vertex_main"
285     };
286     const fragmentStageDescriptor = {
287         module: shaderModule,
288         entryPoint: "fragment_main"
289     };
290     const colorState = {
291         format: "bgra8unorm",
292         alphaBlend: {
293             srcFactor: "src-alpha",
294             dstFactor: "one-minus-src-alpha",
295             operation: "add"
296         },
297         colorBlend: {
298             srcFactor: "src-alpha",
299             dstFactor: "one-minus-src-alpha",
300             operation: "add"
301         },
302         writeMask: GPUColorWriteBits.ALL
303     };
304     const pipelineDescriptor = {
305         layout: pipelineLayout,
306
307         vertexStage: vertexStageDescriptor,
308         fragmentStage: fragmentStageDescriptor,
309
310         primitiveTopology: "triangle-list",
311         colorStates: [colorState],
312         depthStencilState: depthStateDescriptor,
313         inputState: inputStateDescriptor
314     };
315     pipeline = device.createRenderPipeline(pipelineDescriptor);
316
317     let colorAttachment = {
318         // attachment is acquired in render loop.
319         loadOp: "clear",
320         storeOp: "store",
321         clearColor: { r: 0.5, g: 1.0, b: 1.0, a: 1.0 } // GPUColor
322     };
323
324     // Depth stencil texture
325
326     // GPUExtent3D
327     const depthSize = {
328         width: canvas.width,
329         height: canvas.height,
330         depth: 1
331     };
332
333     const depthTextureDescriptor = {
334         size: depthSize,
335         arrayLayerCount: 1,
336         mipLevelCount: 1,
337         sampleCount: 1,
338         dimension: "2d",
339         format: "depth32float-stencil8",
340         usage: GPUTextureUsage.OUTPUT_ATTACHMENT
341     };
342
343     const depthTexture = device.createTexture(depthTextureDescriptor);
344
345     // GPURenderPassDepthStencilAttachmentDescriptor
346     const depthAttachment = {
347         attachment: depthTexture.createDefaultView(),
348         depthLoadOp: "clear",
349         depthStoreOp: "store",
350         clearDepth: 1.0
351     };
352
353     renderPassDescriptor = { 
354         colorAttachments: [colorAttachment],
355         depthStencilAttachment: depthAttachment
356     };
357
358     render();
359 }
360
361 /* Transform Buffers and Bindings */
362 const transformSize = 4 * 16;
363 function updateTransformArray(array) {
364     let viewMatrix = mat4.create();
365     mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -5));
366     let now = Date.now() / 1000;
367     mat4.rotate(viewMatrix, viewMatrix, 1, vec3.fromValues(Math.sin(now), Math.cos(now), 0));
368     let modelViewProjectionMatrix = mat4.create();
369     mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix);
370     for (let i = 0; i < 16; i++) {
371         array[i] = modelViewProjectionMatrix[i];
372     }
373 }
374
375 const transformBufferDescriptor = {
376     size: transformSize,
377     usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.MAP_WRITE
378 };
379
380 function createBindGroupDescriptor(transformBuffer, textureViewBinding, samplerBinding) {
381     const transformBufferBinding = {
382         buffer: transformBuffer,
383         offset: 0,
384         size: transformSize
385     };
386     const transformBufferBindGroupBinding = {
387         binding: transformBindingNum,
388         resource: transformBufferBinding
389     };
390     return {
391         layout: bindGroupLayout,
392         bindings: [transformBufferBindGroupBinding, textureViewBinding, samplerBinding]
393     };
394 }
395
396 let mappedGroups = [];
397
398 function render() {
399     if (mappedGroups.length == 0) {
400         const buffer = device.createBuffer(transformBufferDescriptor);
401         buffer.mapWriteAsync().then(arrayBuffer => {
402             const group = device.createBindGroup(createBindGroupDescriptor(buffer, textureViewBinding, samplerBinding));
403
404             let mappedGroup = { buffer: buffer, arrayBuffer: arrayBuffer, bindGroup: group };
405             drawCommands(mappedGroup);
406         });
407     } else
408         drawCommands(mappedGroups.shift());
409 }
410
411 function drawCommands(mappedGroup) {
412     updateTransformArray(new Float32Array(mappedGroup.arrayBuffer));
413     mappedGroup.buffer.unmap();
414
415     const commandEncoder = device.createCommandEncoder();
416     renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView();
417     const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
418     // Encode drawing commands.
419     passEncoder.setPipeline(pipeline);
420     // Vertex attributes
421     passEncoder.setVertexBuffers(vertexBufferIndex, [verticesBuffer], [0]);
422     // Bind groups
423     passEncoder.setBindGroup(bindGroupIndex, mappedGroup.bindGroup);
424     passEncoder.draw(36, 1, 0, 0);
425     passEncoder.endPass();
426
427     queue.submit([commandEncoder.finish()]);
428
429     // Ready the current buffer for update after GPU is done with it.
430     mappedGroup.buffer.mapWriteAsync().then((arrayBuffer) => {
431         mappedGroup.arrayBuffer = arrayBuffer;
432         mappedGroups.push(mappedGroup);
433     });
434
435     requestAnimationFrame(render);
436 }
437
438 init();
439 </script>
440 </body>
441 </html>