Skip to content

Commit

Permalink
Add 'skipUndefined' Comparator (#2082)
Browse files Browse the repository at this point in the history
This adds a Comparator that if the Expectation is undefined, just
returns true, so that tests can include an 'Any' test condition. This
is used in 'pack2x16float' to replace its custom Comparator and allow
for serialization/usage of the cache.

Fixes #2077
  • Loading branch information
zoddicus authored Dec 15, 2022
1 parent e0e7353 commit e583fa4
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 36 deletions.
33 changes: 32 additions & 1 deletion src/unittests/serialization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
deserializeExpectation,
serializeExpectation,
} from '../webgpu/shader/execution/expression/case_cache.js';
import { anyOf, deserializeComparator, SerializedComparator } from '../webgpu/util/compare.js';
import {
anyOf,
deserializeComparator,
SerializedComparator,
skipUndefined,
} from '../webgpu/util/compare.js';
import { kValue } from '../webgpu/util/constants.js';
import {
bool,
Expand Down Expand Up @@ -226,3 +231,29 @@ g.test('anyOf').fn(t => {
}
});
});

g.test('skipUndefined').fn(t => {
enableBuildingDataCache(() => {
for (const c of [
{
comparator: skipUndefined(i32(123)),
testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
},
{
comparator: skipUndefined(undefined),
testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
},
]) {
const serialized = c.comparator as SerializedComparator;
const deserialized = deserializeComparator(serialized);
for (const val of c.testCases) {
const got = deserialized(val);
const expect = c.comparator(val);
t.expect(
got.matched === expect.matched,
`comparator(${val}): got: ${expect.matched}, expect: ${got.matched}`
);
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@ which is then placed in bits 16 × i through 16 × i + 15 of the result.
`;

import { makeTestGroup } from '../../../../../../common/framework/test_group.js';
import { assert } from '../../../../../../common/util/util.js';
import { GPUTest } from '../../../../../gpu_test.js';
import { anyOf, Comparator } from '../../../../../util/compare.js';
import { anyOf, skipUndefined } from '../../../../../util/compare.js';
import {
f32,
pack2x16float,
Scalar,
TypeF32,
TypeU32,
TypeVec,
u32,
vec2,
} from '../../../../../util/conversion.js';
import { cartesianProduct, fullF32Range, quantizeToF32 } from '../../../../../util/math.js';
import { makeCaseCache } from '../../case_cache.js';
import { allInputSources, Case, run } from '../../expression.js';

import { builtin } from './builtin.js';
Expand All @@ -28,29 +27,6 @@ export const g = makeTestGroup(GPUTest);
// pack2x16float has somewhat unusual behaviour, specifically around how it is
// supposed to behave when values go OOB and when they are considered to have
// gone OOB, so has its own bespoke implementation.
//
// The use of a custom Comparator prevents easy serialization in the case cache,
// https://github.com/gpuweb/cts/issues/2077

/**
* @returns a custom comparator for a possible result from pack2x16float
* @param expectation element of the array generated by pack2x16float
*/
function cmp(expectation: number | undefined): Comparator {
return got => {
assert(got instanceof Scalar, `Received non-Scalar Value in pack2x16float comparator`);
let matched = true;
if (expectation !== undefined) {
matched = (got.value as number) === expectation;
}

return {
matched,
got: `${got}`,
expected: `${expectation !== undefined ? u32(expectation) : 'Any'}`,
};
};
}

/**
* @returns a Case for `pack2x16float`
Expand All @@ -68,7 +44,12 @@ function makeCase(param0: number, param1: number, filter_undefined: boolean): Ca
return undefined;
}

return { input: [vec2(f32(param0), f32(param1))], expected: anyOf(...results.map(cmp)) };
return {
input: [vec2(f32(param0), f32(param1))],
expected: anyOf(
...results.map(r => (r === undefined ? skipUndefined(undefined) : skipUndefined(u32(r))))
),
};
}

/**
Expand All @@ -84,6 +65,15 @@ function generateCases(param0s: number[], param1s: number[], filter_undefined: b
.filter((c): c is Case => c !== undefined);
}

export const d = makeCaseCache('pack2x16float', {
f32_const: () => {
return generateCases(fullF32Range(), fullF32Range(), true);
},
f32_non_const: () => {
return generateCases(fullF32Range(), fullF32Range(), false);
},
});

g.test('pack')
.specURL('https://www.w3.org/TR/WGSL/#pack-builtin-functions')
.desc(
Expand All @@ -93,9 +83,6 @@ g.test('pack')
)
.params(u => u.combine('inputSource', allInputSources))
.fn(async t => {
const cases =
t.params.inputSource === 'const'
? generateCases(fullF32Range(), fullF32Range(), true)
: generateCases(fullF32Range(), fullF32Range(), false);
const cases = await d.get(t.params.inputSource === 'const' ? 'f32_const' : 'f32_non_const');
await run(t, builtin('pack2x16float'), [TypeVec(2, TypeF32)], TypeU32, t.params, cases);
});
11 changes: 9 additions & 2 deletions src/webgpu/shader/execution/expression/case_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,15 @@ export function serializeExpectation(e: Expectation): SerializedExpectation {
}
if (e instanceof Function) {
const comp = (e as unknown) as SerializedComparator;
if (comp.kind !== undefined && comp.data !== undefined) {
return { kind: 'comparator', value: { kind: comp.kind, data: comp.data } };
if (comp !== undefined) {
// if blocks used to refine the type of comp.kind, otherwise it is
// actually the union of the string values
if (comp.kind === 'anyOf') {
return { kind: 'comparator', value: { kind: comp.kind, data: comp.data } };
}
if (comp.kind === 'skipUndefined') {
return { kind: 'comparator', value: { kind: comp.kind, data: comp.data } };
}
}
throw 'cannot serialize comparator';
}
Expand Down
36 changes: 34 additions & 2 deletions src/webgpu/util/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,43 @@ export function anyOf(
return comparator;
}

/** @returns a Comparator that skips the test if the expectation is undefined */
export function skipUndefined(
expectation: Expectation | undefined
): Comparator | (Comparator & SerializedComparator) {
const comparator = (got: Value) => {
if (expectation !== undefined) {
return toComparator(expectation)(got);
}
return { matched: true, got: got.toString(), expected: `Treating 'undefined' as Any` };
};

if (getIsBuildingDataCache()) {
// If there's an active DataCache, and it supports storing, then append the
// comparator kind and serialized expectations to the comparator, so it can
// be serialized.
comparator.kind = 'skipUndefined';
if (expectation !== undefined) {
comparator.data = serializeExpectation(expectation);
}
}
return comparator;
}

/** SerializedComparatorAnyOf is the serialized type of an `anyOf` comparator. */
type SerializedComparatorAnyOf = {
kind: 'anyOf';
data: SerializedExpectation[];
};

/** SerializedComparatorSkipUndefined is the serialized type of an `skipUndefined` comparator. */
type SerializedComparatorSkipUndefined = {
kind: 'skipUndefined';
data?: SerializedExpectation;
};

/** SerializedComparator is a union of all the possible serialized comparator types. */
export type SerializedComparator = SerializedComparatorAnyOf;
export type SerializedComparator = SerializedComparatorAnyOf | SerializedComparatorSkipUndefined;

/**
* Deserializes a comparator from a SerializedComparator.
Expand All @@ -245,6 +274,9 @@ export function deserializeComparator(data: SerializedComparator): Comparator {
case 'anyOf': {
return anyOf(...data.data.map(e => deserializeExpectation(e)));
}
case 'skipUndefined': {
return skipUndefined(data.data !== undefined ? deserializeExpectation(data.data) : undefined);
}
}
throw `unhandled comparator kind: ${data.kind}`;
throw `unhandled comparator kind`;
}

0 comments on commit e583fa4

Please sign in to comment.