[WebGPU] Fix up demos on and add compute demo to webkit.org/demos
authorjustin_fan@apple.com <justin_fan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 6 Aug 2019 20:18:43 +0000 (20:18 +0000)
committerjustin_fan@apple.com <justin_fan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 6 Aug 2019 20:18:43 +0000 (20:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=200454

Reviewed by Jon Lee.

Add the compute-blur demo.
Ensure that WebGPU demos will work on upcoming STP release.

* demos/webgpu/compute-blur.html: Added.
* demos/webgpu/css/style.css: Sync with internal demo repository stylesheet.
(body):
(canvas):
(body.error img):
(body.error input):
(#error p):
* demos/webgpu/hello-cube.html:
* demos/webgpu/hello-triangle.html:
* demos/webgpu/index.html:
* demos/webgpu/resources/compute-blur.png: Added.
* demos/webgpu/resources/hello-cube.png:
* demos/webgpu/resources/textured-cube.png: Added.
* demos/webgpu/scripts/compute-blur.js: Added.
(async.init):
(async.loadImage):
(setUpCompute):
(async.computeBlur):
(async.setUniforms):
(createShaderCode):
* demos/webgpu/scripts/hello-triangle.js:
(async.helloTriangle):
* demos/webgpu/textured-cube.html: Renmaed from Websites/webkit.org/demos/webgpu/hello-cube.html.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@248309 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Websites/webkit.org/ChangeLog
Websites/webkit.org/demos/webgpu/compute-blur.html [new file with mode: 0644]
Websites/webkit.org/demos/webgpu/css/style.css
Websites/webkit.org/demos/webgpu/hello-cube.html
Websites/webkit.org/demos/webgpu/hello-triangle.html
Websites/webkit.org/demos/webgpu/index.html
Websites/webkit.org/demos/webgpu/resources/compute-blur.png [new file with mode: 0644]
Websites/webkit.org/demos/webgpu/resources/hello-cube.png
Websites/webkit.org/demos/webgpu/resources/textured-cube.png [new file with mode: 0644]
Websites/webkit.org/demos/webgpu/scripts/compute-blur.js [new file with mode: 0644]
Websites/webkit.org/demos/webgpu/scripts/hello-triangle.js
Websites/webkit.org/demos/webgpu/textured-cube.html [new file with mode: 0644]

index 7bc2f70..29555e3 100644 (file)
@@ -1,3 +1,37 @@
+2019-08-06  Justin Fan  <justin_fan@apple.com>
+
+        [WebGPU] Fix up demos on and add compute demo to webkit.org/demos
+        https://bugs.webkit.org/show_bug.cgi?id=200454
+
+        Reviewed by Jon Lee.
+
+        Add the compute-blur demo.
+        Ensure that WebGPU demos will work on upcoming STP release. 
+
+        * demos/webgpu/compute-blur.html: Added.
+        * demos/webgpu/css/style.css: Sync with internal demo repository stylesheet.
+        (body):
+        (canvas):
+        (body.error img):
+        (body.error input):
+        (#error p):
+        * demos/webgpu/hello-cube.html:
+        * demos/webgpu/hello-triangle.html:
+        * demos/webgpu/index.html:
+        * demos/webgpu/resources/compute-blur.png: Added.
+        * demos/webgpu/resources/hello-cube.png:
+        * demos/webgpu/resources/textured-cube.png: Added.
+        * demos/webgpu/scripts/compute-blur.js: Added.
+        (async.init):
+        (async.loadImage):
+        (setUpCompute):
+        (async.computeBlur):
+        (async.setUniforms):
+        (createShaderCode):
+        * demos/webgpu/scripts/hello-triangle.js:
+        (async.helloTriangle):
+        * demos/webgpu/textured-cube.html: Renmaed from Websites/webkit.org/demos/webgpu/hello-cube.html.
+
 2019-07-03  Jon Davis  <jond@apple.com>
 
         Added a domain check for validation URLs in Apple Pay demo.
diff --git a/Websites/webkit.org/demos/webgpu/compute-blur.html b/Websites/webkit.org/demos/webgpu/compute-blur.html
new file mode 100644 (file)
index 0000000..dd7ace9
--- /dev/null
@@ -0,0 +1,86 @@
+<!-- This demo loosely based off of https://github.com/d3dcoder/d3d12book/tree/master/Chapter%2013%20The%20Compute%20Shader/Blur -->
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="viewport" content="width=600">
+<meta http-equiv="Content-type" content="text/html; charset=utf-8">
+<title>WebGPU Compute</title>
+<link rel="stylesheet" href="css/style.css">
+<style>
+body {
+    font-family: system-ui;
+    color: #f7f7ff;
+    background-color: rgb(13, 77, 153);
+    text-align: center;
+}
+canvas {
+    margin: 0 auto;
+}
+p {
+    margin: 0 auto 1em;
+    width: 600px;
+    position: relative;
+}
+
+.slider {
+    -webkit-appearance: none;
+    width: 50%;
+    height: 10px;
+    border-radius: 5px;
+    background: #d3d3d3;
+    outline: none;
+    opacity: 0.7;
+    -webkit-transition: .2s;
+    transition: opacity .2s;
+}
+
+.slider:hover {
+    opacity: 1;
+}
+
+.slider::-webkit-slider-thumb {
+    -webkit-appearance: none;
+    appearance: none;
+    width: 25px;
+    height: 25px;
+    border-radius: 50%;
+    background: rgb(66, 200, 255);
+    cursor: pointer;
+}
+
+.slider::-moz-range-thumb {
+    width: 25px;
+    height: 25px;
+    border-radius: 50%;
+    background: rgb(66, 200, 255);
+    cursor: pointer;
+}
+</style>
+</head>
+<body>
+<div id="contents">
+    <h1>Compute Demo</h1>
+    <p>
+        This demo uses the WebGPU compute pipeline to perform a Gaussian blur of the source image data. Use the slider to set the blur radius.
+    </p>
+    <p>
+        <div class="slidecontainer">
+            Blur radius: 
+            <input type="range" min="0" max="32" value="0" class="slider" id="radiusSlider">
+        </div>
+    </p>
+    <div id="demo">
+        <canvas></canvas>
+    </div>
+</div>
+<div id="error">
+    <h2>WebGPU not available</h2>
+    <p>
+        Make sure you are on a system with WebGPU enabled. In
+        Safari, first make sure the Developer Menu is visible (Preferences →
+        Advanced), then Develop → Experimental Features → WebGPU.
+    </p>
+</div>
+<script src="scripts/compute-blur.js"></script>
+</body>
+</html>
index 1c1b16f..0cba814 100644 (file)
@@ -1,25 +1,24 @@
-/* This demo is based on webgl-compute-bitonicSort, found at https://github.com/9ballsyndrome/WebGL_Compute_shader. */
-
 body {
-    background-color: rgb(35%, 65%, 85%);
     margin: 0;
     padding: 0;
-    height: 100vh;
-    display: flex;
-    align-items: center;
-    justify-content: center;
 }
 
 canvas {
     display: block;
-    width: 100vw;
-    height: 100vh;
 }
 
 body.error canvas {
     display: none;
 }
 
+body.error img {
+    display: none;
+}
+
+body.error input {
+    display: none;
+}
+
 h1 {
     font-size: 1.5rem;
 }
@@ -74,4 +73,5 @@ body.error #error {
 
 #error p {
     font-size: 30px;
-}
\ No newline at end of file
+}
+
index b09c3a4..93ac68f 100644 (file)
 <html>
 <head>
 <meta name="viewport" content="width=1000">
-<title>WebGPU Cube demo</title>
+<title>WebGPU Cube</title>
 <script src="scripts/gl-matrix-min.js"></script>
 <link rel="stylesheet" href="css/style.css"/>
+<style>
+body {
+    font-family: system-ui;
+    color: #f7f7ff;
+    background-color: rgb(38, 38, 127);
+    text-align: center;
+}
+canvas {
+    margin: 0 auto;
+}
+</style>
 </head>
 <body>
-<canvas></canvas>
+<div id="contents">
+    <h1>Simple Cube</h1>
+    <p>
+        This demo uses a rotating set of GPUBuffers to upload rotation matrix data every frame.
+    </p>
+    <canvas width="1200" height="1200"></canvas>
+</div>
+<div id="error">
+    <h2>WebGPU not available</h2>
+    <p>
+        Make sure you are on a system with WebGPU enabled. In
+        Safari, first make sure the Developer Menu is visible (Preferences →
+        Advanced), then Develop → Experimental Features → WebGPU.
+    </p>
+</div>
 <script>
+if (!navigator.gpu)
+    document.body.className = 'error';
+
 const positionAttributeNum  = 0;
-const texCoordsAttributeNum = 1;
+const colorAttributeNum = 1;
 
 const transformBindingNum   = 0;
-const textureBindingNum     = 1;
-const samplerBindingNum     = 2;
 
-const vertexBufferIndex     = 0;
 const bindGroupIndex        = 0;
 
 const shader = `
-#include <metal_stdlib>
-
-using namespace metal;
-
-struct Vertex
-{
-    float4 position [[attribute(${positionAttributeNum})]];
-    float2 texCoords [[attribute(${texCoordsAttributeNum})]];
-};
-
-struct FragmentData
-{
-    float4 position [[position]];
-    float2 texCoords;
-};
-
-struct Uniform
-{
-    device float4x4* modelViewProjectionMatrix [[id(${transformBindingNum})]];
-};
-
-struct SampledTexture
-{
-    texture2d<float> faceTexture [[id(${textureBindingNum})]];
-    sampler faceSampler [[id(${samplerBindingNum})]];
-};
+struct FragmentData {
+    float4 position : SV_Position;
+    float4 color : attribute(${colorAttributeNum});
+}
 
-vertex FragmentData vertex_main(Vertex vertexIn [[stage_in]],
-                            const device Uniform& uniforms [[buffer(${bindGroupIndex})]])
+vertex FragmentData vertex_main(
+    float4 position : attribute(${positionAttributeNum}), 
+    float4 color : attribute(${colorAttributeNum}), 
+    constant float4x4[] modelViewProjectionMatrix : register(b${transformBindingNum}))
 {
-    FragmentData output;
-    output.position = uniforms.modelViewProjectionMatrix[0] * vertexIn.position;
-    output.texCoords = vertexIn.texCoords;
-
-    return output;
+    FragmentData out;
+    out.position = mul(modelViewProjectionMatrix[0], position);
+    out.color = color;
+    
+    return out;
 }
 
-fragment float4 fragment_main(FragmentData data [[stage_in]],
-                            const device SampledTexture& args [[buffer(${bindGroupIndex})]])
+fragment float4 fragment_main(float4 color : attribute(${colorAttributeNum})) : SV_Target 0
 {
-    float4 color = args.faceTexture.sample(args.faceSampler, data.texCoords);
-    if (color.a < 1)
-        discard_fragment();
-
     return color;
 }
-`
+`;
 
-let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor, queue, textureViewBinding, samplerBinding;
+let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor;
 let projectionMatrix = mat4.create();
 
-const texCoordsOffset = 4 * 4;
-const vertexSize = 4 * 6;
-function createVerticesArray() {
-    return new Float32Array([
-        // float4 position, float2 texCoords
-        1, -1, 1, 1, 1, 0, 
-        -1, -1, 1, 1, 0, 0, 
-        -1, -1, -1, 1, 0, 1, 
-        1, -1, -1, 1, 1, 1,
-        1, -1, 1, 1, 1, 0,
-        -1, -1, -1, 1, 0, 1,
-
-        1, 1, 1, 1, 0, 0,
-        1, -1, 1, 1, 0, 1,
-        1, -1, -1, 1, 1, 1,
-        1, 1, -1, 1, 1, 0,
-        1, 1, 1, 1, 0, 0,
-        1, -1, -1, 1, 1, 1,
-
-        -1, 1, 1, 1, 0, 1,
-        1, 1, 1, 1, 1, 1,
-        1, 1, -1, 1, 1, 0,
-        -1, 1, -1, 1, 0, 0,
-        -1, 1, 1, 1, 0, 1,
-        1, 1, -1, 1, 1, 0,
-
-        -1, -1, 1, 1, 1, 1,
-        -1, 1, 1, 1, 1, 0,
-        -1, 1, -1, 1, 0, 0,
-        -1, -1, -1, 1, 0, 1,
-        -1, -1, 1, 1, 1, 1,
-        -1, 1, -1, 1, 0, 0,
-
-        1, 1, 1, 1, 1, 0,
-        -1, 1, 1, 1, 0, 0,
-        -1, -1, 1, 1, 0, 1,
-        -1, -1, 1, 1, 0, 1,
-        1, -1, 1, 1, 1, 1,
-        1, 1, 1, 1, 1, 0,
-
-        1, -1, -1, 1, 0, 1,
-        -1, -1, -1, 1, 1, 1,
-        -1, 1, -1, 1, 1, 0,
-        1, 1, -1, 1, 0, 0,
-        1, -1, -1, 1, 0, 1,
-        -1, 1, -1, 1, 1, 0,
-    ]);
-}
+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();
@@ -135,109 +133,42 @@ async function init() {
     const context = canvas.getContext('gpu');
 
     const swapChainDescriptor = { 
-        device: device,
+        device: device, 
         format: "bgra8unorm"
     };
     swapChain = context.configureSwapChain(swapChainDescriptor);
 
-    // WebKit WebGPU accepts only MSL for now.
-    const shaderModuleDescriptor = { code: shader };
+    const shaderModuleDescriptor = { code: shader, isWHLSL: true };
     const shaderModule = device.createShaderModule(shaderModuleDescriptor);
 
-    const verticesArray = createVerticesArray();
     const verticesBufferDescriptor = { 
         size: verticesArray.byteLength, 
         usage: GPUBufferUsage.VERTEX | GPUBufferUsage.TRANSFER_DST
     };
-    verticesBuffer = device.createBuffer(verticesBufferDescriptor);
-    verticesBuffer.setSubData(0, verticesArray.buffer);
+    let verticesArrayBuffer;
+    [verticesBuffer, verticesArrayBuffer] = device.createBufferMapped(verticesBufferDescriptor);
 
-    // Input state. Model will change soon to adopt one of https://github.com/kainino0x/gpuweb/pull/2/'s ideas.
+    const verticesWriteArray = new Float32Array(verticesArrayBuffer);
+    verticesWriteArray.set(verticesArray);
+    verticesBuffer.unmap();
+
+    // Vertex Input
     const positionAttributeDescriptor = {
-        shaderLocation: positionAttributeNum,  // [[attribute(0)]].
-        inputSlot: vertexBufferIndex,       // Used as vertex buffer index in Metal.
+        shaderLocation: positionAttributeNum,  // [[attribute(0)]]
         offset: 0,
         format: "float4"
     };
-    const texCoordsAttributeDescriptor = {
-        shaderLocation: texCoordsAttributeNum,
-        inputSlot: vertexBufferIndex,
-        offset: texCoordsOffset,
-        format: "float2"
+    const colorAttributeDescriptor = {
+        shaderLocation: colorAttributeNum,
+        offset: colorOffset,
+        format: "float4"
     }
     const vertexBufferDescriptor = {
-        inputSlot: vertexBufferIndex,
+        attributeSet: [positionAttributeDescriptor, colorAttributeDescriptor],
         stride: vertexSize,
         stepMode: "vertex"
     };
-    const inputStateDescriptor = {
-        indexFormat: "uint32",
-        attributes: [positionAttributeDescriptor, texCoordsAttributeDescriptor], 
-        inputs: [vertexBufferDescriptor]
-    };
-
-    // Texture
-
-    // Load texture image
-    const image = new Image();
-    const imageLoadPromise = new Promise(resolve => { 
-        image.onload = () => resolve(); 
-        image.src = "resources/safari-alpha.png"
-    });
-    await Promise.resolve(imageLoadPromise);
-
-    const textureSize = {
-        width: image.width,
-        height: image.height,
-        depth: 1
-    };
-
-    const textureDescriptor = {
-        size: textureSize,
-        arrayLayerCount: 1,
-        mipLevelCount: 1,
-        sampleCount: 1,
-        dimension: "2d",
-        format: "rgba8unorm",
-        usage: GPUTextureUsage.TRANSFER_DST | GPUTextureUsage.SAMPLED
-    };
-    const texture = device.createTexture(textureDescriptor);
-
-    // Texture data 
-    const canvas2d = document.createElement('canvas');
-    canvas2d.width = image.width;
-    canvas2d.height = image.height;
-    const context2d = canvas2d.getContext('2d');
-    context2d.drawImage(image, 0, 0);
-
-    const imageData = context2d.getImageData(0, 0, image.width, image.height);
-
-    const textureDataBufferDescriptor = {
-        size: imageData.data.length,
-        usage: GPUBufferUsage.TRANSFER_SRC | GPUBufferUsage.TRANSFER_DST
-    };
-    const textureDataBuffer = device.createBuffer(textureDataBufferDescriptor);
-    textureDataBuffer.setSubData(0, imageData.data.buffer);
-
-    const dataCopyView = {
-        buffer: textureDataBuffer,
-        offset: 0,
-        rowPitch: image.width * 4,
-        imageHeight: 0
-    };
-    const textureCopyView = {
-        texture: texture,
-        mipLevel: 0,
-        arrayLayer: 0,
-        origin: { x: 0, y: 0, z: 0 }
-    };
-
-    const blitCommandEncoder = device.createCommandEncoder();
-    blitCommandEncoder.copyBufferToTexture(dataCopyView, textureCopyView, textureSize);
-
-    queue = device.getQueue();
-
-    queue.submit([blitCommandEncoder.finish()]);
+    const vertexInputDescriptor = { vertexBuffers: [vertexBufferDescriptor] };
 
     // Bind group binding layout
     const transformBufferBindGroupLayoutBinding = {
@@ -246,29 +177,7 @@ async function init() {
         type: "uniform-buffer"
     };
 
-    const textureBindGroupLayoutBinding = {
-        binding: textureBindingNum,
-        visibility: GPUShaderStageBit.FRAGMENT,
-        type: "sampled-texture"
-    };
-    textureViewBinding = {
-        binding: textureBindingNum,
-        resource: texture.createDefaultView()
-    };
-
-    const samplerBindGroupLayoutBinding = {
-        binding: samplerBindingNum,
-        visibility: GPUShaderStageBit.FRAGMENT,
-        type: "sampler"
-    };
-    samplerBinding = {
-        binding: samplerBindingNum,
-        resource: device.createSampler({})
-    };
-
-    const bindGroupLayoutDescriptor = { 
-        bindings: [transformBufferBindGroupLayoutBinding, textureBindGroupLayoutBinding, samplerBindGroupLayoutBinding] 
-    };
+    const bindGroupLayoutDescriptor = { bindings: [transformBufferBindGroupLayoutBinding] };
     bindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDescriptor);
 
     // Pipeline
@@ -310,7 +219,7 @@ async function init() {
         primitiveTopology: "triangle-list",
         colorStates: [colorState],
         depthStencilState: depthStateDescriptor,
-        inputState: inputStateDescriptor
+        vertexInput: vertexInputDescriptor
     };
     pipeline = device.createRenderPipeline(pipelineDescriptor);
 
@@ -318,7 +227,7 @@ async function init() {
         // attachment is acquired in render loop.
         loadOp: "clear",
         storeOp: "store",
-        clearColor: { r: 0.5, g: 1.0, b: 1.0, a: 1.0 } // GPUColor
+        clearColor: { r: 0.15, g: 0.15, b: 0.5, a: 1.0 } // GPUColor
     };
 
     // Depth stencil texture
@@ -360,24 +269,25 @@ async function init() {
 
 /* Transform Buffers and Bindings */
 const transformSize = 4 * 16;
-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);
-    for (let i = 0; i < 16; i++) {
-        array[i] = modelViewProjectionMatrix[i];
-    }
-}
 
 const transformBufferDescriptor = {
     size: transformSize,
     usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.MAP_WRITE
 };
 
-function createBindGroupDescriptor(transformBuffer, textureViewBinding, samplerBinding) {
+let mappedGroups = [];
+
+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,
@@ -389,25 +299,10 @@ function createBindGroupDescriptor(transformBuffer, textureViewBinding, samplerB
     };
     return {
         layout: bindGroupLayout,
-        bindings: [transformBufferBindGroupBinding, textureViewBinding, samplerBinding]
+        bindings: [transformBufferBindGroupBinding]
     };
 }
 
-let mappedGroups = [];
-
-function render() {
-    if (mappedGroups.length == 0) {
-        const buffer = device.createBuffer(transformBufferDescriptor);
-        buffer.mapWriteAsync().then(arrayBuffer => {
-            const group = device.createBindGroup(createBindGroupDescriptor(buffer, textureViewBinding, samplerBinding));
-
-            let mappedGroup = { buffer: buffer, arrayBuffer: arrayBuffer, bindGroup: group };
-            drawCommands(mappedGroup);
-        });
-    } else
-        drawCommands(mappedGroups.shift());
-}
-
 function drawCommands(mappedGroup) {
     updateTransformArray(new Float32Array(mappedGroup.arrayBuffer));
     mappedGroup.buffer.unmap();
@@ -415,16 +310,19 @@ function drawCommands(mappedGroup) {
     const commandEncoder = device.createCommandEncoder();
     renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView();
     const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
-    // Encode drawing commands.
+
+    // Encode drawing commands
+
     passEncoder.setPipeline(pipeline);
     // Vertex attributes
-    passEncoder.setVertexBuffers(vertexBufferIndex, [verticesBuffer], [0]);
+    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);
     passEncoder.endPass();
 
-    queue.submit([commandEncoder.finish()]);
+    device.getQueue().submit([commandEncoder.finish()]);
 
     // Ready the current buffer for update after GPU is done with it.
     mappedGroup.buffer.mapWriteAsync().then((arrayBuffer) => {
@@ -435,7 +333,17 @@ function drawCommands(mappedGroup) {
     requestAnimationFrame(render);
 }
 
-init();
+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>
\ No newline at end of file
+</html>
index 4003314..5fcd1f0 100644 (file)
@@ -3,18 +3,31 @@
 <head>
 <meta name="viewport" content="width=600">
 <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-<title>Web GPU Hello Triangle demo</title>
+<title>WebGPU Hello Triangle</title>
 <link rel="stylesheet" href="css/style.css">
-<link rel="stylesheet" href="https://webkit.org/wp-content/themes/webkit/style.css">
+<style>
+body {
+    font-family: system-ui;
+    color: #f7f7ff;
+    background-color: rgb(38, 38, 127);
+    text-align: center;
+}
+canvas {
+    margin: 0 auto;
+}
+</style>
 </head>
 <body>
-<canvas></canvas>
+<div id="contents">
+    <h1>Simple Triangle</h1>
+    <canvas></canvas>
+</div>
 <div id="error">
-    <h2>Web GPU not available</h2>
+    <h2>WebGPU not available</h2>
     <p>
-        Make sure you are on a system with Web GPU enabled. In
+        Make sure you are on a system with WebGPU enabled. In
         Safari, first make sure the Developer Menu is visible (Preferences →
-        Advanced), then Develop → Experimental Features → Enable Web GPU.
+        Advanced), then Develop → Experimental Features → WebGPU.
     </p>
 </div>
 <script src="scripts/hello-triangle.js"></script>
index 18e2b37..1944a3f 100644 (file)
@@ -3,7 +3,7 @@
 <head>
 <meta name="viewport" content="width=device-width">
 <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-<title>WebMetal demos</title>
+<title>WebGPU demos</title>
 <link rel="stylesheet" href="https://webkit.org/wp-content/themes/webkit/style.css">
 <link rel="stylesheet" href="https://www.apple.com/wss/fonts?family=Myriad+Set+Pro&amp;v=1">
 <style>
@@ -162,27 +162,26 @@ section p a {
 <body>
     <header>
         <img src="resources/circle.svg">
-        <h1>Web GPU<br>demos</h1>
+        <h1>WebGPU<br>demos</h1>
     </header>
     <section class="demos">
         <p class="intro">
             Here are a collection of simple <a
-            href="https://en.wikipedia.org/wiki/WebGPU">Web GPU</a>
+            href="https://en.wikipedia.org/wiki/WebGPU">WebGPU</a>
             examples. They should work in the latest <a
-            href="https://webkit.org/build-archives/">WebKit</a> builds, and 
-            soon in a <a
-            href="https://developer.apple.com/safari/technology-preview/">Safari
-             Technology Preview</a> release. The full specification is a work-in-progress on <a
+            href="https://webkit.org/build-archives/">WebKit</a> builds and 
+            <a href="https://developer.apple.com/safari/technology-preview/">Safari
+             Technology Preview</a> release. The full <a href="https://gpuweb.github.io/gpuweb/">specification</a> is a work-in-progress on <a
             href="https://github.com/gpuweb/gpuweb
             ">GitHub</a>, and the implementation may differ from the current API.
         </p>
         <p class="howto">
-            Make sure you are on a system with Web GPU enabled. In Safari, first
+            Make sure you are on a system with WebGPU enabled. In Safari, first
             make sure the Developer Menu is visible (Preferences → Advanced),
-            then ensure Develop → Experimental Features → Web GPU is checked.
+            then ensure Develop → Experimental Features → WebGPU is checked.
         </p>
         <p class="warning">
-            Web GPU is an experimental technology, and you should not browse the entire
+            WebGPU is an experimental technology, and you should not browse the entire
             Web with it enabled for now. It doesn't do much validation of content, and
             thus may cause some issues with your computer.
         </p>
@@ -194,10 +193,22 @@ section p a {
         </div>
         <div class="example">
             <a href="hello-cube.html">
-            <img src="resources/hello-cube.png"><br>
+            <img src = "resources/hello-cube.png"><br>
+            Hello World Cube
+            </a>
+        </div>
+        <div class="example">
+            <a href="textured-cube.html">
+            <img src="resources/textured-cube.png"><br>
             Textured Spinning Cube
             </a>
         </div>
+        <div class="example">
+            <a href="compute-blur.html">
+            <img src="resources/compute-blur.png"><br>
+            Compute Shader Blur
+            </a>
+        </div>
     </section>
 </div>
 </body>
diff --git a/Websites/webkit.org/demos/webgpu/resources/compute-blur.png b/Websites/webkit.org/demos/webgpu/resources/compute-blur.png
new file mode 100644 (file)
index 0000000..ef2dfe5
Binary files /dev/null and b/Websites/webkit.org/demos/webgpu/resources/compute-blur.png differ
index 6c59a54..fb46fa1 100644 (file)
Binary files a/Websites/webkit.org/demos/webgpu/resources/hello-cube.png and b/Websites/webkit.org/demos/webgpu/resources/hello-cube.png differ
diff --git a/Websites/webkit.org/demos/webgpu/resources/textured-cube.png b/Websites/webkit.org/demos/webgpu/resources/textured-cube.png
new file mode 100644 (file)
index 0000000..d896c93
Binary files /dev/null and b/Websites/webkit.org/demos/webgpu/resources/textured-cube.png differ
diff --git a/Websites/webkit.org/demos/webgpu/scripts/compute-blur.js b/Websites/webkit.org/demos/webgpu/scripts/compute-blur.js
new file mode 100644 (file)
index 0000000..0c61f6a
--- /dev/null
@@ -0,0 +1,354 @@
+const threadsPerThreadgroup = 32;
+
+const sourceBufferBindingNum = 0;
+const outputBufferBindingNum = 1;
+const uniformsBufferBindingNum = 2;
+
+// Enough space to store 1 radius and 33 weights.
+const maxUniformsSize = (32 + 2) * Float32Array.BYTES_PER_ELEMENT;
+
+let image, context2d, device;
+
+const width = 600;
+
+async function init() {
+    if (!navigator.gpu) {
+        document.body.className = "error";
+        return;
+    }
+
+    const slider = document.querySelector("input");
+    const canvas = document.querySelector("canvas");
+    context2d = canvas.getContext("2d");
+
+    const adapter = await navigator.gpu.requestAdapter();
+    device = await adapter.requestDevice();
+    image = await loadImage(canvas);
+
+    setUpCompute();
+
+    let busy = false;
+    let inputQueue = [];
+    slider.oninput = async () => {
+        inputQueue.push(slider.value);
+        
+        if (busy)
+            return;
+
+        busy = true;
+        while (inputQueue.length != 0)
+            await computeBlur(inputQueue.shift());
+        busy = false;
+    };
+}
+
+async function loadImage(canvas) {
+    /* Image */
+    const image = new Image();
+    const imageLoadPromise = new Promise(resolve => { 
+        image.onload = () => resolve(); 
+        image.src = "resources/safari-alpha.png"
+    });
+    await Promise.resolve(imageLoadPromise);
+
+    canvas.height = width;
+    canvas.width = width;
+
+    context2d.drawImage(image, 0, 0, width, width);
+
+    return image;
+}
+
+let originalData, imageSize;
+let originalBuffer, storageBuffer, resultsBuffer, uniformsBuffer;
+let horizontalBindGroup, verticalBindGroup, horizontalPipeline, verticalPipeline;
+
+function setUpCompute() {
+    originalData = context2d.getImageData(0, 0, image.width, image.height);
+    imageSize = originalData.data.length;
+
+    // Buffer creation
+    let originalArrayBuffer;
+    [originalBuffer, originalArrayBuffer] = device.createBufferMapped({ size: imageSize, usage: GPUBufferUsage.STORAGE });
+    const imageWriteArray = new Uint8ClampedArray(originalArrayBuffer);
+    imageWriteArray.set(originalData.data);
+    originalBuffer.unmap();
+
+    storageBuffer = device.createBuffer({ size: imageSize, usage: GPUBufferUsage.STORAGE });
+    resultsBuffer = device.createBuffer({ size: imageSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ });
+    uniformsBuffer = device.createBuffer({ size: maxUniformsSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.MAP_WRITE });
+
+    // Bind buffers to kernel   
+    const bindGroupLayout = device.createBindGroupLayout({
+        bindings: [{
+            binding: sourceBufferBindingNum,
+            visibility: GPUShaderStageBit.COMPUTE,
+            type: "storage-buffer"
+        }, {
+            binding: outputBufferBindingNum,
+            visibility: GPUShaderStageBit.COMPUTE,
+            type: "storage-buffer"
+        }, {
+            binding: uniformsBufferBindingNum,
+            visibility: GPUShaderStageBit.COMPUTE,
+            type: "uniform-buffer"
+        }]
+    });
+
+    horizontalBindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        bindings: [{
+            binding: sourceBufferBindingNum,
+            resource: {
+                buffer: originalBuffer,
+                size: imageSize
+            }
+        }, {
+            binding: outputBufferBindingNum,
+            resource: {
+                buffer: storageBuffer,
+                size: imageSize
+            }
+        }, {
+            binding: uniformsBufferBindingNum,
+            resource: {
+                buffer: uniformsBuffer,
+                size: maxUniformsSize
+            }
+        }]
+    });
+
+    verticalBindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        bindings: [{
+            binding: sourceBufferBindingNum,
+            resource: {
+                buffer: storageBuffer,
+                size: imageSize
+            }
+        }, {
+            binding: outputBufferBindingNum,
+            resource: {
+                buffer: resultsBuffer,
+                size: imageSize
+            }
+        }, {
+            binding: uniformsBufferBindingNum,
+            resource: {
+                buffer: uniformsBuffer,
+                size: maxUniformsSize
+            }
+        }]
+    });
+
+    // Set up pipelines
+    const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
+
+    const shaderModule = device.createShaderModule({ code: createShaderCode(image), isWHLSL: true });
+
+    horizontalPipeline = device.createComputePipeline({ 
+        layout: pipelineLayout, 
+        computeStage: {
+            module: shaderModule,
+            entryPoint: "horizontal"
+        }
+    });
+
+    verticalPipeline = device.createComputePipeline({
+        layout: pipelineLayout,
+        computeStage: {
+            module: shaderModule,
+            entryPoint: "vertical"
+        }
+    });
+}
+
+async function computeBlur(radius) {
+    if (radius == 0) {
+        context2d.drawImage(image, 0, 0, width, width);
+        return;
+    }
+    const setUniformsPromise = setUniforms(radius);
+    const uniformsMappingPromise = uniformsBuffer.mapWriteAsync();
+
+    const [uniforms, uniformsArrayBuffer] = await Promise.all([setUniformsPromise, uniformsMappingPromise]);
+
+    const uniformsWriteArray = new Float32Array(uniformsArrayBuffer);
+    uniformsWriteArray.set(uniforms);
+    uniformsBuffer.unmap();
+
+    // Run horizontal pass first
+    const commandEncoder = device.createCommandEncoder();
+    const passEncoder = commandEncoder.beginComputePass();
+    passEncoder.setBindGroup(0, horizontalBindGroup);
+    passEncoder.setPipeline(horizontalPipeline);
+    const numXGroups = Math.ceil(image.width / threadsPerThreadgroup);
+    passEncoder.dispatch(numXGroups, image.height, 1);
+    passEncoder.endPass();
+
+    // Run vertical pass
+    const verticalPassEncoder = commandEncoder.beginComputePass();
+    verticalPassEncoder.setBindGroup(0, verticalBindGroup);
+    verticalPassEncoder.setPipeline(verticalPipeline);
+    const numYGroups = Math.ceil(image.height / threadsPerThreadgroup);
+    verticalPassEncoder.dispatch(image.width, numYGroups, 1);
+    verticalPassEncoder.endPass();
+
+    device.getQueue().submit([commandEncoder.finish()]);
+
+    // Draw resultsBuffer as imageData back into context2d
+    const resultArrayBuffer = await resultsBuffer.mapReadAsync();
+    const resultArray = new Uint8ClampedArray(resultArrayBuffer);
+    context2d.putImageData(new ImageData(resultArray, image.width, image.height), 0, 0);
+    resultsBuffer.unmap();
+}
+
+window.addEventListener("load", init);
+
+/* Helpers */
+
+let uniformsCache = new Map();
+
+async function setUniforms(radius)
+{
+    let uniforms = uniformsCache.get(radius);
+    if (uniforms != undefined)
+        return uniforms;
+
+    const sigma = radius / 2.0;
+    const twoSigma2 = 2.0 * sigma * sigma;
+
+    uniforms = [radius];
+    let weightSum = 0;
+
+    for (let i = 0; i <= radius; ++i) {
+        const weight = Math.exp(-i * i / twoSigma2);
+        uniforms.push(weight);
+        weightSum += (i == 0) ? weight : weight * 2;
+    }
+
+    // Compensate for loss in brightness
+    const brightnessScale =  1 - (0.1 / 32.0) * radius;
+    weightSum *= brightnessScale;
+    for (let i = 1; i < uniforms.length; ++i)
+        uniforms[i] /= weightSum;
+        
+    uniformsCache.set(radius, uniforms);
+
+    return uniforms;
+}
+
+const byteMask = (1 << 8) - 1;
+
+function createShaderCode(image) {
+    return `
+uint getR(uint rgba)
+{
+    return rgba & ${byteMask};
+}
+
+uint getG(uint rgba)
+{
+    return (rgba >> 8) & ${byteMask};
+}
+
+uint getB(uint rgba)
+{
+    return (rgba >> 16) & ${byteMask};
+}
+
+uint getA(uint rgba)
+{
+    return (rgba >> 24) & ${byteMask};
+}
+
+uint makeRGBA(uint r, uint g, uint b, uint a)
+{
+    return r + (g << 8) + (b << 16) + (a << 24);
+}
+
+void accumulateChannels(thread uint[] channels, uint startColor, float weight)
+{
+    channels[0] += uint(float(getR(startColor)) * weight);
+    channels[1] += uint(float(getG(startColor)) * weight);
+    channels[2] += uint(float(getB(startColor)) * weight);
+    channels[3] += uint(float(getA(startColor)) * weight);
+
+    // Compensate for brightness-adjusted weights.
+    if (channels[0] > 255)
+        channels[0] = 255;
+
+    if (channels[1] > 255)
+        channels[1] = 255;
+
+    if (channels[2] > 255)
+        channels[2] = 255;
+
+    if (channels[3] > 255)
+        channels[3] = 255;
+}
+
+uint horizontallyOffsetIndex(uint index, int offset, int rowStart, int rowEnd)
+{
+    int offsetIndex = int(index) + offset;
+
+    if (offsetIndex < rowStart || offsetIndex >= rowEnd)
+        return index;
+    
+    return uint(offsetIndex);
+}
+
+uint verticallyOffsetIndex(uint index, int offset, uint length)
+{
+    int realOffset = offset * ${image.width};
+    int offsetIndex = int(index) + realOffset;
+
+    if (offsetIndex < 0 || offsetIndex >= int(length))
+        return index;
+    
+    return uint(offsetIndex);
+}
+
+[numthreads(${threadsPerThreadgroup}, 1, 1)]
+compute void horizontal(constant uint[] source : register(u${sourceBufferBindingNum}),
+                        device uint[] output : register(u${outputBufferBindingNum}),
+                        constant float[] uniforms : register(b${uniformsBufferBindingNum}),
+                        float3 dispatchThreadID : SV_DispatchThreadID)
+{
+    int radius = int(uniforms[0]);
+    int rowStart = ${image.width} * int(dispatchThreadID.y);
+    int rowEnd = ${image.width} * (1 + int(dispatchThreadID.y));
+    uint globalIndex = uint(rowStart) + uint(dispatchThreadID.x);
+
+    uint[4] channels;
+
+    for (int i = -radius; i <= radius; ++i) {
+        uint startColor = source[horizontallyOffsetIndex(globalIndex, i, rowStart, rowEnd)];
+        float weight = uniforms[uint(abs(i) + 1)];
+        accumulateChannels(@channels, startColor, weight);
+    }
+
+    output[globalIndex] = makeRGBA(channels[0], channels[1], channels[2], channels[3]);
+}
+
+[numthreads(1, ${threadsPerThreadgroup}, 1)]
+compute void vertical(constant uint[] source : register(u${sourceBufferBindingNum}),
+                        device uint[] output : register(u${outputBufferBindingNum}),
+                        constant float[] uniforms : register(b${uniformsBufferBindingNum}),
+                        float3 dispatchThreadID : SV_DispatchThreadID)
+{
+    int radius = int(uniforms[0]);
+    uint globalIndex = uint(dispatchThreadID.x) * ${image.height} + uint(dispatchThreadID.y);
+
+    uint[4] channels;
+
+    for (int i = -radius; i <= radius; ++i) {
+        uint startColor = source[verticallyOffsetIndex(globalIndex, i, source.length)];
+        float weight = uniforms[uint(abs(i) + 1)];
+        accumulateChannels(@channels, startColor, weight);
+    }
+
+    output[globalIndex] = makeRGBA(channels[0], channels[1], channels[2], channels[3]);
+}
+`;
+}
\ No newline at end of file
index a56d337..c9e45d0 100644 (file)
@@ -12,33 +12,29 @@ async function helloTriangle() {
     /* GPUShaderModule */
     const positionLocation = 0;
     const colorLocation = 1;
-    
-    const shaderSource = `
-    #include <metal_stdlib>
-  
-    using namespace metal;
-  
-    struct Vertex {
-        float4 position [[attribute(${positionLocation})]];
-        float4 color [[attribute(${colorLocation})]];
-    };
-  
+
+    const whlslSource = `
     struct FragmentData {
-        float4 position [[position]];
-        float4 color;
-    };
-  
-    vertex FragmentData vertexMain(const Vertex in [[stage_in]]) 
+        float4 position : SV_Position;
+        float4 color : attribute(${colorLocation});
+    }
+
+    vertex FragmentData vertexMain(float4 position : attribute(${positionLocation}), float4 color : attribute(${colorLocation}))
     {
-        return FragmentData { in.position, in.color };
+        FragmentData out;
+
+        out.position = position;
+        out.color = color;
+
+        return out;
     }
-  
-    fragment float4 fragmentMain(const FragmentData in [[stage_in]])
+
+    fragment float4 fragmentMain(float4 color : attribute(${colorLocation})) : SV_Target 0
     {
-        return in.color;
+        return color;
     }
     `;
-    const shaderModule = device.createShaderModule({ code: shaderSource });
+    const shaderModule = device.createShaderModule({ code: whlslSource, isWHLSL: true });
     
     /* GPUPipelineStageDescriptors */
     const vertexStageDescriptor = { module: shaderModule, entryPoint: "vertexMain" };
@@ -52,12 +48,12 @@ async function helloTriangle() {
     const vertexDataSize = vertexStride * 3;
     
     /* GPUBufferDescriptor */
-    const vertexBufferDescriptor = { 
+    const vertexDataBufferDescriptor = { 
         size: vertexDataSize,
         usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.VERTEX
     };
     /* GPUBuffer */
-    const vertexBuffer = device.createBuffer(vertexBufferDescriptor);
+    const vertexBuffer = device.createBuffer(vertexDataBufferDescriptor);
     
     /*** Write Data To GPU ***/
     
@@ -78,28 +74,24 @@ async function helloTriangle() {
     /* GPUVertexAttributeDescriptors */
     const positionAttribute = {
         shaderLocation: positionLocation,
-        inputSlot: vertexBufferSlot,
         offset: 0,
         format: "float4"
     };
     const colorAttribute = {
         shaderLocation: colorLocation,
-        inputSlot: vertexBufferSlot,
         offset: colorOffset,
         format: "float4"
     };
-    
-    /* GPUVertexInputDescriptor */
-    const vertexInputDescriptor = {
-        inputSlot: vertexBufferSlot,
+
+    /* GPUVertexBufferDescriptor */
+    const vertexBufferDescriptor = {
         stride: vertexStride,
-        stepMode: "vertex"
+        attributeSet: [positionAttribute, colorAttribute]
     };
-    
-    /* GPUInputStateDescriptor */
-    const inputStateDescriptor = {
-        attributes: [positionAttribute, colorAttribute],
-        inputs: [vertexInputDescriptor]
+
+    /* GPUVertexInputDescriptor */
+    const vertexInputDescriptor = {
+        vertexBuffers: [vertexBufferDescriptor]
     };
     
     /*** Finish Pipeline State ***/
@@ -122,7 +114,7 @@ async function helloTriangle() {
         fragmentStage: fragmentStageDescriptor,
         primitiveTopology: "triangle-list",
         colorStates: [colorStateDescriptor],
-        inputState: inputStateDescriptor
+        vertexInput: vertexInputDescriptor
     };
     /* GPURenderPipeline */
     const renderPipeline = device.createRenderPipeline(renderPipelineDescriptor);
@@ -130,9 +122,8 @@ async function helloTriangle() {
     /*** Swap Chain Setup ***/
     
     const canvas = document.querySelector("canvas");
-    let canvasSize = canvas.getBoundingClientRect();
-    canvas.width = canvasSize.width;
-    canvas.height = canvasSize.height;
+    canvas.width = 600;
+    canvas.height = 600;
 
     const gpuContext = canvas.getContext("gpu");
     
@@ -151,7 +142,7 @@ async function helloTriangle() {
     const renderAttachment = swapChainTexture.createDefaultView();
     
     /* GPUColor */
-    const darkBlue = { r: 0, g: 0, b: 0.5, a: 1 };
+    const darkBlue = { r: 0.15, g: 0.15, b: 0.5, a: 1 };
     
     /* GPURenderPassColorATtachmentDescriptor */
     const colorAttachmentDescriptor = {
diff --git a/Websites/webkit.org/demos/webgpu/textured-cube.html b/Websites/webkit.org/demos/webgpu/textured-cube.html
new file mode 100644 (file)
index 0000000..773fec8
--- /dev/null
@@ -0,0 +1,441 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="viewport" content="width=1000">
+<title>WebGPU Cube</title>
+<script src="scripts/gl-matrix-min.js"></script>
+<link rel="stylesheet" href="css/style.css"/>
+<style>
+body {
+    font-family: system-ui;
+    color: #f7f7ff;
+    background-color: rgb(13, 77, 153);
+    text-align: center;
+}
+canvas {
+    margin: 0 auto;
+}
+p {
+    margin: 0 8px;
+}
+</style>
+</head>
+<body>
+<div id="contents">
+    <h1>Textured Cube</h1>
+    <p>
+        This demo uploads a PNG image as texture data and uses it on the faces of a cube.
+    </p>
+    <canvas width="1200" height="1200"></canvas>
+</div>
+<div id="error">
+    <h2>WebGPU not available</h2>
+    <p>
+        Make sure you are on a system with WebGPU enabled. In
+        Safari, first make sure the Developer Menu is visible (Preferences →
+        Advanced), then Develop → Experimental Features → WebGPU.
+    </p>
+</div>
+<script>
+if (!navigator.gpu)
+    document.body.className = 'error';
+
+const positionAttributeNum  = 0;
+const texCoordsAttributeNum = 1;
+
+const transformBindingNum   = 0;
+const textureBindingNum     = 1;
+const samplerBindingNum     = 2;
+
+const bindGroupIndex        = 0;
+
+const shader = `
+struct FragmentData {
+    float4 position : SV_Position;
+    float2 texCoords : attribute(${texCoordsAttributeNum});
+}
+
+vertex FragmentData vertex_main(
+    float4 position : attribute(${positionAttributeNum}), 
+    float2 texCoords : attribute(${texCoordsAttributeNum}), 
+    constant float4x4[] modelViewProjectionMatrix : register(b${transformBindingNum}))
+{
+    FragmentData out;
+    out.position = mul(modelViewProjectionMatrix[0], position);
+    out.texCoords = texCoords;
+    
+    return out;
+}
+
+fragment float4 fragment_main(
+    float2 texCoords : attribute(${texCoordsAttributeNum}),
+    Texture2D<float4> faceTexture : register(t${textureBindingNum}),
+    sampler faceSampler : register(s${samplerBindingNum})) : SV_Target 0
+{
+    return Sample(faceTexture, faceSampler, texCoords);
+}
+`;
+
+let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor, queue, textureViewBinding, samplerBinding;
+let projectionMatrix = mat4.create();
+
+const texCoordsOffset = 4 * 4;
+const vertexSize = 4 * 6;
+const verticesArray = new Float32Array([
+    // float4 position, float2 texCoords
+    1, -1, -1, 1, 0, 1,
+    -1, -1, -1, 1, 1, 1,
+    -1, 1, -1, 1, 1, 0,
+    1, 1, -1, 1, 0, 0,
+    1, -1, -1, 1, 0, 1,
+    -1, 1, -1, 1, 1, 0,
+
+    1, 1, 1, 1, 0, 0,
+    1, -1, 1, 1, 0, 1,
+    1, -1, -1, 1, 1, 1,
+    1, 1, -1, 1, 1, 0,
+    1, 1, 1, 1, 0, 0,
+    1, -1, -1, 1, 1, 1,
+
+    1, -1, 1, 1, 1, 0, 
+    -1, -1, 1, 1, 0, 0, 
+    -1, -1, -1, 1, 0, 1, 
+    1, -1, -1, 1, 1, 1,
+    1, -1, 1, 1, 1, 0,
+    -1, -1, -1, 1, 0, 1,
+
+    -1, 1, 1, 1, 0, 1,
+    1, 1, 1, 1, 1, 1,
+    1, 1, -1, 1, 1, 0,
+    -1, 1, -1, 1, 0, 0,
+    -1, 1, 1, 1, 0, 1,
+    1, 1, -1, 1, 1, 0,
+
+    -1, -1, 1, 1, 1, 1,
+    -1, 1, 1, 1, 1, 0,
+    -1, 1, -1, 1, 0, 0,
+    -1, -1, -1, 1, 0, 1,
+    -1, -1, 1, 1, 1, 1,
+    -1, 1, -1, 1, 0, 0,
+
+    1, 1, 1, 1, 1, 0,
+    -1, 1, 1, 1, 0, 0,
+    -1, -1, 1, 1, 0, 1,
+    -1, -1, 1, 1, 0, 1,
+    1, -1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 0,
+]);
+
+async function init() {
+    const adapter = await navigator.gpu.requestAdapter();
+    device = await adapter.requestDevice();
+
+    const canvas = document.querySelector('canvas');
+
+    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);
+
+    // WebKit WebGPU accepts only MSL for now.
+    const shaderModuleDescriptor = { code: shader, isWHLSL: true };
+    const shaderModule = device.createShaderModule(shaderModuleDescriptor);
+
+    const verticesBufferDescriptor = { 
+        size: verticesArray.byteLength, 
+        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.TRANSFER_DST
+    };
+    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 texCoordsAttributeDescriptor = {
+        shaderLocation: texCoordsAttributeNum,
+        offset: texCoordsOffset,
+        format: "float2"
+    }
+    const vertexBufferDescriptor = {
+        attributeSet: [positionAttributeDescriptor, texCoordsAttributeDescriptor],
+        stride: vertexSize,
+        stepMode: "vertex"
+    };
+    const vertexInputDescriptor = { vertexBuffers: [vertexBufferDescriptor] };
+
+    // Texture
+
+    // Load texture image
+    const image = new Image();
+    const imageLoadPromise = new Promise(resolve => { 
+        image.onload = () => resolve(); 
+        image.src = "resources/safari-alpha.png"
+    });
+    await Promise.resolve(imageLoadPromise);
+
+    const textureSize = {
+        width: image.width,
+        height: image.height,
+        depth: 1
+    };
+
+    const textureDescriptor = {
+        size: textureSize,
+        arrayLayerCount: 1,
+        mipLevelCount: 1,
+        sampleCount: 1,
+        dimension: "2d",
+        format: "rgba8unorm",
+        usage: GPUTextureUsage.TRANSFER_DST | GPUTextureUsage.SAMPLED
+    };
+    const texture = device.createTexture(textureDescriptor);
+
+    // Texture data 
+    const canvas2d = document.createElement('canvas');
+    canvas2d.width = image.width;
+    canvas2d.height = image.height;
+    const context2d = canvas2d.getContext('2d');
+    context2d.drawImage(image, 0, 0);
+
+    const imageData = context2d.getImageData(0, 0, image.width, image.height);
+
+    const textureDataBufferDescriptor = {
+        size: imageData.data.length,
+        usage: GPUBufferUsage.TRANSFER_SRC
+    };
+    const [textureDataBuffer, textureArrayBuffer] = device.createBufferMapped(textureDataBufferDescriptor);
+    
+    const textureWriteArray = new Uint8Array(textureArrayBuffer);
+    textureWriteArray.set(imageData.data);
+    textureDataBuffer.unmap();
+
+    const dataCopyView = {
+        buffer: textureDataBuffer,
+        offset: 0,
+        rowPitch: image.width * 4,
+        imageHeight: 0
+    };
+    const textureCopyView = {
+        texture: texture,
+        mipLevel: 0,
+        arrayLayer: 0,
+        origin: { x: 0, y: 0, z: 0 }
+    };
+
+    const blitCommandEncoder = device.createCommandEncoder();
+    blitCommandEncoder.copyBufferToTexture(dataCopyView, textureCopyView, textureSize);
+
+    queue = device.getQueue();
+
+    queue.submit([blitCommandEncoder.finish()]);
+
+    // Bind group binding layout
+    const transformBufferBindGroupLayoutBinding = {
+        binding: transformBindingNum, // id[[(0)]]
+        visibility: GPUShaderStageBit.VERTEX,
+        type: "uniform-buffer"
+    };
+
+    const textureBindGroupLayoutBinding = {
+        binding: textureBindingNum,
+        visibility: GPUShaderStageBit.FRAGMENT,
+        type: "sampled-texture"
+    };
+    textureViewBinding = {
+        binding: textureBindingNum,
+        resource: texture.createDefaultView()
+    };
+
+    const samplerBindGroupLayoutBinding = {
+        binding: samplerBindingNum,
+        visibility: GPUShaderStageBit.FRAGMENT,
+        type: "sampler"
+    };
+    samplerBinding = {
+        binding: samplerBindingNum,
+        resource: device.createSampler({})
+    };
+
+    const bindGroupLayoutDescriptor = { 
+        bindings: [transformBufferBindGroupLayoutBinding, textureBindGroupLayoutBinding, samplerBindGroupLayoutBinding] 
+    };
+    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.05, g: .3, b: .6, a: 1.0 } // GPUColor
+    };
+
+    // Depth stencil texture
+
+    // GPUExtent3D
+    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
+    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
+};
+
+let mappedGroups = [];
+
+function render() {
+    if (mappedGroups.length === 0) {
+        const [buffer, arrayBuffer] = device.createBufferMapped(transformBufferDescriptor);
+        const group = device.createBindGroup(createBindGroupDescriptor(buffer, textureViewBinding, samplerBinding));
+        let mappedGroup = { buffer: buffer, arrayBuffer: arrayBuffer, bindGroup: group };
+        drawCommands(mappedGroup);
+    } else
+        drawCommands(mappedGroups.shift());
+}
+
+function createBindGroupDescriptor(transformBuffer, textureViewBinding, samplerBinding) {
+    const transformBufferBinding = {
+        buffer: transformBuffer,
+        offset: 0,
+        size: transformSize
+    };
+    const transformBufferBindGroupBinding = {
+        binding: transformBindingNum,
+        resource: transformBufferBinding
+    };
+    return {
+        layout: bindGroupLayout,
+        bindings: [transformBufferBindGroupBinding, textureViewBinding, samplerBinding]
+    };
+}
+
+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);
+    passEncoder.draw(36, 1, 0, 0);
+    passEncoder.endPass();
+
+    queue.submit([commandEncoder.finish()]);
+
+    // Ready the current buffer for update after GPU is done with it.
+    mappedGroup.buffer.mapWriteAsync().then((arrayBuffer) => {
+        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), 1, 1));
+    let modelViewProjectionMatrix = mat4.create();
+    mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix);
+    mat4.copy(array, modelViewProjectionMatrix);
+}
+
+window.addEventListener("load", init);
+</script>
+</body>
+</html>
\ No newline at end of file