diff options
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.ts | 341 |
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), + })); +} |