blob: 379dd8ff548019be0a68287ec7f1c9cc71049783 [file] [log] [blame]
/**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ 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.depth
- when bytesPerRow is not a multiple of 512 and copyExtent.depth > 1: copyExtent.depth % 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 subresouce is not equal to the logical size
- bufferSize - offset < bytesPerImage * copyExtent.depth 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.
* TODO:
- add another initMethod which renders the texture
- test copyT2B with buffer size not divisible by 4 (not done because expectContents 4-byte alignment)
- add tests for 1d / 3d textures
`;
import { params, poptions } from '../../../common/framework/params_builder.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert, unreachable } from '../../../common/framework/util/util.js';
import { kSizedTextureFormatInfo, kSizedTextureFormats } from '../../capability_info.js';
import { GPUTest } from '../../gpu_test.js';
import { align } from '../../util/math.js';
import { getTextureCopyLayout } from '../../util/texture/layout.js';
/** Each combination of methods assume that the ones before it were tested and work correctly. */
const kMethodsToTest = [
// We make sure that CopyT2B works when copying the whole texture for renderable formats:
// TODO
// Then we make sure that WriteTexture works for all formats:
{ initMethod: 'WriteTexture', checkMethod: 'FullCopyT2B' },
// Then we make sure that CopyB2T works for all formats:
{ initMethod: 'CopyB2T', checkMethod: 'FullCopyT2B' },
// Then we make sure that CopyT2B works for all formats:
{ initMethod: 'WriteTexture', checkMethod: 'PartialCopyT2B' },
];
class CopyBetweenLinearDataAndTextureTest extends GPUTest {
bytesInACompleteRow(copyWidth, format) {
const blockWidth = kSizedTextureFormatInfo[format].blockWidth;
assert(copyWidth % blockWidth === 0);
const copyWidthInBlocks = copyWidth / blockWidth;
return kSizedTextureFormatInfo[format].bytesPerBlock * copyWidthInBlocks;
}
requiredBytesInCopy(layout, format, copyExtent) {
assert(layout.rowsPerImage % kSizedTextureFormatInfo[format].blockHeight === 0);
assert(copyExtent.height % kSizedTextureFormatInfo[format].blockHeight === 0);
assert(copyExtent.width % kSizedTextureFormatInfo[format].blockWidth === 0);
if (copyExtent.width === 0 || copyExtent.height === 0 || copyExtent.depth === 0) {
return 0;
} else {
const texelBlockRowsPerImage =
layout.rowsPerImage / kSizedTextureFormatInfo[format].blockHeight;
const bytesPerImage = layout.bytesPerRow * texelBlockRowsPerImage;
const bytesInLastSlice =
layout.bytesPerRow * (copyExtent.height / kSizedTextureFormatInfo[format].blockHeight - 1) +
(copyExtent.width / kSizedTextureFormatInfo[format].blockWidth) *
kSizedTextureFormatInfo[format].bytesPerBlock;
return bytesPerImage * (copyExtent.depth - 1) + bytesInLastSlice;
}
}
/** Offset for a particular texel in the linear texture data */
getTexelOffsetInBytes(textureDataLayout, format, texel, origin = { x: 0, y: 0, z: 0 }) {
const { offset, bytesPerRow, rowsPerImage } = textureDataLayout;
const info = kSizedTextureFormatInfo[format];
assert(texel.x >= origin.x && texel.y >= origin.y && texel.z >= origin.z);
assert(rowsPerImage % info.blockHeight === 0);
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 / info.blockHeight) * bytesPerRow;
return (
offset +
(texel.z - origin.z) * bytesPerImage +
((texel.y - origin.y) / info.blockHeight) * bytesPerRow +
((texel.x - origin.x) / info.blockWidth) * info.bytesPerBlock
);
}
*iterateBlockRows(size, origin, format) {
if (size.width === 0 || size.height === 0 || size.depth === 0) {
// do not iterate anything for an empty region
return;
}
const info = kSizedTextureFormatInfo[format];
assert(size.height % info.blockHeight === 0);
for (let y = 0; y < size.height / info.blockHeight; ++y) {
for (let z = 0; z < size.depth; ++z) {
yield {
x: origin.x,
y: origin.y + y * info.blockHeight,
z: origin.z + z,
};
}
}
}
generateData(byteSize, start = 0) {
const arr = new Uint8Array(byteSize);
for (let i = 0; i < byteSize; ++i) {
arr[i] = (i ** 3 + i + start) % 251;
}
return arr;
}
/**
* This is used for testing passing undefined members of `GPUTextureDataLayout` 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, rowsPerImage, bytesPerRow, changeBeforePass) {
if (changeBeforePass === 'undefined') {
if (offset === 0) {
offset = undefined;
}
if (rowsPerImage === 0) {
rowsPerImage = undefined;
}
}
return { offset, bytesPerRow, rowsPerImage };
}
/**
* This is used for testing passing undefined members of `GPUTextureCopyView` 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, origin_x, origin_y, origin_z, mipLevel, changeBeforePass) {
let origin = { 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, height, depth, changeBeforePass) {
if (changeBeforePass === 'arrays') {
return [width, height, depth];
} else {
return { width, height, depth };
}
}
/** Run a CopyT2B command with appropriate arguments corresponding to `ChangeBeforePass` */
copyTextureToBufferWithAppliedArguments(
buffer,
{ offset, rowsPerImage, bytesPerRow },
{ width, height, depth },
{ texture, mipLevel, origin },
changeBeforePass
) {
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, depth, changeBeforePass);
const encoder = this.device.createCommandEncoder();
encoder.copyTextureToBuffer(
appliedCopyView,
{ buffer, ...appliedDataLayout },
appliedCheckSize
);
this.device.defaultQueue.submit([encoder.finish()]);
}
/** Put data into a part of the texture with an appropriate method. */
uploadLinearTextureDataToTextureSubBox(
textureCopyView,
textureDataLayout,
copySize,
partialData,
method,
changeBeforePass
) {
const { texture, mipLevel, origin } = textureCopyView;
const { offset, rowsPerImage, bytesPerRow } = textureDataLayout;
const { x, y, z } = origin;
const { width, height, depth } = 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, depth, changeBeforePass);
switch (method) {
case 'WriteTexture': {
this.device.defaultQueue.writeTexture(
appliedCopyView,
partialData,
appliedDataLayout,
appliedCopySize
);
break;
}
case 'CopyB2T': {
const buffer = this.device.createBuffer({
mappedAtCreation: true,
size: align(partialData.byteLength, 4),
usage: GPUBufferUsage.COPY_SRC,
});
new Uint8Array(buffer.getMappedRange()).set(partialData);
buffer.unmap();
const encoder = this.device.createCommandEncoder();
encoder.copyBufferToTexture(
{ buffer, ...appliedDataLayout },
appliedCopyView,
appliedCopySize
);
this.device.defaultQueue.submit([encoder.finish()]);
break;
}
default:
unreachable();
}
}
/**
* 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). We also ensure that CopyT2B doesn't overwrite bytes it's not supposed
* to if validateOtherBytesInBuffer is set to true.
*/
copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin },
checkSize,
format,
expected,
expectedDataLayout,
changeBeforePass = 'none'
) {
// The alignment is necessary because we need to copy and map data from this buffer.
const bufferSize = align(expected.byteLength, 4);
// The start value ensures generated data here doesn't match the expected data.
const bufferData = this.generateData(bufferSize, 17);
const buffer = this.device.createBuffer({
mappedAtCreation: true,
size: bufferSize,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
new Uint8Array(buffer.getMappedRange()).set(bufferData);
buffer.unmap();
this.copyTextureToBufferWithAppliedArguments(
buffer,
expectedDataLayout,
checkSize,
{ texture, mipLevel, origin },
changeBeforePass
);
this.updateLinearTextureDataSubBox(
expectedDataLayout,
expectedDataLayout,
checkSize,
origin,
format,
bufferData,
expected
);
this.expectContents(buffer, bufferData);
}
/**
* Copies the whole texture into linear data stored in a buffer for further checks.
*
* Used for `copyWholeTextureToBufferAndCheckContentsWithUpdatedData`.
*/
copyWholeTextureToNewBuffer({ texture, mipLevel }, resultDataLayout) {
const { mipSize, byteLength, bytesPerRow, rowsPerImage } = resultDataLayout;
const buffer = this.device.createBuffer({
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 encoder = this.device.createCommandEncoder();
encoder.copyTextureToBuffer(
{ texture, mipLevel },
{ buffer, bytesPerRow, rowsPerImage },
mipSize
);
this.device.defaultQueue.submit([encoder.finish()]);
return buffer;
}
copyFromArrayToArray(src, srcOffset, dst, dstOffset, size) {
dst.set(src.subarray(srcOffset, srcOffset + size), dstOffset);
}
/**
* Takes the data returned by `copyWholeTextureToNewBuffer` and updates it after a copy operation
* on the texture by emulating the copy behaviour here directly.
*/
updateLinearTextureDataSubBox(
{ bytesPerRow, rowsPerImage },
sourceDataLayout,
copySize,
origin,
format,
destination,
source
) {
for (const texel of this.iterateBlockRows(copySize, origin, format)) {
const sourceOffset = this.getTexelOffsetInBytes(sourceDataLayout, format, texel, origin);
const destinationOffset = this.getTexelOffsetInBytes(
{ bytesPerRow, rowsPerImage, offset: 0 },
format,
texel
);
const rowLength = this.bytesInACompleteRow(copySize.width, format);
this.copyFromArrayToArray(source, sourceOffset, destination, destinationOffset, rowLength);
}
}
/**
* 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 },
fullTextureCopyLayout,
texturePartialDataLayout,
copySize,
format,
fullData,
partialData
) {
const { mipSize, bytesPerRow, rowsPerImage, byteLength } = fullTextureCopyLayout;
const { dst, begin, end } = this.createAlignedCopyForMapRead(fullData, byteLength, 0);
// We add an eventual async expectation which will update the full data and then add
// other eventual async expectations to ensure it will be correct.
this.eventualAsyncExpectation(async () => {
await dst.mapAsync(GPUMapMode.READ);
const actual = new Uint8Array(dst.getMappedRange()).subarray(begin, end);
this.updateLinearTextureDataSubBox(
fullTextureCopyLayout,
texturePartialDataLayout,
copySize,
origin,
format,
actual,
partialData
);
this.copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin: { x: 0, y: 0, z: 0 } },
{ width: mipSize[0], height: mipSize[1], depth: mipSize[2] },
format,
actual,
{ bytesPerRow, rowsPerImage, offset: 0 }
);
dst.destroy();
});
}
/**
* 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 = '2d',
initMethod,
checkMethod,
changeBeforePass = 'none',
}) {
const texture = this.device.createTexture({
size: textureSize,
format,
dimension,
mipLevelCount: mipLevel + 1,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
const data = this.generateData(dataSize);
switch (checkMethod) {
case 'PartialCopyT2B': {
this.uploadLinearTextureDataToTextureSubBox(
{ texture, mipLevel, origin },
textureDataLayout,
copySize,
data,
initMethod,
changeBeforePass
);
this.copyPartialTextureToBufferAndCheckContents(
{ texture, mipLevel, origin },
copySize,
format,
data,
textureDataLayout,
changeBeforePass
);
break;
}
case 'FullCopyT2B': {
const fullTextureCopyLayout = getTextureCopyLayout(format, dimension, textureSize, {
mipLevel,
});
const fullData = this.copyWholeTextureToNewBuffer(
{ texture, mipLevel },
fullTextureCopyLayout
);
this.uploadLinearTextureDataToTextureSubBox(
{ texture, mipLevel, origin },
textureDataLayout,
copySize,
data,
initMethod,
changeBeforePass
);
this.copyWholeTextureToBufferAndCheckContentsWithUpdatedData(
{ texture, mipLevel, origin },
fullTextureCopyLayout,
textureDataLayout,
copySize,
format,
fullData,
data
);
break;
}
default:
unreachable();
}
}
}
/**
* This is a helper function used for filtering test parameters
*
* TODO: Modify this after introducing tests with rendering.
*/
function formatCanBeTested({ format }) {
return kSizedTextureFormatInfo[format].copyDst && kSizedTextureFormatInfo[format].copySrc;
}
export const g = makeTestGroup(CopyBetweenLinearDataAndTextureTest);
// 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.depth
// Covers a special code path for D3D12:
// when bytesPerRow is not a multiple of 512 and copyExtent.depth > 1: copyExtent.depth % 2 == { 0, 1 }
// bytesPerRow == bytesInACompleteCopyImage */
g.test('copy_with_various_rows_per_image_and_bytes_per_row')
.params(
params()
.combine(kMethodsToTest)
.combine([
{ bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 0 }, // no padding
{ bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 6 }, // rowsPerImage padding
{ bytesPerRowPadding: 6, rowsPerImagePaddingInBlocks: 0 }, // bytesPerRow padding
{ bytesPerRowPadding: 15, rowsPerImagePaddingInBlocks: 17 }, // both paddings
])
.combine([
// In the two cases below, for (WriteTexture, PartialCopyB2T) and (CopyB2T, FullCopyT2B)
// sets of methods we will have bytesPerRow = 256 and copyDepth % 2 == { 0, 1 }
// respectively. This covers a special code path for D3D12.
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5 }, // standard copy
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 2 }, // standard copy
{ copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2 }, // copyWidth is 256-aligned
{ copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5 }, // empty copy because of width
{ copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5 }, // empty copy because of height
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0 }, // empty copy because of depth
{ copyWidthInBlocks: 1, copyHeightInBlocks: 3, copyDepth: 5 }, // copyWidth = 1
// The two cases below cover another special code path for D3D12.
// - For (WriteTexture, FullCopyT2B) with r8unorm:
// bytesPerRow = 15 = 3 * 5 = bytesInACompleteCopyImage.
{ copyWidthInBlocks: 32, copyHeightInBlocks: 1, copyDepth: 8 }, // copyHeight = 1
// - For (CopyB2T, FullCopyT2B) and (WriteTexture, PartialCopyT2B) with r8unorm:
// bytesPerRow = 256 = 8 * 32 = bytesInACompleteCopyImage.
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1 }, // copyDepth = 1
{ copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1 }, // copyHeight = 1 and copyDepth = 1
])
.combine(poptions('format', kSizedTextureFormats))
.filter(formatCanBeTested)
)
.fn(async t => {
const {
bytesPerRowPadding,
rowsPerImagePaddingInBlocks,
copyWidthInBlocks,
copyHeightInBlocks,
copyDepth,
format,
initMethod,
checkMethod,
} = t.params;
const info = kSizedTextureFormatInfo[format];
// For CopyB2T and CopyT2B we need to have bytesPerRow 256-aligned,
// to make this happen we align the bytesInACompleteRow value and multiply
// bytesPerRowPadding by 256.
const bytesPerRowAlignment =
initMethod === 'WriteTexture' && checkMethod === 'FullCopyT2B' ? 1 : 256;
const copyWidth = copyWidthInBlocks * info.blockWidth;
const copyHeight = copyHeightInBlocks * info.blockHeight;
const rowsPerImage = copyHeight + rowsPerImagePaddingInBlocks * info.blockHeight;
const bytesPerRow =
align(t.bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) +
bytesPerRowPadding * bytesPerRowAlignment;
const copySize = { width: copyWidth, height: copyHeight, depth: copyDepth };
const minDataSize = t.requiredBytesInCopy(
{ offset: 0, bytesPerRow, rowsPerImage },
format,
copySize
);
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize: minDataSize,
textureSize: [
Math.max(copyWidth, info.blockWidth),
Math.max(copyHeight, info.blockHeight),
Math.max(copyDepth, 1),
],
/* making sure the texture is non-empty */ format,
initMethod,
checkMethod,
});
});
// 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
g.test('copy_with_various_offsets_and_data_sizes')
.params(
params()
.combine(kMethodsToTest)
.combine([
{ offsetInBlocks: 0, dataPaddingInBytes: 0 }, // no offset and no padding
{ offsetInBlocks: 1, dataPaddingInBytes: 0 }, // offset = 1
{ offsetInBlocks: 2, dataPaddingInBytes: 0 }, // offset = 2
{ offsetInBlocks: 15, dataPaddingInBytes: 0 }, // offset = 15
{ offsetInBlocks: 16, dataPaddingInBytes: 0 }, // offset = 16
{ offsetInBlocks: 242, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 242 + 12 = 256 = bytesPerRow
{ offsetInBlocks: 243, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 243 + 12 > 256 = bytesPerRow
{ offsetInBlocks: 768, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 768 = 3 * 256 = bytesInACompleteCopyImage
{ offsetInBlocks: 769, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 769 > 768 = bytesInACompleteCopyImage
{ offsetInBlocks: 0, dataPaddingInBytes: 1 }, // dataPaddingInBytes > 0
{ offsetInBlocks: 1, dataPaddingInBytes: 8 }, // offset > 0 and dataPaddingInBytes > 0
])
.combine(poptions('copyDepth', [1, 2])) // 2d and 2d-array textures
.combine(poptions('format', kSizedTextureFormats))
.filter(formatCanBeTested)
)
.fn(async t => {
const {
offsetInBlocks,
dataPaddingInBytes,
copyDepth,
format,
initMethod,
checkMethod,
} = t.params;
const info = kSizedTextureFormatInfo[format];
const offset = offsetInBlocks * info.bytesPerBlock;
const copySize = {
width: 3 * info.blockWidth,
height: 3 * info.blockHeight,
depth: copyDepth,
};
const rowsPerImage = copySize.height;
const bytesPerRow = 256;
const dataSize =
offset +
t.requiredBytesInCopy({ offset, bytesPerRow, rowsPerImage }, format, copySize) +
dataPaddingInBytes;
// We're copying a (3 x 3 x copyDepth) (in texel blocks) part of a (4 x 4 x copyDepth)
// (in texel blocks) texture with no origin.
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset, bytesPerRow, rowsPerImage },
copySize,
dataSize,
textureSize: [4 * info.blockWidth, 4 * info.blockHeight, copyDepth],
format,
initMethod,
checkMethod,
});
});
// 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].
g.test('copy_with_various_origins_and_copy_extents')
.params(
params()
.combine(kMethodsToTest)
.combine(poptions('originValueInBlocks', [0, 7, 8]))
.combine(poptions('copySizeValueInBlocks', [0, 7, 8]))
.combine(poptions('textureSizePaddingValueInBlocks', [0, 7, 8]))
.unless(
p =>
// we can't create an empty texture
p.copySizeValueInBlocks + p.originValueInBlocks + p.textureSizePaddingValueInBlocks === 0
)
.combine(poptions('coordinateToTest', ['width', 'height', 'depth']))
.combine(poptions('format', kSizedTextureFormats))
.filter(formatCanBeTested)
)
.fn(async t => {
const {
coordinateToTest,
originValueInBlocks,
copySizeValueInBlocks,
textureSizePaddingValueInBlocks,
format,
initMethod,
checkMethod,
} = t.params;
const info = kSizedTextureFormatInfo[format];
const origin = { x: info.blockWidth, y: info.blockHeight, z: 1 };
const copySize = { width: 2 * info.blockWidth, height: 2 * info.blockHeight, depth: 2 };
const textureSize = [3 * info.blockWidth, 3 * info.blockHeight, 3];
switch (coordinateToTest) {
case 'width': {
origin.x = originValueInBlocks * info.blockWidth;
copySize.width = copySizeValueInBlocks * info.blockWidth;
textureSize[0] =
origin.x + copySize.width + textureSizePaddingValueInBlocks * info.blockWidth;
break;
}
case 'height': {
origin.y = originValueInBlocks * info.blockHeight;
copySize.height = copySizeValueInBlocks * info.blockHeight;
textureSize[1] =
origin.y + copySize.height + textureSizePaddingValueInBlocks * info.blockHeight;
break;
}
case 'depth': {
origin.z = originValueInBlocks;
copySize.depth = copySizeValueInBlocks;
textureSize[2] = origin.z + copySize.depth + textureSizePaddingValueInBlocks;
break;
}
}
const rowsPerImage = copySize.height;
const bytesPerRow = align(copySize.width, 256);
const dataSize = t.requiredBytesInCopy(
{ offset: 0, bytesPerRow, rowsPerImage },
format,
copySize
);
// For testing width: we copy a (_ x 2 x 2) (in texel blocks) part of a (_ x 3 x 3)
// (in texel blocks) texture with origin (_, 1, 1) (in texel blocks).
// Similarly for other coordinates.
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
origin,
textureSize,
format,
initMethod,
checkMethod,
changeBeforePass: 'arrays',
});
});
/**
* Generates textureSizes which correspond to the same physicalSizeAtMipLevel including virtual
* sizes at mip level different from the physical ones.
*/
function* textureSizeExpander({ format, mipLevel, _texturePhysicalSizeAtMipLevelInBlocks }) {
const info = kSizedTextureFormatInfo[format];
const widthAtThisLevel = _texturePhysicalSizeAtMipLevelInBlocks.width * info.blockWidth;
const heightAtThisLevel = _texturePhysicalSizeAtMipLevelInBlocks.height * info.blockHeight;
const textureSize = [
widthAtThisLevel << mipLevel,
heightAtThisLevel << mipLevel,
_texturePhysicalSizeAtMipLevelInBlocks.depth,
];
yield {
textureSize,
};
// We choose width and height of the texture so that the values are divisible by blockWidth and
// blockHeight respectively and so that the virtual size at mip level corresponds to the same
// physical size.
// Virtual size at mip level with modified width has width = (physical size width) - (blockWidth / 2).
// Virtual size at mip level with modified height has height = (physical size height) - (blockHeight / 2).
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 {
textureSize: [modifiedWidth, textureSize[1], textureSize[2]],
};
}
if (modifyHeight) {
yield {
textureSize: [textureSize[0], modifiedHeight, textureSize[2]],
};
}
if (modifyWidth && modifyHeight) {
yield {
textureSize: [modifiedWidth, modifiedHeight, textureSize[2]],
};
}
}
// Test that copying various mip levels works.
// Covers two special code paths:
// - the physical size of the subresouce is not equal to the logical size
// - bufferSize - offset < bytesPerImage * copyExtent.depth and copyExtent needs to be clamped for all block formats */
g.test('copy_various_mip_levels')
.params(
params()
.combine(kMethodsToTest)
.combine([
// origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d texture */
{
copySizeInBlocks: { width: 5, height: 4, depth: 1 },
originInBlocks: { x: 3, y: 2, z: 0 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 6, depth: 1 },
mipLevel: 1,
},
// origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d-array texture
{
copySizeInBlocks: { width: 5, height: 4, depth: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 6, depth: 3 },
mipLevel: 2,
},
// origin.x + copySize.width = texturePhysicalSizeAtMipLevel.width
{
copySizeInBlocks: { width: 5, height: 4, depth: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 7, depth: 4 },
mipLevel: 3,
},
// origin.y + copySize.height = texturePhysicalSizeAtMipLevel.height
{
copySizeInBlocks: { width: 5, height: 4, depth: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 6, depth: 4 },
mipLevel: 4,
},
// origin.z + copySize.depth = texturePhysicalSizeAtMipLevel.depth
{
copySizeInBlocks: { width: 5, height: 4, depth: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 7, depth: 3 },
mipLevel: 5,
},
// origin + copySize < texturePhysicalSizeAtMipLevel for all coordinates
{
copySizeInBlocks: { width: 5, height: 4, depth: 2 },
originInBlocks: { x: 3, y: 2, z: 1 },
_texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 7, depth: 4 },
mipLevel: 6,
},
])
.combine(poptions('format', kSizedTextureFormats))
.filter(formatCanBeTested)
.expand(textureSizeExpander)
)
.fn(async t => {
const {
copySizeInBlocks,
originInBlocks,
textureSize,
mipLevel,
format,
initMethod,
checkMethod,
} = t.params;
const info = kSizedTextureFormatInfo[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,
depth: copySizeInBlocks.depth,
};
const rowsPerImage = copySize.height + info.blockHeight;
const bytesPerRow = align(copySize.width, 256);
const dataSize = t.requiredBytesInCopy(
{ offset: 0, bytesPerRow, rowsPerImage },
format,
copySize
);
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize,
origin,
mipLevel,
textureSize,
format,
initMethod,
checkMethod,
});
});
// 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. Origin, offset, mipLevel and rowsPerImage values will be set
// to undefined when appropriate.
g.test('copy_with_no_image_or_slice_padding_and_undefined_values')
.params(
params()
.combine(kMethodsToTest)
.combine([
// copying one row: bytesPerRow and rowsPerImage can be set to 0
{
bytesPerRow: 0,
rowsPerImage: 0,
copySize: { width: 3, height: 1, depth: 1 },
origin: { x: 0, y: 0, z: 0 },
},
// copying one row: bytesPerRow can be < bytesInACompleteRow = 400
{
bytesPerRow: 256,
rowsPerImage: 0,
copySize: { width: 100, height: 1, depth: 1 },
origin: { x: 0, y: 0, z: 0 },
},
// copying one slice: rowsPerImage = 0 will be set to undefined
{
bytesPerRow: 256,
rowsPerImage: 0,
copySize: { width: 3, height: 3, depth: 1 },
origin: { x: 0, y: 0, z: 0 },
},
// copying one slice: rowsPerImage = 2 is valid
{
bytesPerRow: 256,
rowsPerImage: 2,
copySize: { width: 3, height: 3, depth: 1 },
origin: { x: 0, y: 0, z: 0 },
},
// origin.x = 0 will be set to undefined
{
bytesPerRow: 0,
rowsPerImage: 0,
copySize: { width: 1, height: 1, depth: 1 },
origin: { x: 0, y: 1, z: 1 },
},
// origin.y = 0 will be set to undefined
{
bytesPerRow: 0,
rowsPerImage: 0,
copySize: { width: 1, height: 1, depth: 1 },
origin: { x: 1, y: 0, z: 1 },
},
// origin.z = 0 will be set to undefined
{
bytesPerRow: 0,
rowsPerImage: 0,
copySize: { width: 1, height: 1, depth: 1 },
origin: { x: 1, y: 1, z: 0 },
},
])
)
.fn(async t => {
const { bytesPerRow, rowsPerImage, copySize, origin, initMethod, checkMethod } = t.params;
t.uploadTextureAndVerifyCopy({
textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage },
copySize,
dataSize: 100 * 3 * 4,
textureSize: [100, 3, 2],
origin,
format: 'rgba8unorm',
initMethod,
checkMethod,
changeBeforePass: 'undefined',
});
});