| import { assert, ErrorWithExtra, unreachable } from '../../../common/util/util.js'; |
| import { |
| EncodableTextureFormat, |
| getTextureFormatType, |
| isColorTextureFormat, |
| isDepthTextureFormat, |
| } from '../../format_info.js'; |
| import { GPUTestBase } from '../../gpu_test.js'; |
| import { numbersApproximatelyEqual } from '../conversion.js'; |
| import { generatePrettyTable, numericToStringBuilder } from '../pretty_diff_tables.js'; |
| import { reifyExtent3D, reifyOrigin3D } from '../unions.js'; |
| |
| import { fullSubrectCoordinates } from './base.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; |
| }; |
| |
| export type PixelExpectation = PerTexelComponent<number> | Uint8Array; |
| |
| export type PerPixelComparison<E extends PixelExpectation> = { |
| coord: GPUOrigin3D; |
| exp: E; |
| }; |
| |
| 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 numbersApproximatelyEqual(act, exp, maxDiff); |
| }); |
| } |
| |
| /** Create a new mappable GPUBuffer, and copy a subrectangle of GPUTexture data into it. */ |
| function createTextureCopyForMapRead( |
| t: GPUTestBase, |
| source: GPUTexelCopyTextureInfo, |
| copySize: GPUExtent3D, |
| { format }: { format: EncodableTextureFormat } |
| ): { buffer: GPUBuffer; bytesPerRow: number; rowsPerImage: number } { |
| const { byteLength, bytesPerRow, rowsPerImage } = getTextureSubCopyLayout(format, copySize, { |
| aspect: source.aspect, |
| }); |
| |
| const buffer = t.createBufferTracked({ |
| usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, |
| size: byteLength, |
| }); |
| |
| const cmd = t.device.createCommandEncoder({ label: 'createTextureCopyForMapRead' }); |
| cmd.copyTextureToBuffer(source, { buffer, bytesPerRow, rowsPerImage }, copySize); |
| t.device.queue.submit([cmd.finish()]); |
| |
| return { buffer, bytesPerRow, rowsPerImage }; |
| } |
| |
| export function findFailedPixels( |
| format: EncodableTextureFormat, |
| subrectOrigin: Required<GPUOrigin3DDict>, |
| subrectSize: Required<GPUExtent3DDict>, |
| { actTexelView, expTexelView }: { actTexelView: TexelView; expTexelView: TexelView }, |
| texelCompareOptions: TexelCompareOptions, |
| coords?: Generator<Required<GPUOrigin3DDict>> |
| ) { |
| 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 (const coord of coords ?? fullSubrectCoordinates(subrectOrigin, subrectSize)) { |
| const { x, y, z } = coord; |
| if (!comparer.predicate(coord)) { |
| failedPixels.push(coord); |
| 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 repr = kTexelRepresentationInfo[format]; |
| // MAINTENANCE_TODO: Print depth-stencil formats as float+int instead of float+float. |
| const printAsInteger = isColorTextureFormat(format) |
| ? // For color, pick the type based on the format type |
| ['uint', 'sint'].includes(getTextureFormatType(format)) |
| : // Print depth as "float", depth-stencil as "float,float", stencil as "int". |
| !isDepthTextureFormat(format); |
| const numericToString = numericToStringBuilder(printAsInteger); |
| |
| 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 => numericToString(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 => numericToString(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, |
| numericToString, |
| }; |
| 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: GPUTestBase, |
| source: GPUTexelCopyTextureInfo, |
| copySize_: GPUExtent3D, |
| { expTexelView }: { expTexelView: TexelView }, |
| texelCompareOptions: TexelCompareOptions, |
| coords?: Generator<Required<GPUOrigin3DDict>> |
| ): 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, |
| coords |
| ); |
| |
| 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), |
| })); |
| } |