[WHLSL] Rewrite all tests to use WHLSL and delete the isWHLSL flag
[WebKit-https.git] / LayoutTests / webgpu / whlsl / js / test-harness.js
1 /* Type Utilties */
2
3 // FIXME: Support all WHLSL scalar and vector types.
4 // FIXME: Support textures and samplers.
5 const Types = Object.freeze({
6     BOOL: Symbol("bool"),
7     INT: Symbol("int"),
8     UCHAR: Symbol("uchar"),
9     UINT: Symbol("uint"),
10     FLOAT: Symbol("float"),
11     FLOAT4: Symbol("float4"),
12     FLOAT4X4: Symbol("float4x4"),
13     MAX_SIZE: 64 // This needs to be big enough to hold any singular WHLSL type.
14 });
15
16 function isScalar(type)
17 {
18     switch(type) {
19         case Types.FLOAT4:
20         case Types.FLOAT4X4:
21             return false;
22         default:
23             return true;
24     }
25 }
26
27 function convertTypeToArrayType(type)
28 {
29     switch(type) {
30         case Types.BOOL:
31             return Int32Array;
32         case Types.INT:
33             return Int32Array;
34         case Types.UCHAR:
35             return Uint8Array;
36         case Types.UINT:
37             return Uint32Array;
38         case Types.FLOAT:
39         case Types.FLOAT4:
40         case Types.FLOAT4X4:
41             return Float32Array;
42         default:
43             throw new Error("Invalid TYPE provided!");
44     }
45 }
46
47 function convertTypeToWHLSLType(type)
48 {
49     switch(type) {
50         case Types.BOOL:
51             return "bool";
52         case Types.INT:
53             return "int";
54         case Types.UCHAR:
55             return "uchar";
56         case Types.UINT:
57             return "uint";
58         case Types.FLOAT:
59             return "float";
60         case Types.FLOAT4:
61             return "float4";
62         case Types.FLOAT4X4:
63             return "float4x4";
64         default:
65             throw new Error("Invalid TYPE provided!");
66     }
67 }
68
69 function whlslArgumentType(type)
70 {
71     if (type === Types.BOOL)
72         return "int";
73     return convertTypeToWHLSLType(type);
74 }
75
76 function convertToWHLSLOutputType(code, type)
77 {
78     if (type !== Types.BOOL)
79         return code;
80     return `int(${code})`;
81 }
82
83 function convertToWHLSLInputType(code, type)
84 {
85     if (type !== Types.BOOL)
86         return code;
87     return `bool(${code})`;
88 }
89
90 /* Harness Classes */
91
92 class WebGPUUnsupportedError extends Error {
93     constructor()
94     {
95         super("No GPUDevice detected!");
96     }
97 };
98
99 class Data {
100     /**
101      * Upload typed data to and return a wrapper of a GPUBuffer.
102      * @param {Types} type - The WHLSL type to be stored in this Data.
103      * @param {Number or Array[Number]} values - The raw data to be uploaded.
104      */
105     constructor(harness, type, values, isBuffer = false)
106     {
107         if (harness.device === undefined)
108             return;
109         // One or more scalars in an array can be accessed through an array reference.
110         // However, vector types are also created via an array of scalars.
111         // This ensures that buffers of just one vector are usable in a test function.
112         if (Array.isArray(values))
113             this._isBuffer = isScalar(type) ? true : isBuffer;
114         else {
115             this._isBuffer = false;
116             values = [values];
117         }
118
119         this._type = type;
120         this._byteLength = (convertTypeToArrayType(type)).BYTES_PER_ELEMENT * values.length;
121
122         const [buffer, arrayBuffer] = harness.device.createBufferMapped({
123             size: this._byteLength,
124             usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ
125         });
126
127         const typedArray = new (convertTypeToArrayType(type))(arrayBuffer);
128         typedArray.set(values);
129         buffer.unmap();
130
131         this._buffer = buffer;
132     }
133
134     /**
135      * @returns An ArrayBuffer containing the contents of this Data.
136      */
137     async getArrayBuffer()
138     {
139         if (harness.device === undefined)
140             throw new WebGPUUnsupportedError();
141
142         let result;
143         try {
144             result = await this._buffer.mapReadAsync();
145             this._buffer.unmap();
146         } catch {
147             throw new Error("Data error: Unable to get ArrayBuffer!");
148         }
149         return result;
150     }
151
152     get type() { return this._type; }
153     get isBuffer() { return this._isBuffer; }
154     get buffer() { return this._buffer; }
155     get byteLength() { return this._byteLength; }
156 }
157
158 class Harness {
159     constructor ()
160     {
161     }
162
163     async requestDevice()
164     {
165         try {
166             const adapter = await navigator.gpu.requestAdapter();
167             this._device = await adapter.requestDevice();
168         } catch {
169             // WebGPU is not supported.
170             // FIXME: Add support for GPUAdapterRequestOptions and GPUDeviceDescriptor,
171             // and differentiate between descriptor validation errors and no WebGPU support.
172         }
173     }
174
175     /**
176      * Return the return value of a WHLSL function.
177      * @param {Types} type - The return type of the WHLSL function.
178      * @param {String} functions - Custom WHLSL code to be tested.
179      * @param {String} name - The name of the WHLSL function which must be present in 'functions'.
180      * @param {Data or Array[Data]} args - Data arguments to be passed to the call of 'name'.
181      * @returns {TypedArray} - A typed array containing the return value of the function call.
182      */
183     async callTypedFunction(type, functions, name, args)
184     {   
185         if (this._device === undefined)
186             throw new WebGPUUnsupportedError();
187
188         const [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs] = this._setUpArguments(args);
189
190         if (this._resultBuffer) {
191             this._clearResults()
192         } else {
193             this._resultBuffer = this.device.createBuffer({ 
194                 size: Types.MAX_SIZE, 
195                 usage: GPUBufferUsage.STORAGE | GPUBufferUsage.MAP_READ | GPUBufferUsage.TRANSFER_DST
196             });
197         }
198
199         argsLayouts.unshift({
200             binding: 0,
201             visibility: GPUShaderStageBit.COMPUTE,
202             type: "storage-buffer"
203         });
204         argsResourceBindings.unshift({
205             binding: 0,
206             resource: {
207                 buffer: this._resultBuffer,
208                 size: Types.MAX_SIZE
209             }
210         });
211
212         let entryPointCode;
213         argsDeclarations.unshift(`device ${whlslArgumentType(type)}[] result : register(u0)`);
214         let callCode = `${name}(${functionCallArgs.join(", ")})`;
215         callCode = convertToWHLSLOutputType(callCode, type);
216         entryPointCode = `
217 [numthreads(1, 1, 1)]
218 compute void _compute_main(${argsDeclarations.join(", ")})
219 {
220     result[0] = ${callCode};
221 }
222 `;
223         const code = functions + entryPointCode;
224         await this._callFunction(code, argsLayouts, argsResourceBindings);
225     
226         try {
227             var result = await this._resultBuffer.mapReadAsync();
228         } catch {
229             throw new Error("Harness error: Unable to read results!");
230         }
231         const array = new (convertTypeToArrayType(type))(result);
232         this._resultBuffer.unmap();
233
234         return array;
235     }
236
237     /**
238      * Call a WHLSL function to modify the value of argument(buffer)s.
239      * @param {String} functions - Custom WHLSL code to be tested.
240      * @param {String} name - The name of the WHLSL function which must be present in 'functions'.
241      * @param {Data or Array[Data]} args - Data arguments to be passed to the call of 'name'.
242      */
243     callVoidFunction(functions, name, args)
244     {
245         if (this._device === undefined)
246             return;
247
248         const [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs] = this._setUpArguments(args);
249
250         let entryPointCode = `
251 [numthreads(1, 1, 1)]
252 compute void _compute_main(${argsDeclarations.join(", ")})
253 {
254     ${name}(${functionCallArgs.join(", ")});
255 }`;
256         const code = functions + entryPointCode;
257         this._callFunction(code, argsLayouts, argsResourceBindings);
258     }
259
260     /**
261      * Assert that malformed shader code does not compile.
262      * @param {String} source - Custom code to be tested.
263      */
264     async checkCompileFail(source)
265     {
266         if (this._device === undefined)
267             return;
268         
269         let entryPointCode = `
270 [numthreads(1, 1, 1)]
271 compute void _compute_main() { }`;
272
273         const code = source + entryPointCode;
274
275         this._device.pushErrorScope("validation");
276         
277         const shaders = this._device.createShaderModule({ code: code });
278     
279         this._device.createComputePipeline({
280             computeStage: {
281                 module: shaders,
282                 entryPoint: "_compute_main"
283             }
284         });
285
286         const error = await this._device.popErrorScope();
287         if (!error)
288             throw new Error("Compiler error: shader code did not fail to compile!");
289     }
290
291     get device() { return this._device; }
292
293     _clearResults()
294     {
295         if (!this._clearBuffer) {
296             this._clearBuffer = this._device.createBuffer({ 
297                 size: Types.MAX_SIZE, 
298                 usage: GPUBufferUsage.TRANSFER_SRC
299             });
300         }
301         const commandEncoder = this._device.createCommandEncoder();
302         commandEncoder.copyBufferToBuffer(this._clearBuffer, 0, this._resultBuffer, 0, Types.MAX_SIZE);
303         this._device.getQueue().submit([commandEncoder.finish()]);
304     }
305
306     _setUpArguments(args)
307     {
308         if (!Array.isArray(args)) {
309             if (args instanceof Data)
310                 args = [args];
311             else if (!args)
312                 args = [];
313         }
314
315         // Expand bind group structure to represent any arguments.
316         let argsDeclarations = [];
317         let functionCallArgs = [];
318         let argsLayouts = [];
319         let argsResourceBindings = [];
320
321         for (let i = 1; i <= args.length; ++i) {
322             const arg = args[i - 1];
323             argsDeclarations.push(`device ${whlslArgumentType(arg.type)}[] arg${i} : register(u${i})`);
324             functionCallArgs.push(convertToWHLSLInputType(`arg${i}` + (arg.isBuffer ? "" : "[0]"), arg.type));
325             argsLayouts.push({
326                 binding: i,
327                 visibility: GPUShaderStageBit.COMPUTE,
328                 type: "storage-buffer"
329             });
330             argsResourceBindings.push({
331                 binding: i,
332                 resource: {
333                     buffer: arg.buffer,
334                     size: arg.byteLength
335                 }
336             });
337         }
338
339         return [argsLayouts, argsResourceBindings, argsDeclarations, functionCallArgs];
340     }
341
342     async _callFunction(code, argsLayouts, argsResourceBindings)
343     {
344         const bindGroupLayout = this._device.createBindGroupLayout({
345             bindings: argsLayouts
346         });
347
348         const pipelineLayout = this._device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
349
350         const bindGroup = this._device.createBindGroup({
351             layout: bindGroupLayout,
352             bindings: argsResourceBindings
353         });
354
355         this._device.pushErrorScope("validation");
356
357         const shaders = this._device.createShaderModule({ code: code });
358     
359         const pipeline = this._device.createComputePipeline({
360             layout: pipelineLayout,
361             computeStage: {
362                 module: shaders,
363                 entryPoint: "_compute_main"
364             }
365         });
366
367         const commandEncoder = this._device.createCommandEncoder();
368         const passEncoder = commandEncoder.beginComputePass();
369         passEncoder.setBindGroup(0, bindGroup);
370         passEncoder.setPipeline(pipeline);
371         passEncoder.dispatch(1, 1, 1);
372         passEncoder.endPass();
373         
374         this._device.getQueue().submit([commandEncoder.finish()]);
375
376         const error = await this._device.popErrorScope();
377         if (error)
378             throw new Error(error.message);
379     }
380 }
381
382 /* Harness Setup */
383
384 const harness = new Harness();
385 harness.requestDevice();
386
387 /* Global Helper Functions */
388
389 /**
390  * The make___ functions are wrappers around the Data constructor.
391  * Values passed in as an array will be passed in via a device-addressed pointer type in the shader.
392  * @param {Boolean, Number, or Array} values - The data to be stored on the GPU.
393  * @returns A new Data object with storage allocated to store values.
394  */
395 function makeBool(values)
396 {
397     return new Data(harness, Types.BOOL, values);
398 }
399
400 function makeInt(values)
401 {
402     return new Data(harness, Types.INT, values);
403 }
404
405 function makeUchar(values)
406 {
407     return new Data(harness, Types.UCHAR, values);
408 }
409
410 function makeUint(values)
411 {
412     return new Data(harness, Types.UINT, values);
413 }
414
415 function makeFloat(values)
416 {
417     return new Data(harness, Types.FLOAT, values);
418 }
419
420 /**
421  * @param {Array or Array[Array]} values - 1D or 2D array of float values.
422  * The total number of float values must be divisible by 4.
423  * A single 4-element array of floats will be treated as a single float4 argument in the shader.
424  */
425 function makeFloat4(values)
426 {
427     const results = processArrays(values, 4);
428     return new Data(harness, Types.FLOAT4, results.values, results.isBuffer);
429 }
430
431 /**
432  * @param {Array or Array[Array]} values - 1D or 2D array of float values.
433  * The total number of float values must be divisible by 16.
434  * A single 16-element array of floats will be treated as a single float4x4 argument in the shader.
435  * This should follow the glMatrix/OpenGL method of storing 4x4 matrices,
436  * where the x, y, z translation components are the 13th, 14th, and 15th elements respectively.
437  */
438 function makeFloat4x4(values)
439 {
440     const results = processArrays(values, 16);
441     return new Data(harness, Types.FLOAT4X4, results.values, results.isBuffer);
442 }
443
444 function processArrays(values, minimumLength)
445 {
446     const originalLength = values.length;
447     // This works because float4 is tightly packed.
448     // When implementing other vector types, add padding if needed.
449     values = values.flat();
450     if (values.length % minimumLength != 0)
451         throw new Error("Invalid number of elements in non-scalar type!");
452     
453     return { values: values, isBuffer: originalLength === 1 || values.length > minimumLength };
454 }
455
456 /**
457  * @param {String} functions - Shader source code that must contain a definition for 'name'.
458  * @param {String} name - The function to be called from 'functions'.
459  * @param {Data or Array[Data]} args - The arguments to be passed to the call of 'name'.
460  * @returns A Promise that resolves to the return value of a call to 'name' with 'args'.
461  */
462 async function callBoolFunction(functions, name, args)
463 {
464     return !!(await harness.callTypedFunction(Types.BOOL, functions, name, args))[0];
465 }
466
467 async function callIntFunction(functions, name, args)
468 {
469     return (await harness.callTypedFunction(Types.INT, functions, name, args))[0];
470 }
471
472 async function callUcharFunction(functions, name, args)
473 {
474     return (await harness.callTypedFunction(Types.UCHAR, functions, name, args))[0];
475 }
476
477 async function callUintFunction(functions, name, args)
478 {
479     return (await harness.callTypedFunction(Types.UINT, functions, name, args))[0];
480 }
481
482 async function callFloatFunction(functions, name, args)
483 {
484     return (await harness.callTypedFunction(Types.FLOAT, functions, name, args))[0];
485 }
486
487 async function callFloat4Function(functions, name, args)
488 {
489     return (await harness.callTypedFunction(Types.FLOAT4, functions, name, args)).subarray(0, 4);
490 }
491
492 async function callFloat4x4Function(functions, name, args)
493 {
494     return (await harness.callTypedFunction(Types.FLOAT4X4, functions, name, args)).subarray(0, 16);
495 }
496
497 async function checkFail(source) 
498 {
499     return (await harness.checkCompileFail(source));
500 }
501
502 /**
503  * Does not return a Promise. To observe the results of a call, 
504  * call 'getArrayBuffer' on the Data object retaining your output buffer.
505  */
506 function callVoidFunction(functions, name, args)
507 {
508     harness.callVoidFunction(functions, name, args);
509 }
510
511 const webGPUPromiseTest = (testFunc, msg) => {
512     promise_test(async () => { 
513         return testFunc().catch(e => {
514         if (!(e instanceof WebGPUUnsupportedError))
515             throw e;
516         });
517     }, msg);
518 }
519
520 function runTests(obj) {
521     window.addEventListener("load", async () => {
522         try {
523             for (const name in obj) {
524                 if (!name.startsWith("_")) 
525                     await webGPUPromiseTest(obj[name], name);
526             }
527         } catch (e) {
528             if (window.testRunner)
529                 testRunner.notifyDone();
530             
531             throw e;
532         }
533     });
534 }
535