Skip to content

Commit

Permalink
checkPixelsByT2B
Browse files Browse the repository at this point in the history
  • Loading branch information
kainino0x committed Mar 16, 2022
1 parent fe019ad commit c08d618
Showing 1 changed file with 281 additions and 0 deletions.
281 changes: 281 additions & 0 deletions src/webgpu/util/texture/check_pixels.ts
Original file line number Diff line number Diff line change
@@ -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<T> = (coords: Required<GPUOrigin3DDict>) => T;

export type CheckPixelsGenerator = PerPixelAtLevel<PerTexelComponent<number>>;

export type PixelView = PerPixelAtLevel<{
bytes: ArrayBuffer;
ulpFromZero: PerTexelComponent<number>;
color: PerTexelComponent<number>;
}>;

/** 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<boolean> {
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<GPUOrigin3DDict>,
subrectSize: Required<GPUExtent3DDict>,
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<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 (!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<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.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<GPUExtent3DDict>;
range: { readonly black: number; readonly white: number };
texelView: TexelView;

constructor(
size: Required<GPUExtent3DDict>,
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();
}
}

0 comments on commit c08d618

Please sign in to comment.