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