From c08d618f150099a6c265560c3897875bbe66399d Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Thu, 10 Mar 2022 21:16:04 -0800 Subject: [PATCH] checkPixelsByT2B --- src/webgpu/util/texture/check_pixels.ts | 281 ++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 src/webgpu/util/texture/check_pixels.ts diff --git a/src/webgpu/util/texture/check_pixels.ts b/src/webgpu/util/texture/check_pixels.ts new file mode 100644 index 000000000000..41cfd461b56e --- /dev/null +++ b/src/webgpu/util/texture/check_pixels.ts @@ -0,0 +1,281 @@ +import { ErrorWithExtra } 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 = (coords: Required) => T; + +export type CheckPixelsGenerator = PerPixelAtLevel>; + +export type PixelView = PerPixelAtLevel<{ + bytes: ArrayBuffer; + ulpFromZero: PerTexelComponent; + color: PerTexelComponent; +}>; + +/** 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); + + 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 compareTexelViewsByULP( + actTexelView: TexelView, + expTexelView: TexelView, + maxDiffULPs: number +): PerPixelAtLevel { + return coords => { + const actual = actTexelView.ulpFromZero(coords); + const expected = expTexelView.ulpFromZero(coords); + 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) <= maxDiffULPs; + }); + }; +} + +function findFailedPixels( + format: EncodableTextureFormat, + subrectOrigin: Required, + subrectSize: Required, + actTexelView: TexelView, + expTexelView: TexelView, + maxDiffULPs: number +) { + const predicate = compareTexelViewsByULP(actTexelView, expTexelView, maxDiffULPs); + + const lowerCorner = [subrectSize.width, subrectSize.height, subrectSize.depthOrArrayLayers]; + const upperCorner = [0, 0, 0]; + const failedPixels: Required[] = []; + 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 (!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(4); + + 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* [' actual texel bytes', '==', '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 => 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 => pixel[ch]).join(',')}`; + } + })(); + const printActualULPs = (function* () { + yield* [' act. 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* [` ±${maxDiffULPs}ULPs, exp. ULPs-from-zero`, '~=', componentOrderStr]; + for (const coords of failedPixels) { + const pixel = expTexelView.ulpFromZero(coords); + yield `${repr.componentOrder.map(ch => pixel[ch]).join(',')}`; + } + })(); + const printDiffULPs = (function* () { + yield* [` diff in ULPs`, '==', componentOrderStr]; + for (const coords of failedPixels) { + const act = actTexelView.ulpFromZero(coords); + const exp = expTexelView.ulpFromZero(coords); + yield `${repr.componentOrder.map(ch => act[ch]! - exp[ch]!).join(',')}`; + } + })(); + + const opts = { + fillToWidth: 120, + numberToString, + }; + return `\ + between ${lowerCorner} and ${upperCorner} inclusive: +${generatePrettyTable(opts, [ + printCoords, + printActualBytes, + printActualColors, + printExpectedColors, + printActualULPs, + printExpectedULPs, + printDiffULPs, +])}`; +} + +/** + * FIXME + */ +export async function checkPixelsByT2B( + t: GPUTest, + source: GPUImageCopyTexture, + copySize_: GPUExtent3D, + { + maxDiffULPs, + expTexelView, + }: { + maxDiffULPs: number; + expTexelView: TexelView; + } +): Promise { + 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.fromTextureData(format, data, texelViewConfig); + + // MAINTENANCE_TODO(#973): Could somehow allow multiple encodings of a given expected color. + const failedPixelsMessage = findFailedPixels( + format, + subrectOrigin, + subrectSize, + actTexelView, + expTexelView, + maxDiffULPs + ); + + if (failedPixelsMessage === undefined) { + return undefined; + } + + const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage; + return new ErrorWithExtra(msg, () => ({ + expected: new DebugPixelData( + subrectSize, + { black: 0, white: 255 }, // FIXME + expTexelView + ), + actual: new DebugPixelData( + subrectSize, + { black: 0, white: 255 }, // FIXME + // For DebugPixelData, make a new TexelView with a copy of the data. + TexelView.fromTextureData(format, data.slice(), texelViewConfig) + ), + })); +} + +class DebugPixelData { + size: Required; + range: { readonly black: number; readonly white: number }; + texelView: TexelView; + + constructor( + size: Required, + range: { readonly black: number; readonly white: number }, + texelView: TexelView + ) { + this.size = size; + this.range = range; + this.texelView = texelView; + } + + toDataURL(z: number) { + const w = this.size.width; + const h = this.size.height; + const range = this.range; + + const image = new ImageData(w, h); + const remap = (v: number) => ((v - range.black) / (range.white - range.black)) * 255; + for (let y = 0; y < h; ++y) { + for (let x = 0; x < w; ++x) { + const color = this.texelView.color({ x, y, z }); + color.R ??= 0; + color.G ??= 0; + color.B ??= 0; + color.A ??= this.range.white; + // Uint8ClampedArray automatically clamps values to its valid range. + image.data[(y * w + x) * 4 + 0] = remap(color.R); + image.data[(y * w + x) * 4 + 1] = remap(color.G); + image.data[(y * w + x) * 4 + 2] = remap(color.B); + image.data[(y * w + x) * 4 + 3] = remap(color.A); + } + } + + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d')!; + ctx.putImageData(image, 0, 0); + return canvas.toDataURL(); + } +}