summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts341
1 files changed, 341 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts
new file mode 100644
index 0000000000..3481c90b1f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/util/texture/texture_ok.ts
@@ -0,0 +1,341 @@
+import { assert, ErrorWithExtra, unreachable } from '../../../common/util/util.js';
+import { EncodableTextureFormat, kTextureFormatInfo } from '../../capability_info.js';
+import { GPUTest } from '../../gpu_test.js';
+import { generatePrettyTable } from '../pretty_diff_tables.js';
+import { reifyExtent3D, reifyOrigin3D } from '../unions.js';
+
+import { getTextureSubCopyLayout } from './layout.js';
+import { kTexelRepresentationInfo, PerTexelComponent, TexelComponent } from './texel_data.js';
+import { TexelView } from './texel_view.js';
+
+type PerPixelAtLevel<T> = (coords: Required<GPUOrigin3DDict>) => T;
+
+/** Threshold options for comparing texels of different formats (norm/float/int). */
+export type TexelCompareOptions = {
+ /** Threshold for integer texture formats. Defaults to 0. */
+ maxIntDiff?: number;
+ /** Threshold for non-integer (norm/float) texture formats, if not overridden. */
+ maxFractionalDiff?: number;
+ /** Threshold in ULPs for unorm/snorm texture formats. Overrides `maxFractionalDiff`. */
+ maxDiffULPsForNormFormat?: number;
+ /** Threshold in ULPs for float/ufloat texture formats. Overrides `maxFractionalDiff`. */
+ maxDiffULPsForFloatFormat?: number;
+};
+
+type TexelViewComparer = {
+ /** Given coords, returns whether the two texel views are considered matching at that point. */
+ predicate: PerPixelAtLevel<boolean>;
+ /**
+ * Given a list of failed coords, returns table rows for `generatePrettyTable` that
+ * display the actual/expected values and diffs for debugging.
+ */
+ tableRows: (failedCoords: readonly Required<GPUOrigin3DDict>[]) => Iterable<string>[];
+};
+
+function makeTexelViewComparer(
+ format: EncodableTextureFormat,
+ { actTexelView, expTexelView }: { actTexelView: TexelView; expTexelView: TexelView },
+ opts: TexelCompareOptions
+): TexelViewComparer {
+ const {
+ maxIntDiff = 0,
+ maxFractionalDiff,
+ maxDiffULPsForNormFormat,
+ maxDiffULPsForFloatFormat,
+ } = opts;
+
+ assert(maxIntDiff >= 0, 'threshold must be non-negative');
+ if (maxFractionalDiff !== undefined) {
+ assert(maxFractionalDiff >= 0, 'threshold must be non-negative');
+ }
+ if (maxDiffULPsForFloatFormat !== undefined) {
+ assert(maxDiffULPsForFloatFormat >= 0, 'threshold must be non-negative');
+ }
+ if (maxDiffULPsForNormFormat !== undefined) {
+ assert(maxDiffULPsForNormFormat >= 0, 'threshold must be non-negative');
+ }
+
+ const fmtIsInt = format.includes('int');
+ const fmtIsNorm = format.includes('norm');
+ const fmtIsFloat = format.includes('float');
+
+ const tvc = {} as TexelViewComparer;
+ if (fmtIsInt) {
+ tvc.predicate = coords =>
+ comparePerComponent(actTexelView.color(coords), expTexelView.color(coords), maxIntDiff);
+ } else if (fmtIsNorm && maxDiffULPsForNormFormat !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.ulpFromZero(coords),
+ expTexelView.ulpFromZero(coords),
+ maxDiffULPsForNormFormat
+ );
+ } else if (fmtIsFloat && maxDiffULPsForFloatFormat !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.ulpFromZero(coords),
+ expTexelView.ulpFromZero(coords),
+ maxDiffULPsForFloatFormat
+ );
+ } else if (maxFractionalDiff !== undefined) {
+ tvc.predicate = coords =>
+ comparePerComponent(
+ actTexelView.color(coords),
+ expTexelView.color(coords),
+ maxFractionalDiff
+ );
+ } else {
+ if (fmtIsNorm) {
+ unreachable('need maxFractionalDiff or maxDiffULPsForNormFormat to compare norm textures');
+ } else if (fmtIsFloat) {
+ unreachable('need maxFractionalDiff or maxDiffULPsForFloatFormat to compare float textures');
+ } else {
+ unreachable();
+ }
+ }
+
+ const repr = kTexelRepresentationInfo[format];
+ if (fmtIsInt) {
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${maxIntDiff}`],
+ (function* () {
+ yield* [` diff (act - exp)`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.color(coords);
+ const exp = expTexelView.color(coords);
+ yield repr.componentOrder.map(ch => act[ch]! - exp[ch]!).join(',');
+ }
+ })(),
+ ];
+ } else if (
+ (fmtIsNorm && maxDiffULPsForNormFormat !== undefined) ||
+ (fmtIsFloat && maxDiffULPsForFloatFormat !== undefined)
+ ) {
+ const toleranceULPs = fmtIsNorm ? maxDiffULPsForNormFormat! : maxDiffULPsForFloatFormat!;
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${toleranceULPs} normal-ULPs`],
+ (function* () {
+ yield* [` diff (act - exp) in normal-ULPs`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.ulpFromZero(coords);
+ const exp = expTexelView.ulpFromZero(coords);
+ yield repr.componentOrder.map(ch => act[ch]! - exp[ch]!).join(',');
+ }
+ })(),
+ ];
+ } else {
+ assert(maxFractionalDiff !== undefined);
+ tvc.tableRows = failedCoords => [
+ [`tolerance ± ${maxFractionalDiff}`],
+ (function* () {
+ yield* [` diff (act - exp)`, '==', ''];
+ for (const coords of failedCoords) {
+ const act = actTexelView.color(coords);
+ const exp = expTexelView.color(coords);
+ yield repr.componentOrder.map(ch => (act[ch]! - exp[ch]!).toPrecision(4)).join(',');
+ }
+ })(),
+ ];
+ }
+
+ return tvc;
+}
+
+function comparePerComponent(
+ actual: PerTexelComponent<number>,
+ expected: PerTexelComponent<number>,
+ maxDiff: number
+) {
+ return Object.keys(actual).every(key => {
+ const k = key as TexelComponent;
+ const act = actual[k]!;
+ const exp = expected[k];
+ if (exp === undefined) return false;
+ return Math.abs(act - exp) <= maxDiff;
+ });
+}
+
+/** Create a new mappable GPUBuffer, and copy a subrectangle of GPUTexture data into it. */
+function createTextureCopyForMapRead(
+ t: GPUTest,
+ source: GPUImageCopyTexture,
+ copySize: GPUExtent3D,
+ { format }: { format: EncodableTextureFormat }
+): { buffer: GPUBuffer; bytesPerRow: number; rowsPerImage: number } {
+ const { byteLength, bytesPerRow, rowsPerImage } = getTextureSubCopyLayout(format, copySize, {
+ aspect: source.aspect,
+ });
+
+ const buffer = t.device.createBuffer({
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
+ size: byteLength,
+ });
+ t.trackForCleanup(buffer);
+
+ const cmd = t.device.createCommandEncoder();
+ cmd.copyTextureToBuffer(source, { buffer, bytesPerRow, rowsPerImage }, copySize);
+ t.device.queue.submit([cmd.finish()]);
+
+ return { buffer, bytesPerRow, rowsPerImage };
+}
+
+function findFailedPixels(
+ format: EncodableTextureFormat,
+ subrectOrigin: Required<GPUOrigin3DDict>,
+ subrectSize: Required<GPUExtent3DDict>,
+ { actTexelView, expTexelView }: { actTexelView: TexelView; expTexelView: TexelView },
+ texelCompareOptions: TexelCompareOptions
+) {
+ const comparer = makeTexelViewComparer(
+ format,
+ { actTexelView, expTexelView },
+ texelCompareOptions
+ );
+
+ const lowerCorner = [subrectSize.width, subrectSize.height, subrectSize.depthOrArrayLayers];
+ const upperCorner = [0, 0, 0];
+ const failedPixels: Required<GPUOrigin3DDict>[] = [];
+ for (let z = subrectOrigin.z; z < subrectOrigin.z + subrectSize.depthOrArrayLayers; ++z) {
+ for (let y = subrectOrigin.y; y < subrectOrigin.y + subrectSize.height; ++y) {
+ for (let x = subrectOrigin.x; x < subrectOrigin.x + subrectSize.width; ++x) {
+ const coords = { x, y, z };
+
+ if (!comparer.predicate(coords)) {
+ failedPixels.push(coords);
+ lowerCorner[0] = Math.min(lowerCorner[0], x);
+ lowerCorner[1] = Math.min(lowerCorner[1], y);
+ lowerCorner[2] = Math.min(lowerCorner[2], z);
+ upperCorner[0] = Math.max(upperCorner[0], x);
+ upperCorner[1] = Math.max(upperCorner[1], y);
+ upperCorner[2] = Math.max(upperCorner[2], z);
+ }
+ }
+ }
+ }
+ if (failedPixels.length === 0) {
+ return undefined;
+ }
+
+ const info = kTextureFormatInfo[format];
+ const repr = kTexelRepresentationInfo[format];
+
+ const integerSampleType = info.sampleType === 'uint' || info.sampleType === 'sint';
+ const numberToString = integerSampleType
+ ? (n: number) => n.toFixed()
+ : (n: number) => n.toPrecision(6);
+
+ const componentOrderStr = repr.componentOrder.join(',') + ':';
+
+ const printCoords = (function* () {
+ yield* [' coords', '==', 'X,Y,Z:'];
+ for (const coords of failedPixels) yield `${coords.x},${coords.y},${coords.z}`;
+ })();
+ const printActualBytes = (function* () {
+ yield* [' act. texel bytes (little-endian)', '==', '0x:'];
+ for (const coords of failedPixels) {
+ yield Array.from(actTexelView.bytes(coords), b => b.toString(16).padStart(2, '0')).join(' ');
+ }
+ })();
+ const printActualColors = (function* () {
+ yield* [' act. colors', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = actTexelView.color(coords);
+ yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`;
+ }
+ })();
+ const printExpectedColors = (function* () {
+ yield* [' exp. colors', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = expTexelView.color(coords);
+ yield `${repr.componentOrder.map(ch => numberToString(pixel[ch]!)).join(',')}`;
+ }
+ })();
+ const printActualULPs = (function* () {
+ yield* [' act. normal-ULPs-from-zero', '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = actTexelView.ulpFromZero(coords);
+ yield `${repr.componentOrder.map(ch => pixel[ch]).join(',')}`;
+ }
+ })();
+ const printExpectedULPs = (function* () {
+ yield* [` exp. normal-ULPs-from-zero`, '==', componentOrderStr];
+ for (const coords of failedPixels) {
+ const pixel = expTexelView.ulpFromZero(coords);
+ yield `${repr.componentOrder.map(ch => pixel[ch]).join(',')}`;
+ }
+ })();
+
+ const opts = {
+ fillToWidth: 120,
+ numberToString,
+ };
+ return `\
+ between ${lowerCorner} and ${upperCorner} inclusive:
+${generatePrettyTable(opts, [
+ printCoords,
+ printActualBytes,
+ printActualColors,
+ printExpectedColors,
+ printActualULPs,
+ printExpectedULPs,
+ ...comparer.tableRows(failedPixels),
+])}`;
+}
+
+/**
+ * Check the contents of a GPUTexture by reading it back (with copyTextureToBuffer+mapAsync), then
+ * comparing the data with the data in `expTexelView`.
+ *
+ * The actual and expected texture data are both converted to the "NormalULPFromZero" format,
+ * which is a signed number representing how far the number is from zero, in ULPs, skipping
+ * subnormal numbers (where ULP is defined for float, normalized, and integer formats).
+ */
+export async function textureContentIsOKByT2B(
+ t: GPUTest,
+ source: GPUImageCopyTexture,
+ copySize_: GPUExtent3D,
+ { expTexelView }: { expTexelView: TexelView },
+ texelCompareOptions: TexelCompareOptions
+): Promise<ErrorWithExtra | undefined> {
+ const subrectOrigin = reifyOrigin3D(source.origin ?? [0, 0, 0]);
+ const subrectSize = reifyExtent3D(copySize_);
+ const format = expTexelView.format;
+
+ const { buffer, bytesPerRow, rowsPerImage } = createTextureCopyForMapRead(
+ t,
+ source,
+ subrectSize,
+ { format }
+ );
+
+ await buffer.mapAsync(GPUMapMode.READ);
+ const data = new Uint8Array(buffer.getMappedRange());
+
+ const texelViewConfig = {
+ bytesPerRow,
+ rowsPerImage,
+ subrectOrigin,
+ subrectSize,
+ } as const;
+
+ const actTexelView = TexelView.fromTextureDataByReference(format, data, texelViewConfig);
+
+ const failedPixelsMessage = findFailedPixels(
+ format,
+ subrectOrigin,
+ subrectSize,
+ { actTexelView, expTexelView },
+ texelCompareOptions
+ );
+
+ if (failedPixelsMessage === undefined) {
+ return undefined;
+ }
+
+ const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage;
+ return new ErrorWithExtra(msg, () => ({
+ expTexelView,
+ // Make a new TexelView with a copy of the data so we can unmap the buffer (debug mode only).
+ actTexelView: TexelView.fromTextureDataByReference(format, data.slice(), texelViewConfig),
+ }));
+}