export const description = `writeTexture + copyBufferToTexture + copyTextureToBuffer operation tests.
* copy_with_various_rows_per_image_and_bytes_per_row: test that copying data with various bytesPerRow (including { ==, > } bytesInACompleteRow) and\
rowsPerImage (including { ==, > } copyExtent.height) values and minimum required bytes in copy works for every format. Also covers special code paths:
- bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
- when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1: copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
- bytesPerRow == bytesInACompleteCopyImage
* copy_with_various_offsets_and_data_sizes: test that copying data with various offset (including { ==, > } 0 and is/isn't power of 2) values and additional\
data paddings works for every format with 2d and 2d-array textures. Also covers special code paths:
- offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
- offset > bytesInACompleteCopyImage
* copy_with_various_origins_and_copy_extents: test that copying slices of a texture works with various origin (including { origin.x, origin.y, origin.z }\
{ ==, > } 0 and is/isn't power of 2) and copyExtent (including { copyExtent.x, copyExtent.y, copyExtent.z } { ==, > } 0 and is/isn't power of 2) values\
(also including {origin._ + copyExtent._ { ==, < } the subresource size of textureCopyView) works for all formats. origin and copyExtent values are passed\
as [number, number, number] instead of GPUExtent3DDict.
* copy_various_mip_levels: test that copying various mip levels works for all formats. Also covers special code paths:
- the physical size of the subresource is not equal to the logical size
- bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers and copyExtent needs to be clamped
* copy_with_no_image_or_slice_padding_and_undefined_values: test that when copying a single row we can set any bytesPerRow value and when copying a single\
slice we can set rowsPerImage to 0. Also test setting offset, rowsPerImage, mipLevel, origin, origin.{x,y,z} to undefined.
Note: more coverage of memory synchronization for different read and write texture methods are in same_subresource.spec.ts.
* TODO:
- test copyT2B with buffer size not divisible by 4 (not done because expectContents 4-byte alignment)
- Convert the float32 values in initialData into the ones compatible to the depth aspect of
depthFormats when depth16unorm is supported by the browsers in
DoCopyTextureToBufferWithDepthAspectTest().
- add more cases for textureBindingViewDimension: 'cube'
- test compressed textures
- test more cases in general - refactor tests generate 'cube' compatible texture sizes.
TODO: Expand tests of GPUExtent3D [1]
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import {
assert,
ErrorWithExtra,
memcpy,
TypedArrayBufferView,
unreachable,
} from '../../../../common/util/util.js';
import { kBufferSizeAlignment, kTextureDimensions } from '../../../capability_info.js';
import {
kDepthStencilFormats,
kColorTextureFormats,
depthStencilBufferTextureCopySupported,
textureFormatAndDimensionPossiblyCompatible,
depthStencilFormatAspectSize,
DepthStencilFormat,
ColorTextureFormat,
RegularTextureFormat,
isCompressedTextureFormat,
isDepthTextureFormat,
isStencilTextureFormat,
getBlockInfoForTextureFormat,
getBlockInfoForColorTextureFormat,
canCopyToAllAspectsOfTextureFormat,
canCopyFromAllAspectsOfTextureFormat,
getMaxValidTextureSizeForFormatAndDimension,
} from '../../../format_info.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as ttu from '../../../texture_test_utils.js';
import { checkElementsEqual } from '../../../util/check_contents.js';
import { align } from '../../../util/math.js';
import { physicalMipSizeFromTexture } from '../../../util/texture/base.js';
import { DataArrayGenerator } from '../../../util/texture/data_generation.js';
import {
bytesInACompleteRow,
dataBytesForCopyOrFail,
getTextureCopyLayout,
kBytesPerRowAlignment,
TextureCopyLayout,
} from '../../../util/texture/layout.js';
import { TexelView } from '../../../util/texture/texel_view.js';
import { findFailedPixels } from '../../../util/texture/texture_ok.js';
import { reifyExtent3D } from '../../../util/unions.js';
interface TextureCopyViewWithRequiredOrigin {
texture: GPUTexture;
mipLevel: number | undefined;
origin: Required<GPUOrigin3DDict>;
}
type InitMethod = 'WriteTexture' | 'CopyB2T';
* - PartialCopyT2B: do CopyT2B to check that the part of the texture we copied to with InitMethod
* matches the data we were copying and that we don't overwrite any data in the target buffer that
* we're not supposed to - that's primarily for testing CopyT2B functionality.
* - FullCopyT2B: do CopyT2B on the whole texture and check whether the part we copied to matches
* the data we were copying and that the nothing else was modified - that's primarily for testing
* WriteTexture and CopyB2T.
*
* Note: in compatibility mode, copyTextureToBuffer is not supported for compressed textures.
* In this case, we render the texture as well as a texture with the contents we except in the
* copy and then expect the rendered results to match.
*/
type CheckMethod = 'PartialCopyT2B' | 'FullCopyT2B';
* This describes in what form the arguments will be passed to WriteTexture/CopyB2T/CopyT2B. If
* undefined, then default values are passed as undefined instead of default values. If arrays, then
* `GPUOrigin3D` and `GPUExtent3D` are passed as `[number, number, number]`. *
*
* [1]: Try to expand this with something like:
* ```ts
* function encodeExtent3D(
* mode: 'partial-array' | 'full-array' | 'extra-array' | 'partial-dict' | 'full-dict',
* value: GPUExtent3D
* ): GPUExtent3D { ... }
* ```
*/
type ChangeBeforePass = 'none' | 'undefined' | 'arrays';
const kMethodsToTest = [
{ initMethod: 'WriteTexture', checkMethod: 'FullCopyT2B' },
{ initMethod: 'CopyB2T', checkMethod: 'FullCopyT2B' },
{ initMethod: 'WriteTexture', checkMethod: 'PartialCopyT2B' },
] as const;
const dataGenerator = new DataArrayGenerator();
const altDataGenerator = new DataArrayGenerator();
function applyTextureBindingViewDimensionForTest(descriptor: GPUTextureDescriptor) {
const size = reifyExtent3D(descriptor.size);
if (
descriptor.textureBindingViewDimension === undefined &&
descriptor.dimension === '2d' &&
size.width === size.height &&
size.depthOrArrayLayers === 6 &&
!isCompressedTextureFormat(descriptor.format)
) {
descriptor.textureBindingViewDimension = 'cube';
}
return descriptor;
}
class ImageCopyTest extends AllFeaturesMaxLimitsGPUTest {
* This is used for testing passing undefined members of `GPUTexelCopyBufferLayout` instead of actual
* values where possible. Passing arguments as values and not as objects so that they are passed
* by copy and not by reference.
*/
undefDataLayoutIfNeeded(
offset: number | undefined,
rowsPerImage: number | undefined,
bytesPerRow: number | undefined,
changeBeforePass: ChangeBeforePass
): GPUTexelCopyBufferLayout {
if (changeBeforePass === 'undefined') {
if (offset === 0) {
offset = undefined;
}
if (bytesPerRow === 0) {
bytesPerRow = undefined;
}
if (rowsPerImage === 0) {
rowsPerImage = undefined;
}
}
return { offset, bytesPerRow, rowsPerImage };
}
* This is used for testing passing undefined members of `GPUTexelCopyTextureInfo` instead of actual
* values where possible and also for testing passing the origin as `[number, number, number]`.
* Passing arguments as values and not as objects so that they are passed by copy and not by
* reference.
*/
undefOrArrayCopyViewIfNeeded(
texture: GPUTexture,
origin_x: number | undefined,
origin_y: number | undefined,
origin_z: number | undefined,
mipLevel: number | undefined,
changeBeforePass: ChangeBeforePass
): GPUTexelCopyTextureInfo {
let origin: GPUOrigin3D | undefined = { x: origin_x, y: origin_y, z: origin_z };
if (changeBeforePass === 'undefined') {
if (origin_x === 0 && origin_y === 0 && origin_z === 0) {
origin = undefined;
} else {
if (origin_x === 0) {
origin_x = undefined;
}
if (origin_y === 0) {
origin_y = undefined;
}
if (origin_z === 0) {
origin_z = undefined;
}
origin = { x: origin_x, y: origin_y, z: origin_z };
}
if (mipLevel === 0) {
mipLevel = undefined;
}
}
if (changeBeforePass === 'arrays') {
origin = [origin_x!, origin_y!, origin_z!];
}
return { texture, origin, mipLevel };
}
* This is used for testing passing `GPUExtent3D` as `[number, number, number]` instead of
* `GPUExtent3DDict`. Passing arguments as values and not as objects so that they are passed by
* copy and not by reference.
*/
arrayCopySizeIfNeeded(
width: number,
height: number,
depthOrArrayLayers: number,
changeBeforePass: ChangeBeforePass
): GPUExtent3D {
if (changeBeforePass === 'arrays') {
return [width, height, depthOrArrayLayers];
} else {
return { width, height, depthOrArrayLayers };
}
}
* Compares data in `expected` to data in `buffer.
* Areas defined by size and dataLayout are compared by interpreting the data as appropriate
* for the texture format. As an example, with 'rgb9e5ufloat' multiple values can
* represent the same number. For example, double the exponent and halving the
* mantissa. Areas outside the area defined by size and dataLayout are expected to match
* by binary comparison.
*/
expectGPUBufferValuesEqualWhenInterpretedAsTextureFormat(
expected: Uint8Array,
buffer: GPUBuffer,
format: ColorTextureFormat,
size: Required<GPUExtent3DDict>,
dataLayout: Required<GPUTexelCopyBufferLayout>
) {
if (isCompressedTextureFormat(format)) {
this.expectGPUBufferValuesEqual(buffer, expected);
return;
}
const regularFormat = format as RegularTextureFormat;
const checkByTextureFormat = (actual: Uint8Array) => {
const zero = { x: 0, y: 0, z: 0 };
{
const actTexelView = TexelView.fromTextureDataByReference(regularFormat, actual, {
bytesPerRow: dataLayout.bytesPerRow,
rowsPerImage: dataLayout.rowsPerImage,
subrectOrigin: [0, 0, 0],
subrectSize: size,
});
const expTexelView = TexelView.fromTextureDataByReference(regularFormat, expected, {
bytesPerRow: dataLayout.bytesPerRow,
rowsPerImage: dataLayout.rowsPerImage,
subrectOrigin: [0, 0, 0],
subrectSize: size,
});
const failedPixelsMessage = findFailedPixels(
regularFormat,
zero,
size,
{ actTexelView, expTexelView },
{
maxFractionalDiff: 0,
}
);
if (failedPixelsMessage !== undefined) {
const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage;
return new ErrorWithExtra(msg, () => ({
expTexelView,
actTexelView,
}));
}
}
{
const rowLength = bytesInACompleteRow(size.width, format);
let lastOffset = 0;
for (const texel of ttu.iterateBlockRows(size, format)) {
const offset = ttu.getTexelOffsetInBytes(dataLayout, format, texel, zero);
const actualPart = actual.subarray(lastOffset, offset);
const expectedPart = expected.subarray(lastOffset, offset);
const error = checkElementsEqual(actualPart, expectedPart);
if (error) {
return error;
}
assert(offset >= lastOffset);
lastOffset = offset + rowLength;
}
{
const actualPart = actual.subarray(lastOffset, actual.length);
const expectedPart = expected.subarray(lastOffset, expected.length);
return checkElementsEqual(actualPart, expectedPart);
}
}
};
this.expectGPUBufferValuesPassCheck(buffer, checkByTextureFormat, {
srcByteOffset: 0,
type: Uint8Array,
typedLength: expected.length,
method: 'copy',
mode: 'fail',
});
}
copyTextureToBufferWithAppliedArguments(
buffer: GPUBuffer,
{ offset, rowsPerImage, bytesPerRow }: Required<GPUTexelCopyBufferLayout>,
{ width, height, depthOrArrayLayers }: Required<GPUExtent3DDict>,
{ texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
changeBeforePass: ChangeBeforePass
): void {
const { x, y, z } = origin;
const appliedCopyView = this.undefOrArrayCopyViewIfNeeded(
texture,
x,
y,
z,
mipLevel,
changeBeforePass
);
const appliedDataLayout = this.undefDataLayoutIfNeeded(
offset,
rowsPerImage,
bytesPerRow,
changeBeforePass
);
const appliedCheckSize = this.arrayCopySizeIfNeeded(
width,
height,
depthOrArrayLayers,
changeBeforePass
);
const encoder = this.device.createCommandEncoder();
encoder.copyTextureToBuffer(
appliedCopyView,
{ buffer, ...appliedDataLayout },
appliedCheckSize
);
this.device.queue.submit([encoder.finish()]);
}
uploadLinearTextureDataToTextureSubBox(
textureCopyView: TextureCopyViewWithRequiredOrigin,
textureDataLayout: GPUTexelCopyBufferLayout & { bytesPerRow: number },
copySize: Required<GPUExtent3DDict>,
partialData: Uint8Array,
method: InitMethod,
changeBeforePass: ChangeBeforePass
): void {
const { texture, mipLevel, origin } = textureCopyView;
const { offset, rowsPerImage, bytesPerRow } = textureDataLayout;
const { x, y, z } = origin;
const { width, height, depthOrArrayLayers } = copySize;
const appliedCopyView = this.undefOrArrayCopyViewIfNeeded(
texture,
x,
y,
z,
mipLevel,
changeBeforePass
);
const appliedDataLayout = this.undefDataLayoutIfNeeded(
offset,
rowsPerImage,
bytesPerRow,
changeBeforePass
);
const appliedCopySize = this.arrayCopySizeIfNeeded(
width,
height,
depthOrArrayLayers,
changeBeforePass
);
switch (method) {
case 'WriteTexture': {
this.device.queue.writeTexture(
appliedCopyView,
partialData,
appliedDataLayout,
appliedCopySize
);
break;
}
case 'CopyB2T': {
const buffer = this.makeBufferWithContents(partialData, GPUBufferUsage.COPY_SRC);
const encoder = this.device.createCommandEncoder();
encoder.copyBufferToTexture(
{ buffer, ...appliedDataLayout },
appliedCopyView,
appliedCopySize
);
this.device.queue.submit([encoder.finish()]);
break;
}
default:
unreachable();
}
}
generateMatchingTextureInJSRenderAndCompareContents(
{
texture: actualTexture,
mipLevel: mipLevelOrUndefined,
origin,
}: TextureCopyViewWithRequiredOrigin,
copySize: Required<GPUExtent3DDict>,
format: ColorTextureFormat,
expected: Uint8Array,
expectedDataLayout: Required<GPUTexelCopyBufferLayout>
): void {
const size = [
actualTexture.width,
actualTexture.height,
actualTexture.depthOrArrayLayers,
] as const;
const expectedTexture = this.createTextureTracked({
label: 'expectedTexture',
size,
dimension: actualTexture.dimension,
format,
mipLevelCount: actualTexture.mipLevelCount,
usage: actualTexture.usage,
});
const mipLevel = mipLevelOrUndefined || 0;
const fullMipLevelTextureCopyLayout = getTextureCopyLayout(
format,
actualTexture.dimension,
size,
{
mipLevel,
}
);
const expectedTextureMipLevelData = new Uint8Array(
align(fullMipLevelTextureCopyLayout.byteLength, 4)
);
const mipSize = physicalMipSizeFromTexture(expectedTexture, mipLevel);
ttu.updateLinearTextureDataSubBox(this, format, copySize, {
src: {
dataLayout: expectedDataLayout,
origin: { x: 0, y: 0, z: 0 },
data: expected,
},
dest: {
dataLayout: { offset: 0, ...fullMipLevelTextureCopyLayout },
origin,
data: expectedTextureMipLevelData,
},
});
this.queue.writeTexture(
{ texture: expectedTexture, mipLevel },
expectedTextureMipLevelData,
{ ...fullMipLevelTextureCopyLayout, offset: 0 },
mipSize
);
ttu.expectTexturesToMatchByRendering(
this,
actualTexture,
expectedTexture,
mipLevel,
origin,
copySize
);
}
* We check an appropriate part of the texture against the given data.
* Used directly with PartialCopyT2B check method (for a subpart of the texture)
* and by `copyWholeTextureToBufferAndCheckContentsWithUpdatedData` with FullCopyT2B check method
* (for the whole texture).
*/
copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
checkSize: Required<GPUExtent3DDict>,
format: ColorTextureFormat,
expected: Uint8Array,
expectedDataLayout: Required<GPUTexelCopyBufferLayout>,
changeBeforePass: ChangeBeforePass = 'none'
): void {
const bufferSize = align(expected.byteLength, 4);
const bufferData = altDataGenerator.generateAndCopyView(bufferSize, 17);
const buffer = this.makeBufferWithContents(
bufferData,
GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
);
this.copyTextureToBufferWithAppliedArguments(
buffer,
expectedDataLayout,
checkSize,
{ texture, mipLevel, origin },
changeBeforePass
);
ttu.updateLinearTextureDataSubBox(this, format, checkSize, {
src: {
dataLayout: expectedDataLayout,
origin: { x: 0, y: 0, z: 0 },
data: expected,
},
dest: {
dataLayout: expectedDataLayout,
origin: { x: 0, y: 0, z: 0 },
data: bufferData,
},
});
this.expectGPUBufferValuesEqualWhenInterpretedAsTextureFormat(
bufferData,
buffer,
format,
checkSize,
expectedDataLayout
);
}
* Used for checking whether the whole texture was updated correctly by
* `uploadLinearTextureDataToTextureSubpart`. Takes fullData returned by
* `copyWholeTextureToNewBuffer` before the copy operation which is the original texture data,
* then updates it with `updateLinearTextureDataSubpart` and checks the texture against the
* updated data after the copy operation.
*/
copyWholeTextureToBufferAndCheckContentsWithUpdatedData(
{ texture, mipLevel, origin }: TextureCopyViewWithRequiredOrigin,
fullTextureCopyLayout: TextureCopyLayout,
texturePartialDataLayout: Required<GPUTexelCopyBufferLayout>,
copySize: Required<GPUExtent3DDict>,
format: ColorTextureFormat,
fullData: GPUBuffer,
partialData: Uint8Array
): void {
const { mipSize, bytesPerRow, rowsPerImage, byteLength } = fullTextureCopyLayout;
const readbackPromise = this.readGPUBufferRangeTyped(fullData, {
type: Uint8Array,
typedLength: byteLength,
});
const destinationOrigin = { x: 0, y: 0, z: 0 };
this.eventualAsyncExpectation(async () => {
const readback = await readbackPromise;
ttu.updateLinearTextureDataSubBox(this, format, copySize, {
dest: {
dataLayout: { offset: 0, ...fullTextureCopyLayout },
origin,
data: readback.data,
},
src: {
dataLayout: texturePartialDataLayout,
origin: { x: 0, y: 0, z: 0 },
data: partialData,
},
});
this.copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin: destinationOrigin },
{ width: mipSize[0], height: mipSize[1], depthOrArrayLayers: mipSize[2] },
format,
readback.data,
{ bytesPerRow, rowsPerImage, offset: 0 }
);
readback.cleanup();
});
}
* Tests copy between linear data and texture by creating a texture, putting some data into it
* with WriteTexture/CopyB2T, then getting data for the whole texture/for a part of it back and
* comparing it with the expectation.
*/
uploadTextureAndVerifyCopy({
textureDataLayout,
copySize,
dataSize,
mipLevel = 0,
origin = { x: 0, y: 0, z: 0 },
textureSize,
format,
dimension,
initMethod,
checkMethod,
changeBeforePass = 'none',
}: {
textureDataLayout: Required<GPUTexelCopyBufferLayout>;
copySize: Required<GPUExtent3DDict>;
dataSize: number;
mipLevel?: number;
origin?: Required<GPUOrigin3DDict>;
textureSize: readonly [number, number, number];
format: ColorTextureFormat;
dimension: GPUTextureDimension;
initMethod: InitMethod;
checkMethod: CheckMethod;
changeBeforePass?: ChangeBeforePass;
}): void {
const texture = this.createTextureTracked(
applyTextureBindingViewDimensionForTest({
size: textureSize as [number, number, number],
format,
dimension,
mipLevelCount: mipLevel + 1,
usage:
GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
})
);
const data = dataGenerator.generateView(dataSize);
switch (checkMethod) {
case 'PartialCopyT2B': {
this.uploadLinearTextureDataToTextureSubBox(
{ texture, mipLevel, origin },
textureDataLayout,
copySize,
data,
initMethod,
changeBeforePass
);
if (this.canCallCopyTextureToBufferWithTextureFormat(texture.format)) {
this.copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin },
copySize,
format,
data,
textureDataLayout,
changeBeforePass
);
} else {
this.generateMatchingTextureInJSRenderAndCompareContents(
{ texture, mipLevel, origin },
copySize,
format,
data,
textureDataLayout
);
}
break;
}
case 'FullCopyT2B': {
this.uploadLinearTextureDataToTextureSubBox(
{ texture, mipLevel, origin },
textureDataLayout,
copySize,
data,
initMethod,
changeBeforePass
);
if (this.canCallCopyTextureToBufferWithTextureFormat(texture.format)) {
const fullTextureCopyLayout = getTextureCopyLayout(format, dimension, textureSize, {
mipLevel,
});
const fullData = ttu.copyWholeTextureToNewBuffer(
this,
{ texture, mipLevel },
fullTextureCopyLayout
);
this.copyWholeTextureToBufferAndCheckContentsWithUpdatedData(
{ texture, mipLevel, origin },
fullTextureCopyLayout,
textureDataLayout,
copySize,
format,
fullData,
data
);
} else {
this.generateMatchingTextureInJSRenderAndCompareContents(
{ texture, mipLevel, origin },
copySize,
format,
data,
textureDataLayout
);
}
break;
}
default:
unreachable();
}
}
doUploadToStencilTest(
format: DepthStencilFormat,
textureSize: readonly [number, number, number],
uploadMethod: 'WriteTexture' | 'CopyB2T',
bytesPerRow: number,
rowsPerImage: number,
initialDataSize: number,
initialDataOffset: number,
mipLevel: number
): void {
const srcTexture = this.createTextureTracked({
size: textureSize,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
format,
mipLevelCount: mipLevel + 1,
});
const copySize = [textureSize[0] >> mipLevel, textureSize[1] >> mipLevel, textureSize[2]];
const initialData = dataGenerator.generateView(
align(initialDataSize, kBufferSizeAlignment),
0,
initialDataOffset
);
switch (uploadMethod) {
case 'WriteTexture':
this.queue.writeTexture(
{ texture: srcTexture, aspect: 'stencil-only', mipLevel },
initialData,
{
offset: initialDataOffset,
bytesPerRow,
rowsPerImage,
},
copySize
);
break;
case 'CopyB2T':
{
const stagingBuffer = this.makeBufferWithContents(initialData, GPUBufferUsage.COPY_SRC);
const encoder = this.device.createCommandEncoder();
encoder.copyBufferToTexture(
{ buffer: stagingBuffer, offset: initialDataOffset, bytesPerRow, rowsPerImage },
{ texture: srcTexture, aspect: 'stencil-only', mipLevel },
copySize
);
this.queue.submit([encoder.finish()]);
}
break;
default:
unreachable();
}
const outputBytesPerRow = align(bytesPerRow, kBytesPerRowAlignment);
const outputBufferSize = align(
dataBytesForCopyOrFail({
layout: { bytesPerRow: outputBytesPerRow, rowsPerImage },
format: 'stencil8',
copySize,
method: uploadMethod,
}),
kBufferSizeAlignment
);
const outputBuffer = this.createBufferTracked({
size: outputBufferSize,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const encoder = this.device.createCommandEncoder();
encoder.copyTextureToBuffer(
{ texture: srcTexture, aspect: 'stencil-only', mipLevel },
{ buffer: outputBuffer, bytesPerRow: outputBytesPerRow, rowsPerImage },
copySize
);
this.queue.submit([encoder.finish()]);
const expectedData = new Uint8Array(outputBufferSize);
for (let z = 0; z < copySize[2]; ++z) {
const baseExpectedOffset = z * outputBytesPerRow * rowsPerImage;
const baseInitialDataOffset = initialDataOffset + z * bytesPerRow * rowsPerImage;
for (let y = 0; y < copySize[1]; ++y) {
memcpy(
{
src: initialData,
start: baseInitialDataOffset + y * bytesPerRow,
length: copySize[0],
},
{ dst: expectedData, start: baseExpectedOffset + y * outputBytesPerRow }
);
}
}
this.expectGPUBufferValuesEqual(outputBuffer, expectedData);
}
doCopyFromStencilTest(
format: DepthStencilFormat,
textureSize: readonly [number, number, number],
bytesPerRow: number,
rowsPerImage: number,
offset: number,
mipLevel: number
): void {
const srcTexture = this.createTextureTracked({
size: textureSize,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
format,
mipLevelCount: mipLevel + 1,
});
const copySize = [textureSize[0] >> mipLevel, textureSize[1] >> mipLevel, textureSize[2]];
const initialData = dataGenerator.generateView(
align(copySize[0] * copySize[1] * copySize[2], kBufferSizeAlignment)
);
this.queue.writeTexture(
{ texture: srcTexture, aspect: 'stencil-only', mipLevel },
initialData,
{ bytesPerRow: copySize[0], rowsPerImage: copySize[1] },
copySize
);
const outputBufferSize = align(
offset +
dataBytesForCopyOrFail({
layout: { bytesPerRow, rowsPerImage },
format: 'stencil8',
copySize,
method: 'CopyT2B',
}),
kBufferSizeAlignment
);
const outputBuffer = this.createBufferTracked({
size: outputBufferSize,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
const encoder = this.device.createCommandEncoder();
encoder.copyTextureToBuffer(
{ texture: srcTexture, aspect: 'stencil-only', mipLevel },
{ buffer: outputBuffer, offset, bytesPerRow, rowsPerImage },
copySize
);
this.queue.submit([encoder.finish()]);
const expectedData = new Uint8Array(outputBufferSize);
for (let z = 0; z < copySize[2]; ++z) {
const baseExpectedOffset = offset + z * bytesPerRow * rowsPerImage;
const baseInitialDataOffset = z * copySize[0] * copySize[1];
for (let y = 0; y < copySize[1]; ++y) {
memcpy(
{
src: initialData,
start: baseInitialDataOffset + y * copySize[0],
length: copySize[0],
},
{ dst: expectedData, start: baseExpectedOffset + y * bytesPerRow }
);
}
}
this.expectGPUBufferValuesEqual(outputBuffer, expectedData);
}
initializeDepthAspectWithRendering(
depthTexture: GPUTexture,
depthFormat: GPUTextureFormat,
copySize: readonly [number, number, number],
copyMipLevel: number,
initialData: Float32Array
): void {
assert(isDepthTextureFormat(depthFormat));
const inputTexture = this.createTextureTracked({
size: copySize,
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
format: 'r32float',
});
this.queue.writeTexture(
{ texture: inputTexture },
initialData,
{
bytesPerRow: copySize[0] * 4,
rowsPerImage: copySize[1],
},
copySize
);
const use2DArray = this.isCompatibility && inputTexture.depthOrArrayLayers > 1;
const [textureType, layerCode] = use2DArray
? ['texture_2d_array', ', baseArrayLayer']
: ['texture_2d', ''];
const renderPipeline = this.device.createRenderPipeline({
layout: 'auto',
vertex: {
module: this.device.createShaderModule({
code: `
@vertex
fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> {
var pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>( 1.0, -1.0));
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}`,
}),
entryPoint: 'main',
},
fragment: {
module: this.device.createShaderModule({
code: `
@group(0) @binding(0) var inputTexture: ${textureType}<f32>;
@group(0) @binding(1) var<uniform> baseArrayLayer: u32;
@fragment fn main(@builtin(position) fragcoord : vec4<f32>) ->
@builtin(frag_depth) f32 {
var depthValue : vec4<f32> = textureLoad(inputTexture, vec2<i32>(fragcoord.xy)${layerCode}, 0);
return depthValue.x;
}`,
}),
entryPoint: 'main',
targets: [],
},
primitive: {
topology: 'triangle-list',
},
depthStencil: {
format: depthFormat,
depthWriteEnabled: true,
depthCompare: 'always',
},
});
const encoder = this.device.createCommandEncoder();
for (let z = 0; z < copySize[2]; ++z) {
const depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
view: depthTexture.createView({
dimension: '2d',
baseArrayLayer: z,
arrayLayerCount: 1,
baseMipLevel: copyMipLevel,
mipLevelCount: 1,
}),
};
if (isDepthTextureFormat(depthFormat)) {
depthStencilAttachment.depthClearValue = 0.0;
depthStencilAttachment.depthLoadOp = 'clear';
depthStencilAttachment.depthStoreOp = 'store';
}
if (isStencilTextureFormat(depthFormat)) {
depthStencilAttachment.stencilLoadOp = 'load';
depthStencilAttachment.stencilStoreOp = 'store';
}
const renderPass = encoder.beginRenderPass({
colorAttachments: [],
depthStencilAttachment,
});
renderPass.setPipeline(renderPipeline);
const uniformBufferEntry = use2DArray
? [this.createUniformBufferAndBindGroupEntryForBaseArrayLayer(z)]
: [];
const bindGroup = this.device.createBindGroup({
layout: renderPipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: inputTexture.createView({
dimension: use2DArray ? '2d-array' : '2d',
...(!use2DArray && {
baseArrayLayer: z,
arrayLayerCount: 1,
}),
baseMipLevel: 0,
mipLevelCount: 1,
}),
},
...uniformBufferEntry,
],
});
renderPass.setBindGroup(0, bindGroup);
renderPass.draw(6);
renderPass.end();
}
this.queue.submit([encoder.finish()]);
}
createUniformBufferAndBindGroupEntryForBaseArrayLayer(z: number) {
const buffer = this.createBufferTracked({
usage: GPUBufferUsage.UNIFORM,
size: 4,
mappedAtCreation: true,
});
new Uint32Array(buffer.getMappedRange()).set([z]);
buffer.unmap();
return {
binding: 1,
resource: {
buffer,
},
};
}
doCopyTextureToBufferWithDepthAspectTest(
format: DepthStencilFormat,
copySize: readonly [number, number, number],
bytesPerRowPadding: number,
rowsPerImagePadding: number,
offset: number,
dataPaddingInBytes: number,
mipLevel: number
): void {
const initialData = new Float32Array(copySize[0] * copySize[1] * copySize[2]);
for (let i = 0; i < initialData.length; ++i) {
const baseValue = 0.05 * i;
initialData[i] = i % 40 === 0 ? 1 : baseValue - Math.floor(baseValue);
assert(initialData[i] >= 0 && initialData[i] <= 1);
}
let formatInitialData: TypedArrayBufferView = initialData;
if (format === 'depth16unorm') {
const u16Data = new Uint16Array(initialData.length);
for (let i = 0; i < initialData.length; i++) {
u16Data[i] = initialData[i] * 65535;
initialData[i] = u16Data[i] / 65535.0;
}
formatInitialData = u16Data;
}
const depthTexture = this.createTextureTracked({
format,
size: [copySize[0] << mipLevel, copySize[1] << mipLevel, copySize[2]] as const,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
mipLevelCount: mipLevel + 1,
});
this.initializeDepthAspectWithRendering(depthTexture, format, copySize, mipLevel, initialData);
const aspectBytesPerBlock = depthStencilFormatAspectSize(format, 'depth-only');
const bytesPerRow =
align(aspectBytesPerBlock * copySize[0], kBytesPerRowAlignment) +
bytesPerRowPadding * kBytesPerRowAlignment;
const rowsPerImage = copySize[1] + rowsPerImagePadding;
const destinationBufferSize = align(
bytesPerRow * rowsPerImage * copySize[2] +
bytesPerRow * (copySize[1] - 1) +
aspectBytesPerBlock * copySize[0] +
offset +
dataPaddingInBytes,
kBufferSizeAlignment
);
const destinationBuffer = this.createBufferTracked({
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
size: destinationBufferSize,
});
const copyEncoder = this.device.createCommandEncoder();
copyEncoder.copyTextureToBuffer(
{
texture: depthTexture,
mipLevel,
aspect: 'depth-only',
},
{
buffer: destinationBuffer,
offset,
bytesPerRow,
rowsPerImage,
},
copySize
);
this.queue.submit([copyEncoder.finish()]);
const expectedData = new Uint8Array(destinationBufferSize);
for (let z = 0; z < copySize[2]; ++z) {
const baseExpectedOffset = z * bytesPerRow * rowsPerImage + offset;
const baseInitialDataOffset = z * copySize[0] * copySize[1];
for (let y = 0; y < copySize[1]; ++y) {
memcpy(
{
src: formatInitialData,
start: baseInitialDataOffset + y * copySize[0],
length: copySize[0],
},
{ dst: expectedData, start: baseExpectedOffset + y * bytesPerRow }
);
}
}
this.expectGPUBufferValuesEqual(destinationBuffer, expectedData);
}
}
* This is a helper function used for filtering test parameters
*/
function formatCanBeTested({ format }: { format: ColorTextureFormat }): boolean {
return canCopyToAllAspectsOfTextureFormat(format) && canCopyFromAllAspectsOfTextureFormat(format);
}
export const g = makeTestGroup(ImageCopyTest);
const kRowsPerImageAndBytesPerRowParams = {
paddings: [
{ bytesPerRowPadding: 0, rowsPerImagePadding: 0 },
{ bytesPerRowPadding: 0, rowsPerImagePadding: 6 },
{ bytesPerRowPadding: 6, rowsPerImagePadding: 0 },
{ bytesPerRowPadding: 15, rowsPerImagePadding: 17 },
],
copySizes: [
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 2 },
{ copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0 },
{ copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2 },
{ copyWidthInBlocks: 1, copyHeightInBlocks: 3, copyDepth: 5 },
{ copyWidthInBlocks: 32, copyHeightInBlocks: 1, copyDepth: 8 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1 },
{ copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 3, copyDepth: 1 },
{ copyWidthInBlocks: 4, copyHeightInBlocks: 4, copyDepth: 6 },
],
copySizes1D: [
{ copyWidthInBlocks: 3, copyHeightInBlocks: 1, copyDepth: 1 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 1, copyDepth: 1 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 1 },
{ copyWidthInBlocks: 0, copyHeightInBlocks: 1, copyDepth: 1 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 1, copyDepth: 0 },
],
};
g.test('rowsPerImage_and_bytesPerRow')
.desc(
`Test that copying data with various bytesPerRow and rowsPerImage values and minimum required
bytes in copy works for every format.
Covers a special code path for Metal:
bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
Covers a special code path for D3D12:
when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1: copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
bytesPerRow == bytesInACompleteCopyImage
TODO: Cover the special code paths for 3D textures in D3D12.
`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('format', kColorTextureFormats)
.filter(formatCanBeTested)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combineWithParams(kRowsPerImageAndBytesPerRowParams.paddings)
.expandWithParams(p => {
if (p.dimension === '1d') {
return kRowsPerImageAndBytesPerRowParams.copySizes1D;
}
return kRowsPerImageAndBytesPerRowParams.copySizes;
})
)
.fn(t => {
const {
bytesPerRowPadding,
rowsPerImagePadding,
copyWidthInBlocks,
copyHeightInBlocks,
copyDepth,
format,
dimension,
initMethod,
checkMethod,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForTextureFormat(format);
const maxSize = getMaxValidTextureSizeForFormatAndDimension(t.device, format, dimension);
const bytesPerRowAlignment =
initMethod === 'WriteTexture' && checkMethod === 'FullCopyT2B' ? 1 : 256;
const copyWidth = Math.min(copyWidthInBlocks * info.blockWidth, maxSize[0]);
const copyHeight = Math.min(copyHeightInBlocks * info.blockHeight, maxSize[1]);
const rowsPerImage = copyHeightInBlocks + rowsPerImagePadding;
const bytesPerRow =
align(bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) +
bytesPerRowPadding * bytesPerRowAlignment;
const copySize = { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth };
const dataSize = dataBytesForCopyOrFail({
layout: { offset: 0, bytesPerRow, rowsPerImage },
format,
copySize,
method: initMethod,
});
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
textureSize: [
Math.max(copyWidth, info.blockWidth),
Math.max(copyHeight, info.blockHeight),
Math.max(copyDepth, dimension === '2d' ? 6 : 1),
] ,
format,
dimension,
initMethod,
checkMethod,
});
});
const kOffsetsAndSizesParams = {
offsetsAndPaddings: [
{ offsetInBlocks: 0, dataPaddingInBytes: 0 },
{ offsetInBlocks: 1, dataPaddingInBytes: 0 },
{ offsetInBlocks: 2, dataPaddingInBytes: 0 },
{ offsetInBlocks: 15, dataPaddingInBytes: 0 },
{ offsetInBlocks: 16, dataPaddingInBytes: 0 },
{ offsetInBlocks: 242, dataPaddingInBytes: 0 },
{ offsetInBlocks: 243, dataPaddingInBytes: 0 },
{ offsetInBlocks: 768, dataPaddingInBytes: 0 },
{ offsetInBlocks: 769, dataPaddingInBytes: 0 },
{ offsetInBlocks: 0, dataPaddingInBytes: 1 },
{ offsetInBlocks: 1, dataPaddingInBytes: 8 },
],
copyDepth: [1, 2, 6],
};
g.test('offsets_and_sizes')
.desc(
`Test that copying data with various offset values and additional data paddings
works for every format with 2d and 2d-array textures.
Covers two special code paths for D3D12:
offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
offset > bytesInACompleteCopyImage
Covers spceial cases for OpenGL Compat:
offset % 4 > 0 while:
- padding bytes at end of each row/layer: bytesPerRow % 256 > 0 || rowsPerImage > copyDepth
- rows/layers are compact: bytesPerRow % 256 == 0 && rowsPerImage == copyDepth
- padding bytes at front and end of the same 4-byte word: format == 'r8snorm' && copyWidth <= 2
TODO: Cover the special code paths for 3D textures in D3D12.
TODO: Make a variant for depth-stencil formats.
`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('format', kColorTextureFormats)
.filter(formatCanBeTested)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combineWithParams(kOffsetsAndSizesParams.offsetsAndPaddings)
.combine('copyDepth', kOffsetsAndSizesParams.copyDepth)
.combine('copyWidth', [3, 1, 2, 127, 128, 255, 256] as const)
.filter(({ format, copyWidth }) => {
switch (format) {
case 'r8snorm':
case 'rg8snorm':
return true;
default:
return copyWidth === 3;
}
})
.combine('rowsPerImageEqualsCopyHeight', [true, false] as const)
.unless(p => p.dimension === '1d' && p.copyDepth !== 1)
)
.fn(t => {
const {
offsetInBlocks,
dataPaddingInBytes,
copyDepth,
format,
dimension,
initMethod,
checkMethod,
copyWidth,
rowsPerImageEqualsCopyHeight,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
if (!(t.isCompatibility && (format === 'r8snorm' || format === 'rg8snorm'))) {
if (rowsPerImageEqualsCopyHeight === false) {
t.skip(
'rowsPerImageEqualsCopyHeight === false is only for r8snorm and rg8snorm on compatibility mode'
);
}
if (copyWidth !== 3) {
t.skip('copyWidth !== 3 is only for r8snorm and rg8snorm on compatibility mode');
}
}
const info = getBlockInfoForColorTextureFormat(format);
const offset = offsetInBlocks * info.bytesPerBlock;
const copyHeight = 3;
const copySize = {
width: copyWidth * info.blockWidth,
height: copyHeight * info.blockHeight,
depthOrArrayLayers: copyDepth,
};
let textureHeight = 4 * info.blockHeight;
let rowsPerImage = rowsPerImageEqualsCopyHeight ? copyHeight : copyHeight + 1;
const bytesPerRow = align(copyWidth * info.bytesPerBlock, 256);
if (dimension === '1d') {
copySize.height = 1;
textureHeight = info.blockHeight;
rowsPerImage = 1;
}
const textureSize = [(copyWidth + 1) * info.blockWidth, textureHeight, copyDepth] as const;
const minDataSize = dataBytesForCopyOrFail({
layout: { offset, bytesPerRow, rowsPerImage },
format,
copySize,
method: initMethod,
});
const dataSize = minDataSize + dataPaddingInBytes;
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset, bytesPerRow, rowsPerImage },
copySize,
dataSize,
textureSize,
format,
dimension,
initMethod,
checkMethod,
});
});
g.test('origins_and_extents')
.desc(
`Test that copying slices of a texture works with various origin and copyExtent values
for all formats. We pass origin and copyExtent as [number, number, number].`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('format', kColorTextureFormats)
.filter(formatCanBeTested)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combine('originValueInBlocks', [0, 6, 7, 8])
.combine('copySizeValueInBlocks', [0, 6, 7, 8])
.combine('textureSizePaddingValueInBlocks', [0, 7, 8])
.unless(
p =>
p.copySizeValueInBlocks + p.originValueInBlocks + p.textureSizePaddingValueInBlocks === 0
)
.combine('coordinateToTest', [0, 1, 2] as const)
.unless(p => p.dimension === '1d' && p.coordinateToTest !== 0)
)
.fn(t => {
const {
originValueInBlocks,
copySizeValueInBlocks,
textureSizePaddingValueInBlocks,
format,
dimension,
initMethod,
checkMethod,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForColorTextureFormat(format);
let originBlocks = [1, 1, 1];
let copySizeBlocks = [2, 2, 2];
let texSizeBlocks = [3, 3, 3];
if (dimension === '1d') {
originBlocks = [1, 0, 0];
copySizeBlocks = [2, 1, 1];
texSizeBlocks = [3, 1, 1];
}
{
const ctt = t.params.coordinateToTest;
originBlocks[ctt] = originValueInBlocks;
copySizeBlocks[ctt] = copySizeValueInBlocks;
texSizeBlocks[ctt] =
originBlocks[ctt] + copySizeBlocks[ctt] + textureSizePaddingValueInBlocks;
}
const origin: Required<GPUOrigin3DDict> = {
x: originBlocks[0] * info.blockWidth,
y: originBlocks[1] * info.blockHeight,
z: originBlocks[2],
};
const copySize = {
width: copySizeBlocks[0] * info.blockWidth,
height: copySizeBlocks[1] * info.blockHeight,
depthOrArrayLayers: copySizeBlocks[2],
};
const textureSize = [
texSizeBlocks[0] * info.blockWidth,
texSizeBlocks[1] * info.blockHeight,
texSizeBlocks[2],
] as const;
const rowsPerImage = copySizeBlocks[1];
const bytesPerRow = align(copySizeBlocks[0] * info.bytesPerBlock, 256);
const dataSize = dataBytesForCopyOrFail({
layout: { offset: 0, bytesPerRow, rowsPerImage },
format,
copySize,
method: initMethod,
});
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
origin,
textureSize,
format,
dimension,
initMethod,
checkMethod,
changeBeforePass: 'arrays',
});
});
* Generates textureSizes which correspond to the same physicalSizeAtMipLevel including virtual
* sizes at mip level different from the physical ones.
*/
function* generateTestTextureSizes({
format,
dimension,
mipLevel,
_mipSizeInBlocks,
}: {
format: ColorTextureFormat;
dimension: GPUTextureDimension;
mipLevel: number;
_mipSizeInBlocks: Required<GPUExtent3DDict>;
}): Generator<[number, number, number]> {
assert(dimension !== '1d');
const info = getBlockInfoForColorTextureFormat(format);
const widthAtThisLevel = _mipSizeInBlocks.width * info.blockWidth;
const heightAtThisLevel = _mipSizeInBlocks.height * info.blockHeight;
const textureSize: [number, number, number] = [
widthAtThisLevel << mipLevel,
heightAtThisLevel << mipLevel,
_mipSizeInBlocks.depthOrArrayLayers << (dimension === '3d' ? mipLevel : 0),
];
yield textureSize;
const widthAtPrevLevel = widthAtThisLevel << 1;
const heightAtPrevLevel = heightAtThisLevel << 1;
assert(mipLevel > 0);
assert(widthAtPrevLevel >= info.blockWidth && heightAtPrevLevel >= info.blockHeight);
const modifiedWidth = (widthAtPrevLevel - info.blockWidth) << (mipLevel - 1);
const modifiedHeight = (heightAtPrevLevel - info.blockHeight) << (mipLevel - 1);
const modifyWidth = info.blockWidth > 1 && modifiedWidth !== textureSize[0];
const modifyHeight = info.blockHeight > 1 && modifiedHeight !== textureSize[1];
if (modifyWidth) {
yield [modifiedWidth, textureSize[1], textureSize[2]];
}
if (modifyHeight) {
yield [textureSize[0], modifiedHeight, textureSize[2]];
}
if (modifyWidth && modifyHeight) {
yield [modifiedWidth, modifiedHeight, textureSize[2]];
}
if (dimension === '3d') {
yield [textureSize[0], textureSize[1], textureSize[2] + 1];
}
}
g.test('mip_levels')
.desc(
`Test that copying various mip levels works. Covers two special code paths:
- The physical size of the subresource is not equal to the logical size.
- bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers, and copyExtent needs to be clamped for all block formats.
- For 3D textures test copying to a sub-range of the depth.
Tests both 2D and 3D textures. 1D textures are skipped because they can only have one mip level.
TODO: Make a variant for depth-stencil formats.
`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('format', kColorTextureFormats)
.filter(formatCanBeTested)
.combine('dimension', ['2d', '3d'] as const)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combineWithParams([
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 1 },
originInBlocks: { x: 3, y: 2, z: 0 },
_mipSizeInBlocks: { width: 8, height: 6, depthOrArrayLayers: 1 },
mipLevel: 1,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 8, height: 6, depthOrArrayLayers: 3 },
mipLevel: 2,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 8, height: 7, depthOrArrayLayers: 4 },
mipLevel: 3,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 9, height: 6, depthOrArrayLayers: 4 },
mipLevel: 4,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 9, height: 7, depthOrArrayLayers: 3 },
mipLevel: 4,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 9, height: 7, depthOrArrayLayers: 4 },
mipLevel: 4,
},
{
copySizeInBlocks: { width: 5, height: 4, depthOrArrayLayers: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_mipSizeInBlocks: { width: 9, height: 9, depthOrArrayLayers: 6 },
mipLevel: 4,
},
])
.expand('textureSize', generateTestTextureSizes)
)
.fn(t => {
const {
copySizeInBlocks,
originInBlocks,
textureSize,
mipLevel,
format,
dimension,
initMethod,
checkMethod,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForColorTextureFormat(format);
const origin = {
x: originInBlocks.x * info.blockWidth,
y: originInBlocks.y * info.blockHeight,
z: originInBlocks.z,
};
const copySize = {
width: copySizeInBlocks.width * info.blockWidth,
height: copySizeInBlocks.height * info.blockHeight,
depthOrArrayLayers: copySizeInBlocks.depthOrArrayLayers,
};
const rowsPerImage = copySizeInBlocks.height + 1;
const bytesPerRow = align(copySize.width, 256);
const dataSize = dataBytesForCopyOrFail({
layout: { offset: 0, bytesPerRow, rowsPerImage },
format,
copySize,
method: initMethod,
});
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
origin,
mipLevel,
textureSize,
format,
dimension,
initMethod,
checkMethod,
});
});
g.test('compressed_textures,unaligned_mip_level_0')
.desc(
`
Test image copies (writeTexture / copyBufferToTexture / copyTextureToBuffer) that touch the partial
edge blocks of mip level 0 of a block-compressed texture whose mip level 0 size is NOT a multiple
of the texel block size, using the 'texture-compression-unaligned' feature.
This mirrors the 'mip_levels' test (which exercises the physical-size != logical-size path at
non-zero mip levels), but here the partial edge blocks are at mip level 0 itself. As elsewhere, the
copy is performed against the physical (rounded-up) size, so block-aligned copies reach the edge
blocks which are not fully inside the texture.
Covers the full copy as well as just the edge column / row / corner blocks, for the WriteTexture,
CopyB2T and CopyT2B methods.
`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('format', kColorTextureFormats)
.filter(formatCanBeTested)
.filter(({ format }) => isCompressedTextureFormat(format))
.beginSubcases()
.combine('copyCase', [
{ originInBlocks: { x: 0, y: 0 }, copySizeInBlocks: { width: 4, height: 4 } },
{ originInBlocks: { x: 3, y: 0 }, copySizeInBlocks: { width: 1, height: 4 } },
{ originInBlocks: { x: 0, y: 3 }, copySizeInBlocks: { width: 4, height: 1 } },
{ originInBlocks: { x: 3, y: 3 }, copySizeInBlocks: { width: 1, height: 1 } },
{ originInBlocks: { x: 0, y: 0 }, copySizeInBlocks: { width: 1, height: 1 } },
] as const)
)
.fn(t => {
const { format, initMethod, checkMethod, copyCase } = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfDeviceDoesNotHaveFeature('texture-compression-unaligned' as GPUFeatureName);
const info = getBlockInfoForColorTextureFormat(format);
const textureSize = [3 * info.blockWidth + 1, 3 * info.blockHeight + 1, 1] as const;
const origin = {
x: copyCase.originInBlocks.x * info.blockWidth,
y: copyCase.originInBlocks.y * info.blockHeight,
z: 0,
};
const copySize = {
width: copyCase.copySizeInBlocks.width * info.blockWidth,
height: copyCase.copySizeInBlocks.height * info.blockHeight,
depthOrArrayLayers: 1,
};
const rowsPerImage = copyCase.copySizeInBlocks.height + 1;
const bytesPerRow = align(copySize.width, 256);
const dataSize = dataBytesForCopyOrFail({
layout: { offset: 0, bytesPerRow, rowsPerImage },
format,
copySize,
method: initMethod,
});
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
origin,
mipLevel: 0,
textureSize,
format,
dimension: '2d',
initMethod,
checkMethod,
});
});
const UND = undefined;
g.test('undefined_params')
.desc(
`Tests undefined values of bytesPerRow, rowsPerImage, and origin.x/y/z.
Ensures bytesPerRow/rowsPerImage=undefined are valid and behave as expected.
Ensures origin.x/y/z undefined default to 0.`
)
.params(u =>
u
.combineWithParams(kMethodsToTest)
.combine('dimension', kTextureDimensions)
.beginSubcases()
.combineWithParams([
{ copySize: [3, 1, 1], origin: [UND, UND, UND], bytesPerRow: UND, rowsPerImage: UND },
{ copySize: [3, 1, 1], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: UND },
{ copySize: [3, 3, 1], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: UND },
{ copySize: [3, 3, 2], origin: [UND, UND, UND], bytesPerRow: 256, rowsPerImage: 3 },
{ copySize: [1, 1, 1], origin: [UND, 1, 1], bytesPerRow: UND, rowsPerImage: UND },
{ copySize: [1, 1, 1], origin: [1, UND, 1], bytesPerRow: UND, rowsPerImage: UND },
{ copySize: [1, 1, 1], origin: [1, 1, UND], bytesPerRow: UND, rowsPerImage: UND },
])
.expandWithParams(p => [
{
_textureSize: [
100,
p.copySize[1] + (p.origin[1] ?? 0),
p.copySize[2] + (p.origin[2] ?? 0),
] as const,
},
])
.unless(p => p.dimension === '1d' && (p._textureSize[1] > 1 || p._textureSize[2] > 1))
)
.fn(t => {
const {
dimension,
_textureSize,
bytesPerRow,
rowsPerImage,
copySize,
origin,
initMethod,
checkMethod,
} = t.params;
t.uploadTextureAndVerifyCopy({
textureDataLayout: {
offset: 0,
bytesPerRow: bytesPerRow ?? 0,
rowsPerImage: rowsPerImage ?? 0,
},
copySize: { width: copySize[0], height: copySize[1], depthOrArrayLayers: copySize[2] },
dataSize: 2000,
textureSize: _textureSize,
origin: { x: origin[0] ?? 0, y: origin[1] ?? 0, z: origin[2] ?? 0 },
format: 'rgba8unorm',
dimension,
initMethod,
checkMethod,
changeBeforePass: 'undefined',
});
});
function copyMethodSupportedWithDepthStencilFormat(
aspect: 'depth-only' | 'stencil-only',
format: DepthStencilFormat,
copyMethod: 'WriteTexture' | 'CopyB2T' | 'CopyT2B'
): boolean {
{
return (
(aspect === 'stencil-only' && isStencilTextureFormat(format)) ||
(aspect === 'depth-only' &&
isDepthTextureFormat(format) &&
copyMethod === 'CopyT2B' &&
depthStencilBufferTextureCopySupported('CopyT2B', format, aspect))
);
}
}
g.test('rowsPerImage_and_bytesPerRow_depth_stencil')
.desc(
`Test that copying data with various bytesPerRow and rowsPerImage values and minimum required
bytes in copy works for copyBufferToTexture(), copyTextureToBuffer() and writeTexture() with stencil
aspect and copyTextureToBuffer() with depth aspect.
Covers a special code path for Metal:
bufferSize - offset < bytesPerImage * copyExtent.depthOrArrayLayers
Covers a special code path for D3D12:
when bytesPerRow is not a multiple of 512 and copyExtent.depthOrArrayLayers > 1:
copyExtent.depthOrArrayLayers % 2 == { 0, 1 }
bytesPerRow == bytesInACompleteCopyImage
`
)
.params(u =>
u
.combine('format', kDepthStencilFormats)
.combine('copyMethod', ['WriteTexture', 'CopyB2T', 'CopyT2B'] as const)
.combine('aspect', ['depth-only', 'stencil-only'] as const)
.filter(t => copyMethodSupportedWithDepthStencilFormat(t.aspect, t.format, t.copyMethod))
.beginSubcases()
.combineWithParams(kRowsPerImageAndBytesPerRowParams.paddings)
.combineWithParams(kRowsPerImageAndBytesPerRowParams.copySizes)
.filter(t => {
return t.copyWidthInBlocks * t.copyHeightInBlocks * t.copyDepth > 0;
})
.combine('mipLevel', [0, 2])
)
.fn(t => {
const {
format,
copyMethod,
aspect,
bytesPerRowPadding,
rowsPerImagePadding,
copyWidthInBlocks,
copyHeightInBlocks,
copyDepth,
mipLevel,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
const bytesPerBlock = depthStencilFormatAspectSize(format, aspect);
const rowsPerImage = copyHeightInBlocks + rowsPerImagePadding;
const bytesPerRowAlignment = copyMethod === 'WriteTexture' ? 1 : kBytesPerRowAlignment;
const bytesPerRow =
align(bytesPerBlock * copyWidthInBlocks, bytesPerRowAlignment) +
bytesPerRowPadding * bytesPerRowAlignment;
const copySize = [copyWidthInBlocks, copyHeightInBlocks, copyDepth] as const;
const textureSize = [
copyWidthInBlocks << mipLevel,
copyHeightInBlocks << mipLevel,
copyDepth,
] as const;
if (copyMethod === 'CopyT2B') {
if (aspect === 'depth-only') {
t.doCopyTextureToBufferWithDepthAspectTest(
format,
copySize,
bytesPerRowPadding,
rowsPerImagePadding,
0,
0,
mipLevel
);
} else {
t.doCopyFromStencilTest(format, textureSize, bytesPerRow, rowsPerImage, 0, mipLevel);
}
} else {
assert(
aspect === 'stencil-only' && (copyMethod === 'CopyB2T' || copyMethod === 'WriteTexture')
);
const initialDataSize = dataBytesForCopyOrFail({
layout: { bytesPerRow, rowsPerImage },
format: 'stencil8',
copySize,
method: copyMethod,
});
t.doUploadToStencilTest(
format,
textureSize,
copyMethod,
bytesPerRow,
rowsPerImage,
initialDataSize,
0,
mipLevel
);
}
});
g.test('offsets_and_sizes_copy_depth_stencil')
.desc(
`Test that copying data with various offset values and additional data paddings
works for copyBufferToTexture(), copyTextureToBuffer() and writeTexture() with stencil aspect and
copyTextureToBuffer() with depth aspect.
Covers two special code paths for D3D12:
offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow
offset > bytesInACompleteCopyImage
`
)
.params(u =>
u
.combine('format', kDepthStencilFormats)
.combine('copyMethod', ['WriteTexture', 'CopyB2T', 'CopyT2B'] as const)
.combine('aspect', ['depth-only', 'stencil-only'] as const)
.filter(t => copyMethodSupportedWithDepthStencilFormat(t.aspect, t.format, t.copyMethod))
.beginSubcases()
.combineWithParams(kOffsetsAndSizesParams.offsetsAndPaddings)
.filter(t => t.offsetInBlocks % 4 === 0)
.combine('copyDepth', kOffsetsAndSizesParams.copyDepth)
.combine('mipLevel', [0, 2])
)
.fn(t => {
const { format, copyMethod, aspect, offsetInBlocks, dataPaddingInBytes, copyDepth, mipLevel } =
t.params;
t.skipIfTextureFormatNotSupported(format);
const bytesPerBlock = depthStencilFormatAspectSize(format, aspect);
const initialDataOffset = offsetInBlocks * bytesPerBlock;
const copySize = [3, 3, copyDepth] as const;
const rowsPerImage = 3;
const bytesPerRow = 256;
const textureSize = [copySize[0] << mipLevel, copySize[1] << mipLevel, copyDepth] as const;
if (copyMethod === 'CopyT2B') {
if (aspect === 'depth-only') {
t.doCopyTextureToBufferWithDepthAspectTest(format, copySize, 0, 0, 0, 0, mipLevel);
} else {
t.doCopyFromStencilTest(
format,
textureSize,
bytesPerRow,
rowsPerImage,
initialDataOffset,
mipLevel
);
}
} else {
assert(
aspect === 'stencil-only' && (copyMethod === 'CopyB2T' || copyMethod === 'WriteTexture')
);
const minDataSize = dataBytesForCopyOrFail({
layout: { offset: initialDataOffset, bytesPerRow, rowsPerImage },
format: 'stencil8',
copySize,
method: copyMethod,
});
const initialDataSize = minDataSize + dataPaddingInBytes;
t.doUploadToStencilTest(
format,
textureSize,
copyMethod,
bytesPerRow,
rowsPerImage,
initialDataSize,
initialDataOffset,
mipLevel
);
}
});