summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common/util
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/common/util')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts58
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/colors.ts127
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts129
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts86
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts149
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts7
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/types.ts97
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/util.ts476
-rw-r--r--dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts24
9 files changed, 1153 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts b/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts
new file mode 100644
index 0000000000..670028d41c
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/collect_garbage.ts
@@ -0,0 +1,58 @@
+import { resolveOnTimeout } from './util.js';
+
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+declare const Components: any;
+
+/**
+ * Attempts to trigger JavaScript garbage collection, either using explicit methods if exposed
+ * (may be available in testing environments with special browser runtime flags set), or using
+ * some weird tricks to incur GC pressure. Adopted from the WebGL CTS.
+ */
+export async function attemptGarbageCollection(): Promise<void> {
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ const w: any = globalThis;
+ if (w.GCController) {
+ w.GCController.collect();
+ return;
+ }
+
+ if (w.opera && w.opera.collect) {
+ w.opera.collect();
+ return;
+ }
+
+ try {
+ w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .garbageCollect();
+ return;
+ } catch (e) {
+ // ignore any failure
+ }
+
+ if (w.gc) {
+ w.gc();
+ return;
+ }
+
+ if (w.CollectGarbage) {
+ w.CollectGarbage();
+ return;
+ }
+
+ let i: number;
+ function gcRec(n: number): void {
+ if (n < 1) return;
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
+ let temp: object | string = { i: 'ab' + i + i / 100000 };
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
+ temp = temp + 'foo';
+ temp; // dummy use of unused variable
+ gcRec(n - 1);
+ }
+ for (i = 0; i < 1000; i++) {
+ gcRec(10);
+ }
+
+ return resolveOnTimeout(35); // Let the event loop run a few frames in case it helps.
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts b/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts
new file mode 100644
index 0000000000..709d159320
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/colors.ts
@@ -0,0 +1,127 @@
+/**
+ * The interface used for formatting strings to contain color metadata.
+ *
+ * Use the interface properties to construct a style, then use the
+ * `(s: string): string` function to format the provided string with the given
+ * style.
+ */
+export interface Colors {
+ // Are colors enabled?
+ enabled: boolean;
+
+ // Returns the string formatted to contain the specified color or style.
+ (s: string): string;
+
+ // modifiers
+ reset: Colors;
+ bold: Colors;
+ dim: Colors;
+ italic: Colors;
+ underline: Colors;
+ inverse: Colors;
+ hidden: Colors;
+ strikethrough: Colors;
+
+ // colors
+ black: Colors;
+ red: Colors;
+ green: Colors;
+ yellow: Colors;
+ blue: Colors;
+ magenta: Colors;
+ cyan: Colors;
+ white: Colors;
+ gray: Colors;
+ grey: Colors;
+
+ // bright colors
+ blackBright: Colors;
+ redBright: Colors;
+ greenBright: Colors;
+ yellowBright: Colors;
+ blueBright: Colors;
+ magentaBright: Colors;
+ cyanBright: Colors;
+ whiteBright: Colors;
+
+ // background colors
+ bgBlack: Colors;
+ bgRed: Colors;
+ bgGreen: Colors;
+ bgYellow: Colors;
+ bgBlue: Colors;
+ bgMagenta: Colors;
+ bgCyan: Colors;
+ bgWhite: Colors;
+
+ // bright background colors
+ bgBlackBright: Colors;
+ bgRedBright: Colors;
+ bgGreenBright: Colors;
+ bgYellowBright: Colors;
+ bgBlueBright: Colors;
+ bgMagentaBright: Colors;
+ bgCyanBright: Colors;
+ bgWhiteBright: Colors;
+}
+
+/**
+ * The interface used for formatting strings with color metadata.
+ *
+ * Currently Colors will use the 'ansi-colors' module if it can be loaded.
+ * If it cannot be loaded, then the Colors implementation is a straight pass-through.
+ *
+ * Colors may also be a no-op if the current environment does not support colors.
+ */
+export let Colors: Colors;
+
+try {
+ /* eslint-disable-next-line node/no-unpublished-require */
+ Colors = require('ansi-colors') as Colors;
+} catch {
+ const passthrough = ((s: string) => s) as Colors;
+ passthrough.enabled = false;
+ passthrough.reset = passthrough;
+ passthrough.bold = passthrough;
+ passthrough.dim = passthrough;
+ passthrough.italic = passthrough;
+ passthrough.underline = passthrough;
+ passthrough.inverse = passthrough;
+ passthrough.hidden = passthrough;
+ passthrough.strikethrough = passthrough;
+ passthrough.black = passthrough;
+ passthrough.red = passthrough;
+ passthrough.green = passthrough;
+ passthrough.yellow = passthrough;
+ passthrough.blue = passthrough;
+ passthrough.magenta = passthrough;
+ passthrough.cyan = passthrough;
+ passthrough.white = passthrough;
+ passthrough.gray = passthrough;
+ passthrough.grey = passthrough;
+ passthrough.blackBright = passthrough;
+ passthrough.redBright = passthrough;
+ passthrough.greenBright = passthrough;
+ passthrough.yellowBright = passthrough;
+ passthrough.blueBright = passthrough;
+ passthrough.magentaBright = passthrough;
+ passthrough.cyanBright = passthrough;
+ passthrough.whiteBright = passthrough;
+ passthrough.bgBlack = passthrough;
+ passthrough.bgRed = passthrough;
+ passthrough.bgGreen = passthrough;
+ passthrough.bgYellow = passthrough;
+ passthrough.bgBlue = passthrough;
+ passthrough.bgMagenta = passthrough;
+ passthrough.bgCyan = passthrough;
+ passthrough.bgWhite = passthrough;
+ passthrough.bgBlackBright = passthrough;
+ passthrough.bgRedBright = passthrough;
+ passthrough.bgGreenBright = passthrough;
+ passthrough.bgYellowBright = passthrough;
+ passthrough.bgBlueBright = passthrough;
+ passthrough.bgMagentaBright = passthrough;
+ passthrough.bgCyanBright = passthrough;
+ passthrough.bgWhiteBright = passthrough;
+ Colors = passthrough;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts b/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts
new file mode 100644
index 0000000000..dc57328ab2
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/data_tables.ts
@@ -0,0 +1,129 @@
+import { ResolveType, ZipKeysWithValues } from './types.js';
+
+export type valueof<K> = K[keyof K];
+
+export function keysOf<T extends string>(obj: { [k in T]: unknown }): readonly T[] {
+ return Object.keys(obj) as unknown[] as T[];
+}
+
+export function numericKeysOf<T>(obj: object): readonly T[] {
+ return Object.keys(obj).map(n => Number(n)) as unknown[] as T[];
+}
+
+/**
+ * @returns a new Record from `objects`, using the string returned by Object.toString() as the keys
+ * and the objects as the values.
+ */
+export function objectsToRecord<T extends Object>(objects: readonly T[]): Record<string, T> {
+ const record = {};
+ return objects.reduce((obj, type) => {
+ return {
+ ...obj,
+ [type.toString()]: type,
+ };
+ }, record);
+}
+
+/**
+ * Creates an info lookup object from a more nicely-formatted table. See below for examples.
+ *
+ * Note: Using `as const` on the arguments to this function is necessary to infer the correct type.
+ */
+export function makeTable<
+ Members extends readonly string[],
+ Defaults extends readonly unknown[],
+ Table extends { readonly [k: string]: readonly unknown[] },
+>(
+ members: Members,
+ defaults: Defaults,
+ table: Table
+): {
+ readonly [k in keyof Table]: ResolveType<ZipKeysWithValues<Members, Table[k], Defaults>>;
+} {
+ const result: { [k: string]: { [m: string]: unknown } } = {};
+ for (const [k, v] of Object.entries<readonly unknown[]>(table)) {
+ const item: { [m: string]: unknown } = {};
+ for (let i = 0; i < members.length; ++i) {
+ item[members[i]] = v[i] ?? defaults[i];
+ }
+ result[k] = item;
+ }
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ return result as any;
+}
+
+/**
+ * Creates an info lookup object from a more nicely-formatted table.
+ *
+ * Note: Using `as const` on the arguments to this function is necessary to infer the correct type.
+ *
+ * Example:
+ *
+ * ```
+ * const t = makeTableWithDefaults(
+ * { c: 'default' }, // columnRenames
+ * ['a', 'default', 'd'], // columnsKept
+ * ['a', 'b', 'c', 'd'], // columns
+ * [123, 456, 789, 1011], // defaults
+ * { // table
+ * foo: [1, 2, 3, 4],
+ * bar: [5, , , 8],
+ * moo: [ , 9,10, ],
+ * }
+ * );
+ *
+ * // t = {
+ * // foo: { a: 1, default: 3, d: 4 },
+ * // bar: { a: 5, default: 789, d: 8 },
+ * // moo: { a: 123, default: 10, d: 1011 },
+ * // };
+ * ```
+ *
+ * MAINTENANCE_TODO: `ZipKeysWithValues<Members, Table[k], Defaults>` is incorrect
+ * because Members no longer maps to Table[k]. It's not clear if this is even possible to fix
+ * because it requires mapping, not zipping. Maybe passing in a index mapping
+ * would fix it (which is gross) but if you have columnsKept as [0, 2, 3] then maybe it would
+ * be possible to generate the correct type? I don't think we can generate the map at compile time
+ * so we'd have to hand code it. Other ideas, don't generate kLimitsInfoCore and kLimitsInfoCompat
+ * where they are keys of infos. Instead, generate kLimitsInfoCoreDefaults, kLimitsInfoCoreMaximums,
+ * kLimitsInfoCoreClasses where each is just a `{[k: string]: type}`. Could zip those after or,
+ * maybe that suggests passing in the hard coded indices would work.
+ *
+ * @param columnRenames the name of the column in the table that will be assigned to the 'default' property of each entry.
+ * @param columnsKept the names of properties you want in the generated lookup table. This must be a subset of the columns of the tables except for the name 'default' which is looked from the previous argument.
+ * @param columns the names of the columns of the name
+ * @param defaults the default value by column for any element in a row of the table that is undefined
+ * @param table named table rows.
+ */
+export function makeTableRenameAndFilter<
+ Members extends readonly string[],
+ DataMembers extends readonly string[],
+ Defaults extends readonly unknown[],
+ Table extends { readonly [k: string]: readonly unknown[] },
+>(
+ columnRenames: { [key: string]: string },
+ columnsKept: Members,
+ columns: DataMembers,
+ defaults: Defaults,
+ table: Table
+): {
+ readonly [k in keyof Table]: ResolveType<ZipKeysWithValues<Members, Table[k], Defaults>>;
+} {
+ const result: { [k: string]: { [m: string]: unknown } } = {};
+ const keyToIndex = new Map<string, number>(
+ columnsKept.map(name => {
+ const remappedName = columnRenames[name] === undefined ? name : columnRenames[name];
+ return [name, columns.indexOf(remappedName)];
+ })
+ );
+ for (const [k, v] of Object.entries<readonly unknown[]>(table)) {
+ const item: { [m: string]: unknown } = {};
+ for (const member of columnsKept) {
+ const ndx = keyToIndex.get(member)!;
+ item[member] = v[ndx] ?? defaults[ndx];
+ }
+ result[k] = item;
+ }
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ return result as any;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts b/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts
new file mode 100644
index 0000000000..4110a0edb5
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/navigator_gpu.ts
@@ -0,0 +1,86 @@
+/// <reference types="@webgpu/types" />
+
+import { TestCaseRecorder } from '../framework/fixture.js';
+
+import { ErrorWithExtra, assert, objectEquals } from './util.js';
+
+/**
+ * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
+ * Throws an exception if not found.
+ */
+function defaultGPUProvider(): GPU {
+ assert(
+ typeof navigator !== 'undefined' && navigator.gpu !== undefined,
+ 'No WebGPU implementation found'
+ );
+ return navigator.gpu;
+}
+
+/**
+ * GPUProvider is a function that creates and returns a new GPU instance.
+ * May throw an exception if a GPU cannot be created.
+ */
+export type GPUProvider = () => GPU;
+
+let gpuProvider: GPUProvider = defaultGPUProvider;
+
+/**
+ * Sets the function to create and return a new GPU instance.
+ */
+export function setGPUProvider(provider: GPUProvider) {
+ assert(impl === undefined, 'setGPUProvider() should not be after getGPU()');
+ gpuProvider = provider;
+}
+
+let impl: GPU | undefined = undefined;
+
+let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined;
+
+export function setDefaultRequestAdapterOptions(options: GPURequestAdapterOptions) {
+ // It's okay to call this if you don't change the options
+ if (objectEquals(options, defaultRequestAdapterOptions)) {
+ return;
+ }
+ if (impl) {
+ throw new Error('must call setDefaultRequestAdapterOptions before getGPU');
+ }
+ defaultRequestAdapterOptions = { ...options };
+}
+
+export function getDefaultRequestAdapterOptions() {
+ return defaultRequestAdapterOptions;
+}
+
+/**
+ * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
+ * Throws an exception if not found.
+ */
+export function getGPU(recorder: TestCaseRecorder | null): GPU {
+ if (impl) {
+ return impl;
+ }
+
+ impl = gpuProvider();
+
+ if (defaultRequestAdapterOptions) {
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ const oldFn = impl.requestAdapter;
+ impl.requestAdapter = function (
+ options?: GPURequestAdapterOptions
+ ): Promise<GPUAdapter | null> {
+ const promise = oldFn.call(this, { ...defaultRequestAdapterOptions, ...options });
+ if (recorder) {
+ void promise.then(async adapter => {
+ if (adapter) {
+ const info = await adapter.requestAdapterInfo();
+ const infoString = `Adapter: ${info.vendor} / ${info.architecture} / ${info.device}`;
+ recorder.debug(new ErrorWithExtra(infoString, () => ({ adapterInfo: info })));
+ }
+ });
+ }
+ return promise;
+ };
+ }
+
+ return impl;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts b/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts
new file mode 100644
index 0000000000..6a26b290bc
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/preprocessor.ts
@@ -0,0 +1,149 @@
+import { assert } from './util.js';
+
+// The state of the preprocessor is a stack of States.
+type StateStack = { allowsFollowingElse: boolean; state: State }[];
+const enum State {
+ Seeking, // Still looking for a passing condition
+ Passing, // Currently inside a passing condition (the root is always in this state)
+ Skipping, // Have already seen a passing condition; now skipping the rest
+}
+
+// The transitions in the state space are the following preprocessor directives:
+// - Sibling elif
+// - Sibling else
+// - Sibling endif
+// - Child if
+abstract class Directive {
+ private readonly depth: number;
+
+ constructor(depth: number) {
+ this.depth = depth;
+ }
+
+ protected checkDepth(stack: StateStack): void {
+ assert(
+ stack.length === this.depth,
+ `Number of "$"s must match nesting depth, currently ${stack.length} (e.g. $if $$if $$endif $endif)`
+ );
+ }
+
+ abstract applyTo(stack: StateStack): void;
+}
+
+class If extends Directive {
+ private readonly predicate: boolean;
+
+ constructor(depth: number, predicate: boolean) {
+ super(depth);
+ this.predicate = predicate;
+ }
+
+ applyTo(stack: StateStack) {
+ this.checkDepth(stack);
+ const parentState = stack[stack.length - 1].state;
+ stack.push({
+ allowsFollowingElse: true,
+ state:
+ parentState !== State.Passing
+ ? State.Skipping
+ : this.predicate
+ ? State.Passing
+ : State.Seeking,
+ });
+ }
+}
+
+class ElseIf extends If {
+ override applyTo(stack: StateStack) {
+ assert(stack.length >= 1);
+ const { allowsFollowingElse, state: siblingState } = stack.pop()!;
+ this.checkDepth(stack);
+ assert(allowsFollowingElse, 'pp.elif after pp.else');
+ if (siblingState !== State.Seeking) {
+ stack.push({ allowsFollowingElse: true, state: State.Skipping });
+ } else {
+ super.applyTo(stack);
+ }
+ }
+}
+
+class Else extends Directive {
+ applyTo(stack: StateStack) {
+ assert(stack.length >= 1);
+ const { allowsFollowingElse, state: siblingState } = stack.pop()!;
+ this.checkDepth(stack);
+ assert(allowsFollowingElse, 'pp.else after pp.else');
+ stack.push({
+ allowsFollowingElse: false,
+ state: siblingState === State.Seeking ? State.Passing : State.Skipping,
+ });
+ }
+}
+
+class EndIf extends Directive {
+ applyTo(stack: StateStack) {
+ stack.pop();
+ this.checkDepth(stack);
+ }
+}
+
+/**
+ * A simple template-based, non-line-based preprocessor implementing if/elif/else/endif.
+ *
+ * @example
+ * ```
+ * const shader = pp`
+ * ${pp._if(expr)}
+ * const x: ${type} = ${value};
+ * ${pp._elif(expr)}
+ * ${pp.__if(expr)}
+ * ...
+ * ${pp.__else}
+ * ...
+ * ${pp.__endif}
+ * ${pp._endif}`;
+ * ```
+ *
+ * @param strings - The array of constant string chunks of the template string.
+ * @param ...values - The array of interpolated `${}` values within the template string.
+ */
+export function pp(
+ strings: TemplateStringsArray,
+ ...values: ReadonlyArray<Directive | string | number>
+): string {
+ let result = '';
+ const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }];
+
+ for (let i = 0; i < values.length; ++i) {
+ const passing = stateStack[stateStack.length - 1].state === State.Passing;
+ if (passing) {
+ result += strings[i];
+ }
+
+ const value = values[i];
+ if (value instanceof Directive) {
+ value.applyTo(stateStack);
+ } else {
+ if (passing) {
+ result += value;
+ }
+ }
+ }
+ assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file');
+ result += strings[values.length];
+
+ return result;
+}
+pp._if = (predicate: boolean) => new If(1, predicate);
+pp._elif = (predicate: boolean) => new ElseIf(1, predicate);
+pp._else = new Else(1);
+pp._endif = new EndIf(1);
+pp.__if = (predicate: boolean) => new If(2, predicate);
+pp.__elif = (predicate: boolean) => new ElseIf(2, predicate);
+pp.__else = new Else(2);
+pp.__endif = new EndIf(2);
+pp.___if = (predicate: boolean) => new If(3, predicate);
+pp.___elif = (predicate: boolean) => new ElseIf(3, predicate);
+pp.___else = new Else(3);
+pp.___endif = new EndIf(3);
+// Add more if needed.
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts b/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts
new file mode 100644
index 0000000000..13c3b7fb90
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/timeout.ts
@@ -0,0 +1,7 @@
+/** Defined by WPT. Like `setTimeout`, but applies a timeout multiplier for slow test systems. */
+declare const step_timeout: undefined | typeof setTimeout;
+
+/**
+ * Equivalent of `setTimeout`, but redirects to WPT's `step_timeout` when it is defined.
+ */
+export const timeout = typeof step_timeout !== 'undefined' ? step_timeout : setTimeout;
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/types.ts b/dom/webgpu/tests/cts/checkout/src/common/util/types.ts
new file mode 100644
index 0000000000..746095a23e
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/types.ts
@@ -0,0 +1,97 @@
+/** Forces a type to resolve its type definitions, to make it readable/debuggable. */
+export type ResolveType<T> = T extends object
+ ? T extends infer O
+ ? { [K in keyof O]: ResolveType<O[K]> }
+ : never
+ : T;
+
+/** Returns the type `true` iff X and Y are exactly equal */
+export type TypeEqual<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
+ ? true
+ : false;
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+export function assertTypeTrue<_ extends true>() {}
+
+/** `ReadonlyArray` of `ReadonlyArray`s. */
+export type ROArrayArray<T> = ReadonlyArray<ReadonlyArray<T>>;
+/** `ReadonlyArray` of `ReadonlyArray`s of `ReadonlyArray`s. */
+export type ROArrayArrayArray<T> = ReadonlyArray<ReadonlyArray<ReadonlyArray<T>>>;
+
+/**
+ * Deep version of the Readonly<> type, with support for tuples (up to length 7).
+ * <https://gist.github.com/masterkidan/7322752f569b1bba53e0426266768623>
+ */
+export type DeepReadonly<T> = T extends [infer A]
+ ? DeepReadonlyObject<[A]>
+ : T extends [infer A, infer B]
+ ? DeepReadonlyObject<[A, B]>
+ : T extends [infer A, infer B, infer C]
+ ? DeepReadonlyObject<[A, B, C]>
+ : T extends [infer A, infer B, infer C, infer D]
+ ? DeepReadonlyObject<[A, B, C, D]>
+ : T extends [infer A, infer B, infer C, infer D, infer E]
+ ? DeepReadonlyObject<[A, B, C, D, E]>
+ : T extends [infer A, infer B, infer C, infer D, infer E, infer F]
+ ? DeepReadonlyObject<[A, B, C, D, E, F]>
+ : T extends [infer A, infer B, infer C, infer D, infer E, infer F, infer G]
+ ? DeepReadonlyObject<[A, B, C, D, E, F, G]>
+ : T extends Map<infer U, infer V>
+ ? ReadonlyMap<DeepReadonlyObject<U>, DeepReadonlyObject<V>>
+ : T extends Set<infer U>
+ ? ReadonlySet<DeepReadonlyObject<U>>
+ : T extends Promise<infer U>
+ ? Promise<DeepReadonlyObject<U>>
+ : T extends Primitive
+ ? T
+ : T extends (infer A)[]
+ ? DeepReadonlyArray<A>
+ : DeepReadonlyObject<T>;
+
+type Primitive = string | number | boolean | undefined | null | Function | symbol;
+type DeepReadonlyArray<T> = ReadonlyArray<DeepReadonly<T>>;
+type DeepReadonlyObject<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> };
+
+/**
+ * Computes the intersection of a set of types, given the union of those types.
+ *
+ * From: https://stackoverflow.com/a/56375136
+ */
+export type UnionToIntersection<U> =
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
+
+/** "Type asserts" that `X` is a subtype of `Y`. */
+type EnsureSubtype<X, Y> = X extends Y ? X : never;
+
+type TupleHeadOr<T, Default> = T extends readonly [infer H, ...(readonly unknown[])] ? H : Default;
+type TupleTailOr<T, Default> = T extends readonly [unknown, ...infer Tail] ? Tail : Default;
+type TypeOr<T, Default> = T extends undefined ? Default : T;
+
+/**
+ * Zips a key tuple type and a value tuple type together into an object.
+ *
+ * @template Keys Keys of the resulting object.
+ * @template Values Values of the resulting object. If a key corresponds to a `Values` member that
+ * is undefined or past the end, it defaults to the corresponding `Defaults` member.
+ * @template Defaults Default values. If a key corresponds to a `Defaults` member that is past the
+ * end, the default falls back to `undefined`.
+ */
+export type ZipKeysWithValues<
+ Keys extends readonly string[],
+ Values extends readonly unknown[],
+ Defaults extends readonly unknown[],
+> =
+ //
+ Keys extends readonly [infer KHead, ...infer KTail]
+ ? {
+ readonly [k in EnsureSubtype<KHead, string>]: TypeOr<
+ TupleHeadOr<Values, undefined>,
+ TupleHeadOr<Defaults, undefined>
+ >;
+ } & ZipKeysWithValues<
+ EnsureSubtype<KTail, readonly string[]>,
+ TupleTailOr<Values, []>,
+ TupleTailOr<Defaults, []>
+ >
+ : {}; // K exhausted
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/util.ts b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts
new file mode 100644
index 0000000000..9433aaddb0
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/util.ts
@@ -0,0 +1,476 @@
+import { Float16Array } from '../../external/petamoriken/float16/float16.js';
+import { SkipTestCase } from '../framework/fixture.js';
+import { globalTestConfig } from '../framework/test_config.js';
+import { Logger } from '../internal/logging/logger.js';
+
+import { keysOf } from './data_tables.js';
+import { timeout } from './timeout.js';
+
+/**
+ * Error with arbitrary `extra` data attached, for debugging.
+ * The extra data is omitted if not running the test in debug mode (`?debug=1`).
+ */
+export class ErrorWithExtra extends Error {
+ readonly extra: { [k: string]: unknown };
+
+ /**
+ * `extra` function is only called if in debug mode.
+ * If an `ErrorWithExtra` is passed, its message is used and its extras are passed through.
+ */
+ constructor(message: string, extra: () => {});
+ constructor(base: ErrorWithExtra, newExtra: () => {});
+ constructor(baseOrMessage: string | ErrorWithExtra, newExtra: () => {}) {
+ const message = typeof baseOrMessage === 'string' ? baseOrMessage : baseOrMessage.message;
+ super(message);
+
+ const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {};
+ this.extra = Logger.globalDebugMode
+ ? { ...oldExtras, ...newExtra() }
+ : { omitted: 'pass ?debug=1' };
+ }
+}
+
+/**
+ * Asserts `condition` is true. Otherwise, throws an `Error` with the provided message.
+ */
+export function assert(condition: boolean, msg?: string | (() => string)): asserts condition {
+ if (!condition) {
+ throw new Error(msg && (typeof msg === 'string' ? msg : msg()));
+ }
+}
+
+/** If the argument is an Error, throw it. Otherwise, pass it back. */
+export function assertOK<T>(value: Error | T): T {
+ if (value instanceof Error) {
+ throw value;
+ }
+ return value;
+}
+
+/** Options for assertReject, shouldReject, and friends. */
+export type ExceptionCheckOptions = { allowMissingStack?: boolean; message?: string };
+
+/**
+ * Resolves if the provided promise rejects; rejects if it does not.
+ */
+export async function assertReject(
+ expectedName: string,
+ p: Promise<unknown>,
+ { allowMissingStack = false, message }: ExceptionCheckOptions = {}
+): Promise<void> {
+ try {
+ await p;
+ unreachable(message);
+ } catch (ex) {
+ // Asserted as expected
+ if (!allowMissingStack) {
+ const m = message ? ` (${message})` : '';
+ assert(
+ ex instanceof Error && typeof ex.stack === 'string',
+ 'threw as expected, but missing stack' + m
+ );
+ }
+ }
+}
+
+/**
+ * Assert this code is unreachable. Unconditionally throws an `Error`.
+ */
+export function unreachable(msg?: string): never {
+ throw new Error(msg);
+}
+
+/**
+ * Throw a `SkipTestCase` exception, which skips the test case.
+ */
+export function skipTestCase(msg: string): never {
+ throw new SkipTestCase(msg);
+}
+
+/**
+ * The `performance` interface.
+ * It is available in all browsers, but it is not in scope by default in Node.
+ */
+const perf = typeof performance !== 'undefined' ? performance : require('perf_hooks').performance;
+
+/**
+ * Calls the appropriate `performance.now()` depending on whether running in a browser or Node.
+ */
+export function now(): number {
+ return perf.now();
+}
+
+/**
+ * Returns a promise which resolves after the specified time.
+ */
+export function resolveOnTimeout(ms: number): Promise<void> {
+ return new Promise(resolve => {
+ timeout(() => {
+ resolve();
+ }, ms);
+ });
+}
+
+export class PromiseTimeoutError extends Error {}
+
+/**
+ * Returns a promise which rejects after the specified time.
+ */
+export function rejectOnTimeout(ms: number, msg: string): Promise<never> {
+ return new Promise((_resolve, reject) => {
+ timeout(() => {
+ reject(new PromiseTimeoutError(msg));
+ }, ms);
+ });
+}
+
+/**
+ * Takes a promise `p`, and returns a new one which rejects if `p` takes too long,
+ * and otherwise passes the result through.
+ */
+export function raceWithRejectOnTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
+ if (globalTestConfig.noRaceWithRejectOnTimeout) {
+ return p;
+ }
+ // Setup a promise that will reject after `ms` milliseconds. We cancel this timeout when
+ // `p` is finalized, so the JavaScript VM doesn't hang around waiting for the timer to
+ // complete, once the test runner has finished executing the tests.
+ const timeoutPromise = new Promise((_resolve, reject) => {
+ const handle = timeout(() => {
+ reject(new PromiseTimeoutError(msg));
+ }, ms);
+ p = p.finally(() => clearTimeout(handle));
+ });
+ return Promise.race([p, timeoutPromise]) as Promise<T>;
+}
+
+/**
+ * Takes a promise `p` and returns a new one which rejects if `p` resolves or rejects,
+ * and otherwise resolves after the specified time.
+ */
+export function assertNotSettledWithinTime(
+ p: Promise<unknown>,
+ ms: number,
+ msg: string
+): Promise<undefined> {
+ // Rejects regardless of whether p resolves or rejects.
+ const rejectWhenSettled = p.then(() => Promise.reject(new Error(msg)));
+ // Resolves after `ms` milliseconds.
+ const timeoutPromise = new Promise<undefined>(resolve => {
+ const handle = timeout(() => {
+ resolve(undefined);
+ }, ms);
+ void p.finally(() => clearTimeout(handle));
+ });
+ return Promise.race([rejectWhenSettled, timeoutPromise]);
+}
+
+/**
+ * Returns a `Promise.reject()`, but also registers a dummy `.catch()` handler so it doesn't count
+ * as an uncaught promise rejection in the runtime.
+ */
+export function rejectWithoutUncaught<T>(err: unknown): Promise<T> {
+ const p = Promise.reject(err);
+ // Suppress uncaught promise rejection.
+ p.catch(() => {});
+ return p;
+}
+
+/**
+ * Returns true if v is a plain JavaScript object.
+ */
+export function isPlainObject(v: unknown) {
+ return !!v && Object.getPrototypeOf(v).constructor === Object.prototype.constructor;
+}
+
+/**
+ * Makes a copy of a JS `object`, with the keys reordered into sorted order.
+ */
+export function sortObjectByKey(v: { [k: string]: unknown }): { [k: string]: unknown } {
+ const sortedObject: { [k: string]: unknown } = {};
+ for (const k of Object.keys(v).sort()) {
+ sortedObject[k] = v[k];
+ }
+ return sortedObject;
+}
+
+/**
+ * Determines whether two JS values are equal, recursing into objects and arrays.
+ * NaN is treated specially, such that `objectEquals(NaN, NaN)`. +/-0.0 are treated as equal
+ * by default, but can be opted to be distinguished.
+ * @param x the first JS values that get compared
+ * @param y the second JS values that get compared
+ * @param distinguishSignedZero if set to true, treat 0.0 and -0.0 as unequal. Default to false.
+ */
+export function objectEquals(
+ x: unknown,
+ y: unknown,
+ distinguishSignedZero: boolean = false
+): boolean {
+ if (typeof x !== 'object' || typeof y !== 'object') {
+ if (typeof x === 'number' && typeof y === 'number' && Number.isNaN(x) && Number.isNaN(y)) {
+ return true;
+ }
+ // Object.is(0.0, -0.0) is false while (0.0 === -0.0) is true. Other than +/-0.0 and NaN cases,
+ // Object.is works in the same way as ===.
+ return distinguishSignedZero ? Object.is(x, y) : x === y;
+ }
+ if (x === null || y === null) return x === y;
+ if (x.constructor !== y.constructor) return false;
+ if (x instanceof Function) return x === y;
+ if (x instanceof RegExp) return x === y;
+ if (x === y || x.valueOf() === y.valueOf()) return true;
+ if (Array.isArray(x) && Array.isArray(y) && x.length !== y.length) return false;
+ if (x instanceof Date) return false;
+ if (!(x instanceof Object)) return false;
+ if (!(y instanceof Object)) return false;
+
+ const x1 = x as { [k: string]: unknown };
+ const y1 = y as { [k: string]: unknown };
+ const p = Object.keys(x);
+ return Object.keys(y).every(i => p.indexOf(i) !== -1) && p.every(i => objectEquals(x1[i], y1[i]));
+}
+
+/**
+ * Generates a range of values `fn(0)..fn(n-1)`.
+ */
+export function range<T>(n: number, fn: (i: number) => T): T[] {
+ return [...new Array(n)].map((_, i) => fn(i));
+}
+
+/**
+ * Generates a range of values `fn(0)..fn(n-1)`.
+ */
+export function* iterRange<T>(n: number, fn: (i: number) => T): Iterable<T> {
+ for (let i = 0; i < n; ++i) {
+ yield fn(i);
+ }
+}
+
+/** Creates a (reusable) iterable object that maps `f` over `xs`, lazily. */
+export function mapLazy<T, R>(xs: Iterable<T>, f: (x: T) => R): Iterable<R> {
+ return {
+ *[Symbol.iterator]() {
+ for (const x of xs) {
+ yield f(x);
+ }
+ },
+ };
+}
+
+const ReorderOrders = {
+ forward: true,
+ backward: true,
+ shiftByHalf: true,
+};
+export type ReorderOrder = keyof typeof ReorderOrders;
+export const kReorderOrderKeys = keysOf(ReorderOrders);
+
+/**
+ * Creates a new array from the given array with the first half
+ * swapped with the last half.
+ */
+export function shiftByHalf<R>(arr: R[]): R[] {
+ const len = arr.length;
+ const half = (len / 2) | 0;
+ const firstHalf = arr.splice(0, half);
+ return [...arr, ...firstHalf];
+}
+
+/**
+ * Creates a reordered array from the input array based on the Order
+ */
+export function reorder<R>(order: ReorderOrder, arr: R[]): R[] {
+ switch (order) {
+ case 'forward':
+ return arr.slice();
+ case 'backward':
+ return arr.slice().reverse();
+ case 'shiftByHalf': {
+ // should this be pseudo random?
+ return shiftByHalf(arr);
+ }
+ }
+}
+
+const TypedArrayBufferViewInstances = [
+ new Uint8Array(),
+ new Uint8ClampedArray(),
+ new Uint16Array(),
+ new Uint32Array(),
+ new Int8Array(),
+ new Int16Array(),
+ new Int32Array(),
+ new Float16Array(),
+ new Float32Array(),
+ new Float64Array(),
+] as const;
+
+export type TypedArrayBufferView = (typeof TypedArrayBufferViewInstances)[number];
+
+export type TypedArrayBufferViewConstructor<A extends TypedArrayBufferView = TypedArrayBufferView> =
+ {
+ // Interface copied from Uint8Array, and made generic.
+ readonly prototype: A;
+ readonly BYTES_PER_ELEMENT: number;
+
+ new (): A;
+ new (elements: Iterable<number>): A;
+ new (array: ArrayLike<number> | ArrayBufferLike): A;
+ new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): A;
+ new (length: number): A;
+
+ from(arrayLike: ArrayLike<number>): A;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ from(arrayLike: Iterable<number>, mapfn?: (v: number, k: number) => number, thisArg?: any): A;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): A;
+ of(...items: number[]): A;
+ };
+
+export const kTypedArrayBufferViews: {
+ readonly [k: string]: TypedArrayBufferViewConstructor;
+} = {
+ ...(() => {
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ const result: { [k: string]: any } = {};
+ for (const v of TypedArrayBufferViewInstances) {
+ result[v.constructor.name] = v.constructor;
+ }
+ return result;
+ })(),
+};
+export const kTypedArrayBufferViewKeys = keysOf(kTypedArrayBufferViews);
+export const kTypedArrayBufferViewConstructors = Object.values(kTypedArrayBufferViews);
+
+interface TypedArrayMap {
+ Int8Array: Int8Array;
+ Uint8Array: Uint8Array;
+ Int16Array: Int16Array;
+ Uint16Array: Uint16Array;
+ Uint8ClampedArray: Uint8ClampedArray;
+ Int32Array: Int32Array;
+ Uint32Array: Uint32Array;
+ Float32Array: Float32Array;
+ Float64Array: Float64Array;
+ BigInt64Array: BigInt64Array;
+ BigUint64Array: BigUint64Array;
+}
+
+type TypedArrayParam<K extends keyof TypedArrayMap> = {
+ type: K;
+ data: readonly number[];
+};
+
+/**
+ * Creates a case parameter for a typedarray.
+ *
+ * You can't put typedarrays in case parameters directly so instead of
+ *
+ * ```
+ * u.combine('data', [
+ * new Uint8Array([1, 2, 3]),
+ * new Float32Array([4, 5, 6]),
+ * ])
+ * ```
+ *
+ * You can use
+ *
+ * ```
+ * u.combine('data', [
+ * typedArrayParam('Uint8Array' [1, 2, 3]),
+ * typedArrayParam('Float32Array' [4, 5, 6]),
+ * ])
+ * ```
+ *
+ * and then convert the params to typedarrays eg.
+ *
+ * ```
+ * .fn(t => {
+ * const data = t.params.data.map(v => typedArrayFromParam(v));
+ * })
+ * ```
+ */
+export function typedArrayParam<K extends keyof TypedArrayMap>(
+ type: K,
+ data: number[]
+): TypedArrayParam<K> {
+ return { type, data };
+}
+
+export function createTypedArray<K extends keyof TypedArrayMap>(
+ type: K,
+ data: readonly number[]
+): TypedArrayMap[K] {
+ return new kTypedArrayBufferViews[type](data) as TypedArrayMap[K];
+}
+
+/**
+ * Converts a TypedArrayParam to a typedarray. See typedArrayParam
+ */
+export function typedArrayFromParam<K extends keyof TypedArrayMap>(
+ param: TypedArrayParam<K>
+): TypedArrayMap[K] {
+ const { type, data } = param;
+ return createTypedArray(type, data);
+}
+
+function subarrayAsU8(
+ buf: ArrayBuffer | TypedArrayBufferView,
+ { start = 0, length }: { start?: number; length?: number }
+): Uint8Array | Uint8ClampedArray {
+ if (buf instanceof ArrayBuffer) {
+ return new Uint8Array(buf, start, length);
+ } else if (buf instanceof Uint8Array || buf instanceof Uint8ClampedArray) {
+ // Don't wrap in new views if we don't need to.
+ if (start === 0 && (length === undefined || length === buf.byteLength)) {
+ return buf;
+ }
+ }
+ const byteOffset = buf.byteOffset + start * buf.BYTES_PER_ELEMENT;
+ const byteLength =
+ length !== undefined
+ ? length * buf.BYTES_PER_ELEMENT
+ : buf.byteLength - (byteOffset - buf.byteOffset);
+ return new Uint8Array(buf.buffer, byteOffset, byteLength);
+}
+
+/**
+ * Copy a range of bytes from one ArrayBuffer or TypedArray to another.
+ *
+ * `start`/`length` are in elements (or in bytes, if ArrayBuffer).
+ */
+export function memcpy(
+ src: { src: ArrayBuffer | TypedArrayBufferView; start?: number; length?: number },
+ dst: { dst: ArrayBuffer | TypedArrayBufferView; start?: number }
+): void {
+ subarrayAsU8(dst.dst, dst).set(subarrayAsU8(src.src, src));
+}
+
+/**
+ * Used to create a value that is specified by multiplying some runtime value
+ * by a constant and then adding a constant to it.
+ */
+export interface ValueTestVariant {
+ mult: number;
+ add: number;
+}
+
+/**
+ * Filters out SpecValues that are the same.
+ */
+export function filterUniqueValueTestVariants(valueTestVariants: ValueTestVariant[]) {
+ return new Map<string, ValueTestVariant>(
+ valueTestVariants.map(v => [`m:${v.mult},a:${v.add}`, v])
+ ).values();
+}
+
+/**
+ * Used to create a value that is specified by multiplied some runtime value
+ * by a constant and then adding a constant to it. This happens often in test
+ * with limits that can only be known at runtime and yet we need a way to
+ * add parameters to a test and those parameters must be constants.
+ */
+export function makeValueTestVariant(base: number, variant: ValueTestVariant) {
+ return base * variant.mult + variant.add;
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts b/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts
new file mode 100644
index 0000000000..7d10520bcb
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/common/util/wpt_reftest_wait.ts
@@ -0,0 +1,24 @@
+import { timeout } from './timeout.js';
+
+// Copied from https://github.com/web-platform-tests/wpt/blob/master/common/reftest-wait.js
+
+/**
+ * Remove the `reftest-wait` class on the document element.
+ * The reftest runner will wait with taking a screenshot while
+ * this class is present.
+ *
+ * See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs
+ */
+export function takeScreenshot() {
+ document.documentElement.classList.remove('reftest-wait');
+}
+
+/**
+ * Call `takeScreenshot()` after a delay of at least `ms` milliseconds.
+ * @param {number} ms - milliseconds
+ */
+export function takeScreenshotDelayed(ms: number) {
+ timeout(() => {
+ takeScreenshot();
+ }, ms);
+}