[WHLSL] Make whlsl-test-harness actually generate WHLSL shaders by default
authorjustin_fan@apple.com <justin_fan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jun 2019 01:40:52 +0000 (01:40 +0000)
committerjustin_fan@apple.com <justin_fan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jun 2019 01:40:52 +0000 (01:40 +0000)
https://bugs.webkit.org/show_bug.cgi?id=199028

Reviewed by Saam Barati.

whlsl-test-harness.js now generates WHLSL shaders and invokes WebKit's WHLSL compiler.
MSL mode remains to facilitate further harness tesing.
In addition, if WebGPU is not supported, synchronous Harness methods do nothing.
Asynchronous methods will throw a WebGPUUnsupportedError that "rejects" the returned Promise.

* TestExpectations:
* platform/mac/TestExpectations:
* webgpu/js/whlsl-test-harness.js:
(WebGPUUnsupportedError): Layout tests should catch these to fail gracefully if WebGPU is not supported.
(Data):
(Data.prototype.async.getArrayBuffer):
(Data.prototype.get isBuffer): Renamed from isPointer.
(Harness):
(Harness.prototype.async.requestDevice): Can be used to re-acquire a GPUDevice.
(Harness.prototype.set isWHLSL): Determines whether harness will generate WHLSL or MSL shaders.
(Harness.prototype.async.callTypedFunction):
(Harness.prototype.callVoidFunction):
(Harness.prototype.get device):
(Harness.prototype._clearResults):
(Harness.prototype._setUpArguments):
(Harness.prototype._callFunction):
(Data.prototype.get isPointer): Deleted.
(Harness.prototype._initialize): Deleted.
(Harness.prototype.async.callVoidFunction): Deleted.
(harness._initialize.async): Deleted.
* webgpu/msl-harness-test-expected.txt: Renamed from LayoutTests/webgpu/whlsl-harness-test-expected.txt.
* webgpu/msl-harness-test.html: Copied from LayoutTests/webgpu/whlsl-harness-test.html.
* webgpu/whlsl-test-harness-test-expected.html: Added.
* webgpu/whlsl-test-harness-test.html: Renamed from LayoutTests/webgpu/whlsl-harness-test.html.

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

LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/platform/mac/TestExpectations
LayoutTests/webgpu/js/whlsl-test-harness.js
LayoutTests/webgpu/msl-harness-test-expected.txt [moved from LayoutTests/webgpu/whlsl-harness-test-expected.txt with 100% similarity]
LayoutTests/webgpu/msl-harness-test.html [moved from LayoutTests/webgpu/whlsl-harness-test.html with 89% similarity]
LayoutTests/webgpu/whlsl-test-harness-test-expected.txt [new file with mode: 0644]
LayoutTests/webgpu/whlsl-test-harness-test.html [new file with mode: 0644]

index 6c61f18..4885271 100644 (file)
@@ -1,3 +1,40 @@
+2019-06-25  Justin Fan  <justin_fan@apple.com>
+
+        [WHLSL] Make whlsl-test-harness actually generate WHLSL shaders by default
+        https://bugs.webkit.org/show_bug.cgi?id=199028
+
+        Reviewed by Saam Barati.
+
+        whlsl-test-harness.js now generates WHLSL shaders and invokes WebKit's WHLSL compiler.
+        MSL mode remains to facilitate further harness tesing.
+        In addition, if WebGPU is not supported, synchronous Harness methods do nothing.
+        Asynchronous methods will throw a WebGPUUnsupportedError that "rejects" the returned Promise.
+
+        * TestExpectations:
+        * platform/mac/TestExpectations:
+        * webgpu/js/whlsl-test-harness.js:
+        (WebGPUUnsupportedError): Layout tests should catch these to fail gracefully if WebGPU is not supported.
+        (Data):
+        (Data.prototype.async.getArrayBuffer):
+        (Data.prototype.get isBuffer): Renamed from isPointer.
+        (Harness):
+        (Harness.prototype.async.requestDevice): Can be used to re-acquire a GPUDevice.
+        (Harness.prototype.set isWHLSL): Determines whether harness will generate WHLSL or MSL shaders.
+        (Harness.prototype.async.callTypedFunction):
+        (Harness.prototype.callVoidFunction):
+        (Harness.prototype.get device):
+        (Harness.prototype._clearResults):
+        (Harness.prototype._setUpArguments):
+        (Harness.prototype._callFunction):
+        (Data.prototype.get isPointer): Deleted.
+        (Harness.prototype._initialize): Deleted.
+        (Harness.prototype.async.callVoidFunction): Deleted.
+        (harness._initialize.async): Deleted.
+        * webgpu/msl-harness-test-expected.txt: Renamed from LayoutTests/webgpu/whlsl-harness-test-expected.txt.
+        * webgpu/msl-harness-test.html: Copied from LayoutTests/webgpu/whlsl-harness-test.html.
+        * webgpu/whlsl-test-harness-test-expected.html: Added.
+        * webgpu/whlsl-test-harness-test.html: Renamed from LayoutTests/webgpu/whlsl-harness-test.html.
+
 2019-06-25  Daniel Bates  <dabates@apple.com>
 
         Non-editable text selections should be modifiable with hardware keyboard
