-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |