blob: bd12291917f0cf1d345fc571136c4234599e6b76 [file] [log] [blame]
import { assert, memcpy, TypedArrayBufferView } from '../common/util/util.js';
import {
ColorTextureFormat,
EncodableTextureFormat,
getBlockInfoForColorTextureFormat,
getBlockInfoForTextureFormat,
kEncodableTextureFormats,
resolvePerAspectFormat,
} from './format_info.js';
import { GPUTest } from './gpu_test.js';
import { checkElementsEqual } from './util/check_contents.js';
import { align } from './util/math.js';
import { physicalMipSizeFromTexture, virtualMipSize } from './util/texture/base.js';
import {
bytesInACompleteRow,
getTextureCopyLayout,
LayoutOptions as TextureLayoutOptions,
} from './util/texture/layout.js';
import { PerTexelComponent } from './util/texture/texel_data.js';
import { TexelView } from './util/texture/texel_view.js';
import {
PerPixelComparison,
PixelExpectation,
TexelCompareOptions,
textureContentIsOKByT2B,
} from './util/texture/texture_ok.js';
import { createTextureFromTexelViews } from './util/texture.js';
import { reifyOrigin3D } from './util/unions.js';
type PipelineType = '2d' | '2d-array' | '3d';
type ImageCopyTestResources = {
pipelineByPipelineType: Map<PipelineType, GPURenderPipeline>;
};
const s_deviceToResourcesMap = new WeakMap<GPUDevice, ImageCopyTestResources>();
/**
* Gets a (cached) pipeline to render a texture to an rgba8unorm texture
*/
function getPipelineToRenderTextureToRGB8UnormTexture(device: GPUDevice, texture: GPUTexture) {
if (!s_deviceToResourcesMap.has(device)) {
s_deviceToResourcesMap.set(device, {
pipelineByPipelineType: new Map<PipelineType, GPURenderPipeline>(),
});
}
const { pipelineByPipelineType } = s_deviceToResourcesMap.get(device)!;
const pipelineType: PipelineType =
texture.dimension === '3d' ? '3d' : texture.depthOrArrayLayers > 1 ? '2d-array' : '2d';
if (!pipelineByPipelineType.get(pipelineType)) {
const [textureType, coordCode] =
pipelineType === '3d'
? [
'texture_3d',
'vec3f(fsInput.texcoord, (f32(uni.baseArrayLayer) + 0.5) / f32(textureDimensions(ourTexture, 0).z))',
]
: pipelineType === '2d'
? ['texture_2d', 'fsInput.texcoord']
: ['texture_2d_array', 'fsInput.texcoord, uni.baseArrayLayer'];
const code = `
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
struct Uniforms {
baseArrayLayer: u32,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> VSOutput {
let pos = array(
vec2f(-1, -1),
vec2f(-1, 3),
vec2f( 3, -1),
);
var vsOutput: VSOutput;
let xy = pos[vertexIndex];
vsOutput.position = vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5);
return vsOutput;
}
@group(0) @binding(0) var ourSampler: sampler;
@group(0) @binding(1) var ourTexture: ${textureType}<f32>;
@group(0) @binding(2) var<uniform> uni: Uniforms;
@fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
_ = uni;
return textureSample(ourTexture, ourSampler, ${coordCode});
}
`;
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({
label: `layer rendered for ${pipelineType}`,
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format: 'rgba8unorm' }],
},
});
pipelineByPipelineType.set(pipelineType, pipeline);
}
const pipeline = pipelineByPipelineType.get(pipelineType)!;
return { pipelineType, pipeline };
}
type LinearCopyParameters = {
dataLayout: Required<GPUTexelCopyBufferLayout>;
origin: Required<GPUOrigin3DDict>;
data: Uint8Array;
};
/**
* Creates a 1 mip level texture with the contents of a TexelView.
*/
export function createTextureFromTexelView(
t: GPUTest,
texelView: TexelView,
desc: Omit<GPUTextureDescriptor, 'format'>
): GPUTexture {
return createTextureFromTexelViews(t, [texelView], desc);
}
export function createTextureFromTexelViewsMultipleMipmaps(
t: GPUTest,
texelViews: TexelView[],
desc: Omit<GPUTextureDescriptor, 'format'>
): GPUTexture {
return createTextureFromTexelViews(t, texelViews, desc);
}
export function expectTexelViewComparisonIsOkInTexture(
t: GPUTest,
src: GPUTexelCopyTextureInfo,
exp: TexelView,
size: GPUExtent3D,
comparisonOptions = {
maxIntDiff: 0,
maxDiffULPsForNormFormat: 1,
maxDiffULPsForFloatFormat: 1,
} as TexelCompareOptions
): void {
t.eventualExpectOK(
textureContentIsOKByT2B(t, src, size, { expTexelView: exp }, comparisonOptions)
);
}
export function expectSinglePixelComparisonsAreOkInTexture<E extends PixelExpectation>(
t: GPUTest,
src: GPUTexelCopyTextureInfo,
exp: PerPixelComparison<E>[],
comparisonOptions = {
maxIntDiff: 0,
maxDiffULPsForNormFormat: 1,
maxDiffULPsForFloatFormat: 1,
} as TexelCompareOptions
): void {
assert(exp.length > 0, 'must specify at least one pixel comparison');
assert(
(kEncodableTextureFormats as GPUTextureFormat[]).includes(src.texture.format),
() => `${src.texture.format} is not an encodable format`
);
const lowerCorner = [src.texture.width, src.texture.height, src.texture.depthOrArrayLayers];
const upperCorner = [0, 0, 0];
const expMap = new Map<string, E>();
const coords: Required<GPUOrigin3DDict>[] = [];
for (const e of exp) {
const coord = reifyOrigin3D(e.coord);
const coordKey = JSON.stringify(coord);
coords.push(coord);
// Compute the minimum sub-rect that encompasses all the pixel comparisons. The
// `lowerCorner` will become the origin, and the `upperCorner` will be used to compute the
// size.
lowerCorner[0] = Math.min(lowerCorner[0], coord.x);
lowerCorner[1] = Math.min(lowerCorner[1], coord.y);
lowerCorner[2] = Math.min(lowerCorner[2], coord.z);
upperCorner[0] = Math.max(upperCorner[0], coord.x);
upperCorner[1] = Math.max(upperCorner[1], coord.y);
upperCorner[2] = Math.max(upperCorner[2], coord.z);
// Build a sparse map of the coordinates to the expected colors for the texel view.
assert(
!expMap.has(coordKey),
() => `duplicate pixel expectation at coordinate (${coord.x},${coord.y},${coord.z})`
);
expMap.set(coordKey, e.exp);
}
const size: GPUExtent3D = [
upperCorner[0] - lowerCorner[0] + 1,
upperCorner[1] - lowerCorner[1] + 1,
upperCorner[2] - lowerCorner[2] + 1,
];
let expTexelView: TexelView;
if (Symbol.iterator in exp[0].exp) {
expTexelView = TexelView.fromTexelsAsBytes(
src.texture.format as EncodableTextureFormat,
coord => {
const res = expMap.get(JSON.stringify(coord));
assert(
res !== undefined,
() => `invalid coordinate (${coord.x},${coord.y},${coord.z}) in sparse texel view`
);
return res as Uint8Array;
}
);
} else {
expTexelView = TexelView.fromTexelsAsColors(
src.texture.format as EncodableTextureFormat,
coord => {
const res = expMap.get(JSON.stringify(coord));
assert(
res !== undefined,
() => `invalid coordinate (${coord.x},${coord.y},${coord.z}) in sparse texel view`
);
return res as PerTexelComponent<number>;
}
);
}
const coordsF = (function* () {
for (const coord of coords) {
yield coord;
}
})();
t.eventualExpectOK(
textureContentIsOKByT2B(
t,
{ ...src, origin: reifyOrigin3D(lowerCorner) },
size,
{ expTexelView },
comparisonOptions,
coordsF
)
);
}
export function expectTexturesToMatchByRendering(
t: GPUTest,
actualTexture: GPUTexture,
expectedTexture: GPUTexture,
mipLevel: number,
origin: Required<GPUOrigin3DDict>,
size: Required<GPUExtent3DDict>
): void {
// Render every layer of both textures at mipLevel to an rgba8unorm texture
// that matches the size of the mipLevel. After each render, copy the
// result to a buffer and expect the results from both textures to match.
const { pipelineType, pipeline } = getPipelineToRenderTextureToRGB8UnormTexture(
t.device,
actualTexture
);
const readbackPromisesPerTexturePerLayer = [actualTexture, expectedTexture].map(
(texture, ndx) => {
const attachmentSize = virtualMipSize(
actualTexture.dimension,
[texture.width, texture.height, 1],
mipLevel
);
const attachment = t.createTextureTracked({
label: `readback${ndx}`,
size: attachmentSize,
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const sampler = t.device.createSampler();
const numLayers = texture.depthOrArrayLayers;
const readbackPromisesPerLayer = [];
const uniformBuffer = t.createBufferTracked({
label: 'expectTexturesToMatchByRendering:uniformBuffer',
size: 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
for (let layer = 0; layer < numLayers; ++layer) {
const viewDescriptor: GPUTextureViewDescriptor = {
baseMipLevel: mipLevel,
mipLevelCount: 1,
dimension: pipelineType,
};
const bindGroup = t.device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{
binding: 1,
resource: texture.createView(viewDescriptor),
},
{
binding: 2,
resource: { buffer: uniformBuffer },
},
],
});
t.device.queue.writeBuffer(uniformBuffer, 0, new Uint32Array([layer]));
const encoder = t.device.createCommandEncoder({
label: 'expectTexturesToMatchByRendering',
});
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: attachment.createView(),
clearValue: [0.5, 0.5, 0.5, 0.5],
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
t.queue.submit([encoder.finish()]);
const buffer = copyWholeTextureToNewBufferSimple(t, attachment, 0);
readbackPromisesPerLayer.push(
t.readGPUBufferRangeTyped(buffer, {
type: Uint8Array,
typedLength: buffer.size,
})
);
}
return readbackPromisesPerLayer;
}
);
t.eventualAsyncExpectation(async niceStack => {
const readbacksPerTexturePerLayer = [];
// Wait for all buffers to be ready
for (const readbackPromises of readbackPromisesPerTexturePerLayer) {
readbacksPerTexturePerLayer.push(await Promise.all(readbackPromises));
}
function arrayNotAllTheSameValue(arr: TypedArrayBufferView | number[], msg?: string) {
const first = arr[0];
return arr.length <= 1 || arr.findIndex(v => v !== first) >= 0
? undefined
: Error(`array is entirely ${first} so likely nothing was tested: ${msg || ''}`);
}
// Compare each layer of each texture as read from buffer.
const [actualReadbacksPerLayer, expectedReadbacksPerLayer] = readbacksPerTexturePerLayer;
for (let layer = 0; layer < actualReadbacksPerLayer.length; ++layer) {
const actualReadback = actualReadbacksPerLayer[layer];
const expectedReadback = expectedReadbacksPerLayer[layer];
const sameOk =
size.width === 0 ||
size.height === 0 ||
layer < origin.z ||
layer >= origin.z + size.depthOrArrayLayers;
t.expectOK(
sameOk ? undefined : arrayNotAllTheSameValue(actualReadback.data, 'actualTexture')
);
t.expectOK(
sameOk ? undefined : arrayNotAllTheSameValue(expectedReadback.data, 'expectedTexture')
);
t.expectOK(checkElementsEqual(actualReadback.data, expectedReadback.data), {
mode: 'fail',
niceStack,
});
actualReadback.cleanup();
expectedReadback.cleanup();
}
});
}
/**
* Expect an entire GPUTexture to have a single color at the given mip level (defaults to 0).
* MAINTENANCE_TODO: Remove this and/or replace it with a helper in TextureTestMixin.
*/
export function expectSingleColorWithTolerance(
t: GPUTest,
src: GPUTexture,
format: GPUTextureFormat,
{
size,
exp,
dimension = '2d',
slice = 0,
layout,
maxFractionalDiff,
}: {
size: [number, number, number];
exp: PerTexelComponent<number>;
dimension?: GPUTextureDimension;
slice?: number;
layout?: TextureLayoutOptions;
maxFractionalDiff?: number;
}
): void {
assert(slice === 0 || dimension === '2d', 'texture slices are only implemented for 2d textures');
format = resolvePerAspectFormat(format, layout?.aspect);
const { mipSize } = getTextureCopyLayout(format, dimension, size, layout);
// MAINTENANCE_TODO: getTextureCopyLayout does not return the proper size for array textures,
// i.e. it will leave the z/depth value as is instead of making it 1 when dealing with 2d
// texture arrays. Since we are passing in the dimension, we should update it to return the
// corrected size.
const copySize = [
mipSize[0],
dimension !== '1d' ? mipSize[1] : 1,
dimension === '3d' ? mipSize[2] : 1,
];
// Create a TexelView that returns exp for all texels.
const expTexelView = TexelView.fromTexelsAsColors(format as EncodableTextureFormat, () => exp);
const source: GPUTexelCopyTextureInfo = {
texture: src,
mipLevel: layout?.mipLevel ?? 0,
aspect: layout?.aspect ?? 'all',
origin: [0, 0, slice],
};
const comparisonOptions = {
maxFractionalDiff: maxFractionalDiff ?? 0,
};
t.eventualExpectOK(
textureContentIsOKByT2B(t, source, copySize, { expTexelView }, comparisonOptions)
);
}
export function copyWholeTextureToNewBufferSimple(
t: GPUTest,
texture: GPUTexture,
mipLevel: number
) {
const { blockWidth, blockHeight, bytesPerBlock } = getBlockInfoForTextureFormat(texture.format);
const mipSize = physicalMipSizeFromTexture(texture, mipLevel);
assert(bytesPerBlock !== undefined);
const blocksPerRow = mipSize[0] / blockWidth;
const blocksPerColumn = mipSize[1] / blockHeight;
assert(blocksPerRow % 1 === 0);
assert(blocksPerColumn % 1 === 0);
const bytesPerRow = align(blocksPerRow * bytesPerBlock, 256);
const byteLength = bytesPerRow * blocksPerColumn * mipSize[2];
return copyWholeTextureToNewBuffer(
t,
{ texture, mipLevel },
{
bytesPerBlock,
bytesPerRow,
rowsPerImage: blocksPerColumn,
byteLength,
}
);
}
export function copyWholeTextureToNewBuffer(
t: GPUTest,
{ texture, mipLevel }: { texture: GPUTexture; mipLevel: number | undefined },
resultDataLayout: {
bytesPerBlock: number;
byteLength: number;
bytesPerRow: number;
rowsPerImage: number;
}
): GPUBuffer {
const { byteLength, bytesPerRow, rowsPerImage } = resultDataLayout;
const buffer = t.createBufferTracked({
label: 'copyWholeTextureToNewBuffer:buffer',
size: align(byteLength, 4), // this is necessary because we need to copy and map data from this buffer
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const mipSize = physicalMipSizeFromTexture(texture, mipLevel || 0);
const encoder = t.device.createCommandEncoder({ label: 'copyWholeTextureToNewBuffer' });
encoder.copyTextureToBuffer(
{ texture, mipLevel },
{ buffer, bytesPerRow, rowsPerImage },
mipSize
);
t.device.queue.submit([encoder.finish()]);
return buffer;
}
export function updateLinearTextureDataSubBox(
t: GPUTest,
format: ColorTextureFormat,
copySize: Required<GPUExtent3DDict>,
copyParams: {
dest: LinearCopyParameters;
src: LinearCopyParameters;
}
): void {
const { src, dest } = copyParams;
const rowLength = bytesInACompleteRow(copySize.width, format);
for (const texel of iterateBlockRows(copySize, format)) {
const srcOffsetElements = getTexelOffsetInBytes(src.dataLayout, format, texel, src.origin);
const dstOffsetElements = getTexelOffsetInBytes(dest.dataLayout, format, texel, dest.origin);
memcpy(
{ src: src.data, start: srcOffsetElements, length: rowLength },
{ dst: dest.data, start: dstOffsetElements }
);
}
}
/** Offset for a particular texel in the linear texture data */
export function getTexelOffsetInBytes(
textureDataLayout: Required<GPUTexelCopyBufferLayout>,
format: ColorTextureFormat,
texel: Required<GPUOrigin3DDict>,
origin: Required<GPUOrigin3DDict> = { x: 0, y: 0, z: 0 }
): number {
const { offset, bytesPerRow, rowsPerImage } = textureDataLayout;
const info = getBlockInfoForColorTextureFormat(format);
assert(texel.x % info.blockWidth === 0);
assert(texel.y % info.blockHeight === 0);
assert(origin.x % info.blockWidth === 0);
assert(origin.y % info.blockHeight === 0);
const bytesPerImage = rowsPerImage * bytesPerRow;
return (
offset +
(texel.z + origin.z) * bytesPerImage +
((texel.y + origin.y) / info.blockHeight) * bytesPerRow +
((texel.x + origin.x) / info.blockWidth) * info.bytesPerBlock
);
}
export function* iterateBlockRows(
size: Required<GPUExtent3DDict>,
format: ColorTextureFormat
): Generator<Required<GPUOrigin3DDict>> {
if (size.width === 0 || size.height === 0 || size.depthOrArrayLayers === 0) {
// do not iterate anything for an empty region
return;
}
const info = getBlockInfoForTextureFormat(format);
assert(size.height % info.blockHeight === 0);
// Note: it's important that the order is in increasing memory address order.
for (let z = 0; z < size.depthOrArrayLayers; ++z) {
for (let y = 0; y < size.height; y += info.blockHeight) {
yield {
x: 0,
y,
z,
};
}
}
}