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