index 54b100b..69382a9 100644 (file)
@@ -3440,3 +3440,5 @@ imported/w3c/web-platform-tests/websockets/Secure-Send-unpaired-surrogates.any.w
 fast/dom/linkify-phone-numbers.html [ ImageOnlyFailure ]
 
 webkit.org/b/199039 editing/deleting/smart-delete-paragraph-003.html [ Skip ]
+
+webkit.org/b/199028 webgpu/whlsl-test-harness-test.html [ Slow ]
index 51ef576..e775c7f 100644 (file)
@@ -1786,7 +1786,8 @@ webkit.org/b/192956 [ Sierra ] webgpu [ Skip ]
 webkit.org/b/192956 [ Sierra ] inspector/canvas/create-context-webgpu.html [ Skip ]
 webkit.org/b/192956 [ Sierra ] inspector/canvas/resolveCanvasContext-webgpu.html [ Skip ]
 
-webkit.org/b/199076 [ HighSierra ] webgpu/whlsl-harness-test.html [ Skip ]
+webkit.org/b/199076 [ HighSierra ] webgpu/whlsl-test-harness-test.html [ Skip ]
+webkit.org/b/199028 [ HighSierra ] webgpu/msl-harness-test.html [ Skip ]
 
 webkit.org/b/189680 platform/mac/media/audio-session-category-video-paused.html [ Pass Timeout ]
 
index 2c92130..eab177d 100644 (file)
@@ -63,28 +63,37 @@ function convertTypeToWHLSLType(type)
 
 /* Harness Classes */
 
