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