export const description = `Validation tests for the linear data layout of linear data <-> texture copies
TODO check if the tests need to be updated to support aspects of depth-stencil textures`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { assert } from '../../../../common/util/util.js';
import { kTextureDimensions } from '../../../capability_info.js';
import {
kSizedTextureFormats,
textureFormatAndDimensionPossiblyCompatible,
getBlockInfoForTextureFormat,
isDepthOrStencilTextureFormat,
getBlockInfoForSizedTextureFormat,
getBlockInfoForColorTextureFormat,
getMaxValidTextureSizeForFormatAndDimension,
} from '../../../format_info.js';
import { align } from '../../../util/math.js';
import {
bytesInACompleteRow,
dataBytesForCopyOrOverestimate,
dataBytesForCopyOrFail,
kImageCopyTypes,
} from '../../../util/texture/layout.js';
import {
ImageCopyTest,
texelBlockAlignmentTestExpanderForOffset,
texelBlockAlignmentTestExpanderForRowsPerImage,
formatCopyableWithMethod,
} from './image_copy.js';
export const g = makeTestGroup(ImageCopyTest);
g.test('bound_on_rows_per_image')
.desc(
`
Test that rowsPerImage must be at least the copy height (if defined).
- for various copy methods
- for all texture dimensions
- for various values of rowsPerImage including undefined
- for various copy heights
- for various copy depths
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combineWithParams([
{ dimension: '1d', size: [4, 1, 1] },
{ dimension: '2d', size: [4, 4, 1] },
{ dimension: '2d', size: [4, 4, 3] },
{ dimension: '3d', size: [4, 4, 3] },
] as const)
.beginSubcases()
.combine('rowsPerImage', [undefined, 0, 1, 2, 1024])
.combine('copyHeightInBlocks', [0, 1, 2])
.combine('copyDepth', [1, 3])
.unless(p => p.dimension === '1d' && p.copyHeightInBlocks !== 1)
.unless(p => p.copyDepth > p.size[2])
)
.fn(t => {
const { rowsPerImage, copyHeightInBlocks, copyDepth, dimension, size, method } = t.params;
const format = 'rgba8unorm';
const copyHeight = copyHeightInBlocks * getBlockInfoForTextureFormat(format).blockHeight;
const texture = t.createTextureTracked({
size,
dimension,
format,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
const layout = { bytesPerRow: 1024, rowsPerImage };
const copySize = { width: 0, height: copyHeight, depthOrArrayLayers: copyDepth };
const { minDataSizeOrOverestimate, copyValid } = dataBytesForCopyOrOverestimate({
layout,
format,
copySize,
method,
});
t.testRun({ texture }, layout, copySize, {
dataSize: minDataSizeOrOverestimate,
method,
success: copyValid,
});
});
g.test('copy_end_overflows_u64')
.desc(
`
Test an error is produced when offset+requiredBytesInCopy overflows GPUSize64.
- for various copy methods
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.beginSubcases()
.combineWithParams([
{ bytesPerRow: 2 ** 31, rowsPerImage: 2 ** 31, depthOrArrayLayers: 1, _success: true },
{ bytesPerRow: 2 ** 31, rowsPerImage: 2 ** 31, depthOrArrayLayers: 16, _success: false },
])
)
.fn(t => {
const { method, bytesPerRow, rowsPerImage, depthOrArrayLayers, _success } = t.params;
const texture = t.createTextureTracked({
size: [1, 1, depthOrArrayLayers],
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
t.testRun(
{ texture },
{ bytesPerRow, rowsPerImage },
{ width: 1, height: 1, depthOrArrayLayers },
{
dataSize: 10000,
method,
success: _success,
}
);
});
g.test('required_bytes_in_copy')
.desc(
`
Test the computation of requiredBytesInCopy by computing the minimum data size for the copy and checking success/error at the boundary.
- for various copy methods
- for all formats
- for all dimensions
- for various extra bytesPerRow/rowsPerImage
- for various copy sizes
- for various offsets in the linear data
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kSizedTextureFormats)
.filter(formatCopyableWithMethod)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combineWithParams([
{ bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 0 },
{ bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 6 },
{ bytesPerRowPadding: 6, rowsPerImagePaddingInBlocks: 0 },
{ bytesPerRowPadding: 15, rowsPerImagePaddingInBlocks: 17 },
])
.combineWithParams([
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 3, _offsetMultiplier: 11 },
{ copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0, _offsetMultiplier: 13 },
{ copyWidthInBlocks: 1, copyHeightInBlocks: 4, copyDepth: 5, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 3, copyHeightInBlocks: 1, copyDepth: 5, _offsetMultiplier: 15 },
{ copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1, _offsetMultiplier: 0 },
{ copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1, _offsetMultiplier: 0 },
])
.filter(({ format, copyWidthInBlocks, copyHeightInBlocks, copyDepth }) => {
return (
!isDepthOrStencilTextureFormat(format) ||
(copyWidthInBlocks > 0 && copyHeightInBlocks > 0 && copyDepth > 0)
);
})
.unless(p => p.dimension === '1d' && (p.copyHeightInBlocks > 1 || p.copyDepth > 1))
.expand('offset', p => {
if (isDepthOrStencilTextureFormat(p.format)) {
return [p._offsetMultiplier * 4];
}
return [p._offsetMultiplier * getBlockInfoForSizedTextureFormat(p.format).bytesPerBlock];
})
)
.fn(t => {
const {
offset,
bytesPerRowPadding,
rowsPerImagePaddingInBlocks,
copyWidthInBlocks,
copyHeightInBlocks,
copyDepth,
format,
dimension,
method,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForSizedTextureFormat(format);
const maxSize = getMaxValidTextureSizeForFormatAndDimension(t.device, format, dimension);
const bytesPerRowAlignment = method === 'WriteTexture' ? 1 : 256;
const copyWidth = Math.min(copyWidthInBlocks * info.blockWidth, maxSize[0]);
const copyHeight = Math.min(copyHeightInBlocks * info.blockHeight, maxSize[1]);
const rowsPerImage = copyHeight + rowsPerImagePaddingInBlocks * info.blockHeight;
const bytesPerRow =
align(bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) +
bytesPerRowPadding * bytesPerRowAlignment;
const copySize = { width: copyWidth, height: copyHeight, depthOrArrayLayers: copyDepth };
const layout = { offset, bytesPerRow, rowsPerImage };
const minDataSize = dataBytesForCopyOrFail({ layout, format, copySize, method });
const texture = t.createAlignedTexture(format, copySize, undefined, dimension);
t.testRun({ texture }, layout, copySize, {
dataSize: minDataSize,
method,
success: true,
});
if (minDataSize > 0) {
t.testRun({ texture }, layout, copySize, {
dataSize: minDataSize - 1,
method,
success: false,
});
}
});
g.test('rows_per_image_alignment')
.desc(
`
Test that rowsPerImage has no alignment constraints.
- for various copy methods
- for all sized format
- for all dimensions
- for various rowsPerImage
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kSizedTextureFormats)
.filter(formatCopyableWithMethod)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.expand('rowsPerImage', texelBlockAlignmentTestExpanderForRowsPerImage)
.filter(
({ rowsPerImage, format }) =>
rowsPerImage >= getBlockInfoForSizedTextureFormat(format).blockHeight
)
)
.fn(t => {
const { rowsPerImage, format, dimension, method } = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForSizedTextureFormat(format);
const size = { width: info.blockWidth, height: info.blockHeight, depthOrArrayLayers: 1 };
const texture = t.createTextureTracked({
size,
format,
dimension,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
t.testRun({ texture }, { bytesPerRow: 256, rowsPerImage }, size, {
dataSize: info.bytesPerBlock,
method,
success: true,
});
});
g.test('offset_alignment')
.desc(
`
Test the alignment requirement on the linear data offset (block size, or 4 for depth-stencil).
- for various copy methods
- for all sized formats
- for all dimensions
- for various linear data offsets
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kSizedTextureFormats)
.filter(formatCopyableWithMethod)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.expand('offset', texelBlockAlignmentTestExpanderForOffset)
)
.fn(t => {
const { format, dimension, offset, method } = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForSizedTextureFormat(format);
const size = { width: info.blockWidth, height: info.blockHeight, depthOrArrayLayers: 1 };
const texture = t.createTextureTracked({
size,
format,
dimension,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
let success = false;
if (method === 'WriteTexture') success = true;
if (isDepthOrStencilTextureFormat(format)) {
if (offset % 4 === 0) success = true;
} else {
if (offset % info.bytesPerBlock === 0) success = true;
}
t.testRun({ texture }, { offset, bytesPerRow: 256 }, size, {
dataSize: offset + info.bytesPerBlock,
method,
success,
});
});
g.test('bound_on_bytes_per_row')
.desc(
`
Test that bytesPerRow, if specified must be big enough for a full copy row.
- for various copy methods
- for all sized formats
- for all dimension
- for various copy heights
- for various copy depths
- for various combinations of bytesPerRow and copy width.
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kSizedTextureFormats)
.filter(formatCopyableWithMethod)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combine('copyHeightInBlocks', [1, 2])
.combine('copyDepth', [1, 2])
.unless(p => p.dimension === '1d' && (p.copyHeightInBlocks > 1 || p.copyDepth > 1))
.expandWithParams(p => {
const info = getBlockInfoForSizedTextureFormat(p.format);
assert(128 % info.bytesPerBlock === 0);
return [
{
bytesPerRow: 256,
widthInBlocks: 256 / info.bytesPerBlock,
copyWidthInBlocks: 256 / info.bytesPerBlock,
_success: true,
},
{
bytesPerRow: 256,
widthInBlocks: 256 / info.bytesPerBlock,
copyWidthInBlocks: 256 / info.bytesPerBlock - 1,
_success: !isDepthOrStencilTextureFormat(p.format),
},
{
bytesPerRow: 128,
widthInBlocks: 128 / info.bytesPerBlock,
copyWidthInBlocks: 128 / info.bytesPerBlock,
_success: p.method === 'WriteTexture',
},
{
bytesPerRow: 384,
widthInBlocks: 384 / info.bytesPerBlock,
copyWidthInBlocks: 384 / info.bytesPerBlock,
_success: p.method === 'WriteTexture',
},
{
bytesPerRow: 256,
widthInBlocks: (2 * 256) / info.bytesPerBlock,
copyWidthInBlocks: (2 * 256) / info.bytesPerBlock,
_success: false,
},
{
bytesPerRow: undefined,
widthInBlocks: 256 / info.bytesPerBlock,
copyWidthInBlocks: 256 / info.bytesPerBlock,
_success: !(p.copyHeightInBlocks > 1 || p.copyDepth > 1),
},
];
})
)
.fn(t => {
const {
method,
format,
dimension,
bytesPerRow,
widthInBlocks,
copyWidthInBlocks,
copyHeightInBlocks,
copyDepth,
_success,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForSizedTextureFormat(format);
const texture = t.createAlignedTexture(
format,
{
width: widthInBlocks * info.blockWidth,
height: copyHeightInBlocks * info.blockHeight,
depthOrArrayLayers: copyDepth,
},
{ x: 0, y: 0, z: 0 },
dimension
);
const layout = { bytesPerRow, rowsPerImage: copyHeightInBlocks };
const copySize = {
width: copyWidthInBlocks * info.blockWidth,
height: copyHeightInBlocks * info.blockHeight,
depthOrArrayLayers: copyDepth,
};
const { minDataSizeOrOverestimate } = dataBytesForCopyOrOverestimate({
layout,
format,
copySize,
method,
});
t.testRun({ texture }, layout, copySize, {
dataSize: minDataSizeOrOverestimate,
method,
success: _success,
});
});
g.test('bound_on_offset')
.desc(
`
Test that the offset cannot be larger than the linear data size (even for an empty copy).
- for various offsets and data sizes
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.beginSubcases()
.combine('offsetInBlocks', [0, 1, 2])
.combine('dataSizeInBlocks', [0, 1, 2])
)
.fn(t => {
const { offsetInBlocks, dataSizeInBlocks, method } = t.params;
const format = 'rgba8unorm';
const info = getBlockInfoForColorTextureFormat(format);
const offset = offsetInBlocks * info.bytesPerBlock;
const dataSize = dataSizeInBlocks * info.bytesPerBlock;
const texture = t.createTextureTracked({
size: { width: 4, height: 4, depthOrArrayLayers: 1 },
format,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
const success = offset <= dataSize;
t.testRun(
{ texture },
{ offset, bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize, method, success }
);
});