+class WebGPUUnsupportedError extends Error {
+    constructor()
+    {
+        super("No GPUDevice detected!");
+    }
+};
+
 class Data {
     /**
      * Upload typed data to and return a wrapper of a GPUBuffer.
      * @param {Types} type - The WHLSL type to be stored in this Data.
      * @param {Number or Array[Number]} values - The raw data to be uploaded.
      */
-    constructor(harness, type, values, isPointer = false)
+    constructor(harness, type, values, isBuffer = false)
     {
-        // One or more scalars in an array can be accessed through a pointer to buffer.
+        if (harness.device === undefined)
+            return;
+        // One or more scalars in an array can be accessed through an array reference.
         // However, vector types are also created via an array of scalars.
         // This ensures that buffers of just one vector are usable in a test function.
         if (Array.isArray(values))
-            this._isPointer = isVectorType(type) ? isPointer : true;
+            this._isBuffer = isVectorType(type) ? isBuffer : true;
         else {
-            this._isPointer = false;
+            this._isBuffer = false;
             values = [values];
         }
 
         this._type = type;
         this._byteLength = (convertTypeToArrayType(type)).BYTES_PER_ELEMENT * values.length;
 
-        const [buffer, arrayBuffer] = harness._device.createBufferMapped({
+        const [buffer, arrayBuffer] = harness.device.createBufferMapped({
             size: this._byteLength,
             usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ
         });
@@ -101,6 +110,9 @@ class Data {
      */
     async getArrayBuffer()
     {
+        if (harness.device === undefined)
+            throw new WebGPUUnsupportedError();
+
         let result;
         try {
             result = await this._buffer.mapReadAsync();
@@ -112,22 +124,37 @@ class Data {
     }
 
     get type() { return this._type; }
-    get isPointer() { return this._isPointer; }
+    get isBuffer() { return this._isBuffer; }
     get buffer() { return this._buffer; }
     get byteLength() { return this._byteLength; }
 }
 
 class Harness {
-    constructor()
+    constructor ()
     {
-        this._shaderHeader = `#include <metal_stdlib>
-        using namespace metal;
-        `;
+        this.isWHLSL = true;
     }
 
-    _initialize(callback)
+    async requestDevice()
     {
-        callback.bind(this)();
+        try {
+            const adapter = await navigator.gpu.requestAdapter();
+            this._device = await adapter.requestDevice();
+        } catch {
+            // WebGPU is not supported.
+            // FIXME: Add support for GPUAdapterRequestOptions and GPUDeviceDescriptor,
+            // and differentiate between descriptor validation errors and no WebGPU support.
+        }
+    }
+
+    // Sets whether Harness generates WHLSL or MSL shaders.
+    set isWHLSL(value)
+    {
+        this._isWHLSL = value;
+        this._shaderHeader = value ? "" : `
+#include <metal_stdlib>
+using namespace metal;
+        `;
     }
 
     /**
@@ -140,12 +167,17 @@ class Harness {
      */
     async callTypedFunction(type, functions, name, args)
     {   
-        const [argsLayouts, argsResourceBindings, argsStructCode, functionCallArgs] = this._setUpArguments(args);
+        if (this._device === undefined)
+            throw new WebGPUUnsupportedError();
 
-        if (!this._resultBuffer) {
-            this._resultBuffer = this._device.createBuffer({ 
+        const [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs] = this._setUpArguments(args);
+
+        if (this._resultBuffer) {
+            this._clearResults()
+        } else {
+            this._resultBuffer = this.device.createBuffer({ 
                 size: Types.MAX_SIZE, 
-                usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ 
+                usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ | GPUBufferUsage.TRANSFER_DST
             });
         }
 
@@ -162,17 +194,30 @@ class Harness {
             }
         });
 
-        const code = this._shaderHeader + functions + `
-        struct _compute_args {
-            device ${convertTypeToWHLSLType(type)}* result [[id(0)]];
-            ${argsStructCode}};
-    
-        kernel void _compute_main(device _compute_args& args [[buffer(0)]]) 
-        {
-            *args.result = ${name}(${functionCallArgs.join(", ")});
+        let entryPointCode;
+        if (this._isWHLSL) {
+            argsDeclarations.unshift(`device ${convertTypeToWHLSLType(type)}[] result : register(u0)`);
+            entryPointCode = `
+[numthreads(1, 1, 1)]
+compute void _compute_main(${argsDeclarations.join(", ")})
+{
+    result[0] = ${name}(${functionCallArgs.join(", ")});
+}
+`;
+        } else {
+            argsDeclarations.unshift(`device ${convertTypeToWHLSLType(type)}* result [[id(0)]];`);
+            entryPointCode = `
+struct _compute_args {
+    ${argsDeclarations.join("\n")}
+};
+
+kernel void _compute_main(device _compute_args& args [[buffer(0)]]) 
+{
+    *args.result = ${name}(${functionCallArgs.join(", ")});
+}
+`;
         }
-        `;
-    
+        const code = this._shaderHeader + functions + entryPointCode;
         this._callFunction(code, argsLayouts, argsResourceBindings);
     
         try {
@@ -192,23 +237,52 @@ class Harness {
      * @param {String} name - The name of the WHLSL function which must be present in 'functions'.
      * @param {Data or Array[Data]} args - Data arguments to be passed to the call of 'name'.
      */
-    async callVoidFunction(functions, name, args)
+    callVoidFunction(functions, name, args)
     {
-        const [argsLayouts, argsResourceBindings, argsStructCode, functionCallArgs] = this._setUpArguments(args);
+        if (this._device === undefined)
+            return;
 
-        const code = this._shaderHeader + functions + `
-        struct _compute_args {
-            ${argsStructCode}};
+        const [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs] = this._setUpArguments(args);
 
-        kernel void _compute_main(device _compute_args& args [[buffer(0)]])
-        {
-            ${name}(${functionCallArgs.join(", ")});
+        let entryPointCode;
+        if (this._isWHLSL) {
+            entryPointCode = `
+[numthreads(1, 1, 1)]
+compute void _compute_main(${argsDeclarations.join(", ")})
+{
+    ${name}(${functionCallArgs.join(", ")});
+}`;
+        } else {
+            entryPointCode = `
+struct _compute_args {
+    ${argsDeclarations.join("\n")}
+};
+
+kernel void _compute_main(device _compute_args& args [[buffer(0)]])
+{
+    ${name}(${functionCallArgs.join(", ")});
+}
+`;
         }
-        `;
-
+        const code = this._shaderHeader + functions + entryPointCode;
         this._callFunction(code, argsLayouts, argsResourceBindings);
     }
 
+    get device() { return this._device; }
+
+    _clearResults()
+    {
+        if (!this._clearBuffer) {
+            this._clearBuffer = this._device.createBuffer({ 
+                size: Types.MAX_SIZE, 
+                usage: GPUBufferUsage.TRANSFER_SRC
+            });
+        }
+        const commandEncoder = this._device.createCommandEncoder();
+        commandEncoder.copyBufferToBuffer(this._clearBuffer, 0, this._resultBuffer, 0, Types.MAX_SIZE);
+        this._device.getQueue().submit([commandEncoder.finish()]);
+    }
+
     _setUpArguments(args)
     {
         if (!Array.isArray(args)) {
@@ -218,19 +292,21 @@ class Harness {
                 args = [];
         }
 
-        // FIXME: Replace with WHLSL.
         // Expand bind group structure to represent any arguments.
-        let argsStructCode = "";
+        let argsDeclarations = [];
         let functionCallArgs = [];
         let argsLayouts = [];
         let argsResourceBindings = [];
 
         for (let i = 1; i <= args.length; ++i) {
             const arg = args[i - 1];
-            argsStructCode += `device ${convertTypeToWHLSLType(arg.type)}* arg${i} [[id(${i})]];
-            `;
-            const optionalDeref = (!arg.isPointer) ? "*" : "";
-            functionCallArgs.push(optionalDeref + `args.arg${i}`);
+            if (this._isWHLSL) {
+                argsDeclarations.push(`device ${convertTypeToWHLSLType(arg.type)}[] arg${i} : register(u${i})`);
+                functionCallArgs.push(`arg${i}` + (arg.isBuffer ? "" : "[0]"));
+            } else {
+                argsDeclarations.push(`device ${convertTypeToWHLSLType(arg.type)}* arg${i} [[id(${i})]];`);
+                functionCallArgs.push((arg.isBuffer ? "" : "*") + `args.arg${i}`);
+            }
             argsLayouts.push({
                 binding: i,
                 visibility: GPUShaderStageBit.COMPUTE,
@@ -245,29 +321,33 @@ class Harness {
             });
         }
 
-        return [argsLayouts, argsResourceBindings, argsStructCode, functionCallArgs];
+        return [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs];
     }
 
     _callFunction(code, argsLayouts, argsResourceBindings)
     {
-        const shaders = this._device.createShaderModule({ code: code });
+        const shaders = this._device.createShaderModule({ code: code, isWHLSL: this._isWHLSL });
+
+        const bindGroupLayout = this._device.createBindGroupLayout({
+            bindings: argsLayouts
+        });
+
+        const pipelineLayout = this._device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
+
+        const bindGroup = this._device.createBindGroup({
+            layout: bindGroupLayout,
+            bindings: argsResourceBindings
+        });
+
         // FIXME: Compile errors should be caught and reported here.
         const pipeline = this._device.createComputePipeline({
+            layout: pipelineLayout,
             computeStage: {
                 module: shaders,
                 entryPoint: "_compute_main"
             }
         });
 
-        const layout = this._device.createBindGroupLayout({
-            bindings: argsLayouts
-        });
-    
-        const bindGroup = this._device.createBindGroup({
-            layout: layout,
-            bindings: argsResourceBindings
-        });
-        
         const commandEncoder = this._device.createCommandEncoder();
         const passEncoder = commandEncoder.beginComputePass();
         passEncoder.setBindGroup(0, bindGroup);
@@ -282,14 +362,7 @@ class Harness {
 /* Harness Setup */
 
 const harness = new Harness();
-harness._initialize(async () => {
-    try {
-        const adapter = await navigator.gpu.requestAdapter();
-        harness._device = await adapter.requestDevice();
-    } catch (e) {
-        throw new Error("Harness error: Unable to acquire GPUDevice!");
-    }
-});
+harness.requestDevice();
 
 /* Global Helper Functions */
 
similarity index 89%
rename from LayoutTests/webgpu/whlsl-harness-test.html
rename to LayoutTests/webgpu/msl-harness-test.html
index c00081d..8d43fce 100644 (file)
@@ -24,21 +24,21 @@ const scalarArgMakers = {
     "float": makeFloat
 };
 
-let tests = {};
+let mslTests = {};
 
-tests.literals = () => {
+mslTests.literals = () => {
     checkBools("Return a literal of type bool.", "return true;");
     checkFloat4s("return float4(0, 1, 2, 3);");
     checkNumericScalars("return 42;", [], 42);
 };
 
-tests.singleArgument = () => {
+mslTests.singleArgument = () => {
     checkBools("Upload and return a bool value.", "return in0;", [true]);
     checkFloat4s("return in0.wzyx;", [[3, 2, 1, 0]]);
     checkNumericScalars("return in0;", [42], 42);
 };
 
-tests.manyArguments = () => {
+mslTests.manyArguments = () => {
     checkBools("Upload many bool values and return a calculated result.",
         "return in0 & in1 & in2 & in3 & in4 & in5 & in6 & in7;",  
         [true, true, true, true, true, true, true, true]);
@@ -51,14 +51,14 @@ tests.manyArguments = () => {
     checkNumericScalars(body, [0, 1, 2, 3, 4, 5, 6, 7], 28);
 };
 
-tests.buffersWithOneValue = () => {
+mslTests.buffersWithOneValue = () => {
     const body = `return in0[0];`
     checkBools("Access and return a single bool through a bool*.", body, [[true]]);
     checkFloat4s(body, [[[0, 1, 2, 3]]]);
     checkNumericScalars(body, [[42]], 42);
 };
 
-tests.multipleBufferArguments = () => {
+mslTests.multipleBufferArguments = () => {
     checkBools("Access multiple bools through various buffers and return a bool.", 
         "return in0[0] & in0[1] & in0[2] & in1 & in2[0];", 
         [[true, true, true], true, [true]]);
@@ -69,7 +69,7 @@ tests.multipleBufferArguments = () => {
     checkNumericScalars(body, [[0, 1, 2], 3, [4]], 10);
 };
 
-tests.multipleArgumentTypes = () => {
+mslTests.multipleArgumentTypes = () => {
     const src = `float test(int i, uchar c, device uint* u, bool b, device bool* bs, float4 f4, device float* fs)
     {
         if (b && bs[0] && bs[1])
@@ -84,21 +84,21 @@ tests.multipleArgumentTypes = () => {
     const bs = makeBool([true, true]);
     const f4 = makeFloat4([4, 5, 6, 7]);
     const fs = makeFloat([8, 9]);
-    promise_test(() => {
+    webGPUPromiseTest(() => {
         return callFloatFunction(src, "test", [i, c, u, b, bs, f4, fs]).then(result => {
             assert_approx_equals(result, 45, epsilon, "Test returned expected value.");
         });
     }, "Upload and calculate a result from varied argument types.");
 };
 
-tests.bufferStores = () => {
+mslTests.bufferStores = () => {
     let src = `void test(device float4* out) {
         *out = float4(0, 1, 2, 3);
     }`;
     const float4Out = makeFloat4([[0, 0, 0, 0]]);
     callVoidFunction(src, "test", float4Out);
 
-    promise_test(() => {
+    webGPUPromiseTest(() => {
         return float4Out.getArrayBuffer().then(arrayBuffer => {
             const result = new Float32Array(arrayBuffer);
             for (let i; i < 4; ++i) {
@@ -116,7 +116,7 @@ tests.bufferStores = () => {
     const output = makeUchar([0, 0, 0, 0, 0]);
     callVoidFunction(src, "test", [input, output]);
 
-    promise_test(() => {
+    webGPUPromiseTest(() => {
         return output.getArrayBuffer().then(arrayBuffer => {
             const result = new Uint8Array(arrayBuffer);
             assert_array_equals(array, result, "Test stored expected values.");
@@ -125,8 +125,9 @@ tests.bufferStores = () => {
 };
 
 window.addEventListener("load", () => {
-    for (const name in tests) {
-        tests[name]();
+    harness.isWHLSL = false;
+    for (const name in mslTests) {
+        mslTests[name]();
     }
 });
 
@@ -153,7 +154,7 @@ const checkNumericScalars = (body, argValues, expected) => {
 
     for (const f of functions) {
         const callFunc = numericScalarFuncs[f.type];
-        promise_test(async () => {
+        webGPUPromiseTest(async () => {
             return callFunc(src, f.name, f.args).then(result => {
                 assert_approx_equals(result, f.expected, epsilon, "Test returned expected value.");
             });
@@ -174,7 +175,7 @@ const checkBools = (msg = "Return an expected bool value.", body, argValues = []
     src += `bool boolTest(${inputArgs.join(", ")}) { ${body} }
     `;
 
-    promise_test(async () => {
+    webGPUPromiseTest(async () => {
         return callBoolFunction(src, "boolTest", values).then(result => {
             assert_equals(result, expected, "Test returned expected value.");
         });
@@ -197,12 +198,21 @@ const checkFloat4s = (body, argValues = [], expected = [0, 1, 2, 3]) => {
     src += `float4 float4Test(${inputArgs.join(", ")}) { ${body} }
     `;
 
-    promise_test(async () => {
+    webGPUPromiseTest(async () => {
         return callFloat4Function(src, "float4Test", values).then(result => {
             for (let i = 0; i < 4; ++i)
                 assert_approx_equals(result[i], expected[i], epsilon, "Test returned expected value.");
         });
     }, "Return an expected float4 value.");
 }
+
+const webGPUPromiseTest = (testFunc, msg) => {
+    promise_test(async () => { 
+        return testFunc().catch(e => {
+        if (!(e instanceof WebGPUUnsupportedError))
+            throw e;
+        });
+    }, msg);
+}
 </script>
 </html>
\ No newline at end of file
diff --git a/LayoutTests/webgpu/whlsl-test-harness-test-expected.txt b/LayoutTests/webgpu/whlsl-test-harness-test-expected.txt
new file mode 100644 (file)
index 0000000..d0fc6f2
--- /dev/null
@@ -0,0 +1,25 @@
+
+PASS Return an expected float4 value. 
+PASS Return an expected int value. 
+PASS Return an expected uint value. 
+PASS Return an expected float value. 
+PASS Return an expected float4 value. 
+PASS Return an expected int value. 
+PASS Return an expected uint value. 
+PASS Return an expected float value. 
+PASS Return an expected float4 value. 
+PASS Return an expected int value. 
+PASS Return an expected uint value. 
+PASS Return an expected float value. 
+PASS Return an expected float4 value. 
+PASS Return an expected int value. 
+PASS Return an expected uint value. 
+PASS Return an expected float value. 
+PASS Return an expected float4 value. 
+PASS Return an expected int value. 
+PASS Return an expected uint value. 
+PASS Return an expected float value. 
+PASS Upload and calculate a result from varied argument types. 
+PASS Store into a float4[]. 
+PASS Upload a int[] and store into a int[]. 
+
diff --git a/LayoutTests/webgpu/whlsl-test-harness-test.html b/LayoutTests/webgpu/whlsl-test-harness-test.html
new file mode 100644 (file)
index 0000000..8a9b505
--- /dev/null
@@ -0,0 +1,234 @@
+<!DOCTYPE html><!-- webkit-test-runner [ experimental:WebGPUEnabled=true ] -->
+<html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Test the WHLSL test harness.</title>
+<script src="js/whlsl-test-harness.js"></script>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<script>
+const epsilon = 0.0001;
+
+// FIXME: Add "uchar" back in when operator+(uchar, uchar) is available.
+const numericScalarTypes = ["int", "uint", "float"];
+
+const numericScalarFuncs = {
+    "int": callIntFunction,
+    "uchar": callUcharFunction,
+    "uint": callUintFunction,
+    "float": callFloatFunction
+};
+
+const scalarArgMakers = {
+    "bool": makeBool,
+    "int": makeInt,
+    "uchar": makeUchar,
+    "uint": makeUint,
+    "float": makeFloat
+};
+
+let whlslTests = {};
+
+whlslTests.literals = () => {
+    checkBools("Return a literal of type bool.", "return true;");
+    checkFloat4s("return float4(0, 1, 2, 3);");
+    checkNumericScalars("return 42;", [], 42);
+};
+
+whlslTests.singleArgument = () => {
+    checkBools("Upload and return a bool value.", "return in0;", [true]);
+    checkFloat4s("return in0;", [[0, 1, 2, 3]]);
+    checkNumericScalars("return in0;", [42], 42);
+};
+
+whlslTests.manyArguments = () => {
+    checkBools("Upload many bool values and return a calculated result.",
+        "return in0 & in1 & in2 & in3 & in4 & in5 & in6 & in7;",
+        [true, true, true, true, true, true, true, true]);
+
+    let body = "return float4(in0.x, in1.y, in2.z, in3.w);"
+    let args = [];
+    for (let i = 0; i < 4; ++i)
+        args.push([0, 1, 2, 3]);
+    checkFloat4s(body, args, [0, 1, 2, 3]);
+
+    body = `return in0 + in1 + in2 + in3 + in4 + in5 + in6 + in7;`;
+    checkNumericScalars(body, [0, 1, 2, 3, 4, 5, 6, 7], 28);
+};
+
+whlslTests.buffersWithOneValue = () => {
+    const body = `return in0[0];`
+    checkBools("Access and return a single bool through a bool[].", body, [[true]]);
+    checkFloat4s(body, [[[0, 1, 2, 3]]]);
+    checkNumericScalars(body, [[42]], 42);
+};
+
+whlslTests.multipleBufferArguments = () => {
+    checkBools("Access multiple bools through various buffers and return a bool.", 
+        "return in0[0] & in0[1] & in0[2] & in1 & in2[0];", 
+        [[true, true, true], true, [true]]);
+
+    let body = `
+    float x = in0[0].x + in0[1].x + in0[2].x + in1.x + in2[0].x;
+    float y = in0[0].y + in0[1].y + in0[2].y + in1.y + in2[0].y;
+    float z = in0[0].z + in0[1].z + in0[2].z + in1.z + in2[0].z;
+    float w = in0[0].w + in0[1].w + in0[2].w + in1.w + in2[0].w;
+
+    return float4(x, y, z, w);`;
+    const vector = [0, 1, 2, 3];
+    checkFloat4s(body, [[vector, vector, vector], vector, [vector]], [0, 5, 10, 15]);
+
+    body = `return in0[0] + in0[1] + in0[2] + in1 + in2[0];`;
+    checkNumericScalars(body, [[0, 1, 2], 3, [4]], 10);
+};
+
+whlslTests.multipleArgumentTypes = () => {
+    const src = `float test(int i, uchar c, float4 f4, device uint[] u, device float[] fs)
+    {
+        return float(i) + float(c) + f4.x + f4.y + f4.z + f4.w + float(u[0]) + fs[0] + fs[1];
+    }`;
+    const i = makeInt(1);
+    const c = makeUchar(2);
+    const f4 = makeFloat4([4, 5, 6, 7]);
+    const u = makeUint([3]);
+    const fs = makeFloat([8, 9]);
+    webGPUPromiseTest(() => {
+        return callFloatFunction(src, "test", [i, c, f4, u, fs]).then(result => {
+            assert_approx_equals(result, 45, epsilon, "Test returned expected value.");
+        });
+    }, "Upload and calculate a result from varied argument types.");
+};
+
+whlslTests.bufferStores = () => {
+    let src = `void test(device float4[] out) {
+        out[0] = float4(0, 1, 2, 3);
+    }`;
+    const float4Out = makeFloat4([[0, 0, 0, 0]]);
+    callVoidFunction(src, "test", float4Out);
+
+    webGPUPromiseTest(() => {
+        return float4Out.getArrayBuffer().then(arrayBuffer => {
+            const result = new Float32Array(arrayBuffer);
+            for (let i; i < 4; ++i) {
+                assert_approx_equals(result[i], i, "Test stored expected values.");
+            }
+        });
+    }, "Store into a float4[].");
+
+    src = `void test(device int[] in, device int[] out) {
+        for (uint i = 0; i < 5; i = i + 1)
+            out[i] = in[i];
+    }`;
+    const array = [0, 1, 2, 3, 4];
+    const input = makeInt(array);
+    const output = makeInt([0, 0, 0, 0, 0]);
+    callVoidFunction(src, "test", [input, output]);
+
+    webGPUPromiseTest(() => {
+        return output.getArrayBuffer().then(arrayBuffer => {
+            const result = new Uint32Array(arrayBuffer);
+            assert_array_equals(array, result, "Test stored expected values.");
+        });
+    }, "Upload a int[] and store into a int[].");
+};
+
+window.addEventListener("load", () => {
+    try {
+        for (const name in whlslTests) {
+            if (!name.startsWith("_"))
+                whlslTests[name]();
+        }
+    } catch (e) {
+        if (window.testRunner)
+            testRunner.notifyDone();
+        
+        throw e;
+    }
+});
+
+/* Helper functions */
+
+const checkNumericScalars = (body, argValues, expected) => {
+    let functions = [];
+    let src = "";
+    for (let type of numericScalarTypes) {
+        let name, values;
+        [src, name, values] = appendScalarFunctionToSource(src, type, body, argValues);
+        functions.push({ type: type, name: name, args: values, expected: expected });
+    }
+
+    for (const f of functions) {
+        const callFunc = numericScalarFuncs[f.type];
+        webGPUPromiseTest(async () => {
+            return callFunc(src, f.name, f.args).then(result => {
+                assert_approx_equals(result, f.expected, epsilon, "Test returned expected value.");
+            });
+        }, `Return an expected ${f.type} value.`);
+    }
+};
+
+const checkBools = (msg = "Return an expected bool value.", body, argValues = [], expected = true) => {
+    // FIXME (https://webkit.org/b/199093): Bool[] functions don't compile, so no-op for now.
+    return;
+
+    const [src, name, values] = appendScalarFunctionToSource("", "bool", body, argValues); 
+
+    webGPUPromiseTest(async () => {
+        return callBoolFunction(src, name, values).then(result => {
+            assert_equals(result, expected, "Test returned expected value.");
+        }, e => {
+            if (!(e instanceof WebGPUUnsupportedError))
+                throw e;
+        });
+    }, msg);
+};
+
+const checkFloat4s = (body, argValues = [], expected = [0, 1, 2, 3]) => {
+    let inputArgs = [];
+    let values = [];
+    for (let i = 0; i < argValues.length; ++i) {
+        // Support arrays of float4, including one with a single float4.
+        const totalLength = argValues[i].flat().length;
+        const isBuffer = argValues[i].length === 1 || totalLength > 4;
+        inputArgs.push(`${isBuffer ? "device " : ""}float4${isBuffer ? "[]" : ""} in${i}`);
+        values.push(makeFloat4(argValues[i]));
+    }
+
+    const src = `float4 float4Test(${inputArgs.join(", ")}) { ${body} }
+    `;
+
+    webGPUPromiseTest(async () => {
+        return callFloat4Function(src, "float4Test", values).then(result => {
+            for (let i = 0; i < 4; ++i)
+                assert_approx_equals(result[i], expected[i], epsilon, "Test returned expected value.");
+        });
+    }, "Return an expected float4 value.");
+}
+
+const appendScalarFunctionToSource = (src, type, body, argValues) => {
+    const name = `${type}Test`;
+
+    let inputArgs = [];
+    let values = [];
+    for (let i = 0; i < argValues.length; ++i) {
+        const isBuffer = Array.isArray(argValues[i]);
+        inputArgs.push(`${isBuffer ? "device " : ""}${type}${isBuffer ? "[]" : ""} in${i}`);
+        values.push(scalarArgMakers[type](argValues[i]));
+    }
+
+    src += `${type} ${name}(${inputArgs.join(", ")}) { ${body} }
+    `;
+    
+    return [src, name, values];
+};
+
+const webGPUPromiseTest = (testFunc, msg) => {
+    promise_test(async () => { 
+        return testFunc().catch(e => {
+        if (!(e instanceof WebGPUUnsupportedError))
+            throw e;
+        });
+    }, msg);
+}
+</script>
+</html>
\ No newline at end of file