export const description = `Texture related validation tests for B2T copy and T2B copy and writeTexture.`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { assert } from '../../../../common/util/util.js';
import { kTextureDimensions, kTextureUsages } from '../../../capability_info.js';
import { GPUConst } from '../../../constants.js';
import {
getBlockInfoForColorTextureFormat,
getBlockInfoForSizedTextureFormat,
getBlockInfoForTextureFormat,
isDepthOrStencilTextureFormat,
kColorTextureFormats,
kSizedTextureFormats,
textureFormatAndDimensionPossiblyCompatible,
} from '../../../format_info.js';
import { kResourceStates } from '../../../gpu_test.js';
import { align } from '../../../util/math.js';
import { virtualMipSize } from '../../../util/texture/base.js';
import { kImageCopyTypes } from '../../../util/texture/layout.js';
import * as vtu from '../validation_test_utils.js';
import {
ImageCopyTest,
texelBlockAlignmentTestExpanderForValueToCoordinate,
formatCopyableWithMethod,
getACopyableAspectWithMethod,
} from './image_copy.js';
export const g = makeTestGroup(ImageCopyTest);
g.test('valid')
.desc(
`
Test that the texture must be valid and not destroyed.
- for all copy methods
- for all texture states
- for various dimensions
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('textureState', kResourceStates)
.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)
)
.fn(t => {
const { method, textureState, size, dimension } = t.params;
const texture = vtu.createTextureWithState(t, textureState, {
size,
dimension,
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
const success = textureState === 'valid';
const submit = textureState !== 'invalid';
t.testRun(
{ texture },
{ bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize: 1, method, success, submit }
);
});
g.test('texture,device_mismatch')
.desc('Tests the image copies cannot be called with a texture created from another device')
.paramsSubcasesOnly(u =>
u.combine('method', kImageCopyTypes).combine('mismatched', [true, false])
)
.beforeAllSubcases(t => t.usesMismatchedDevice())
.fn(t => {
const { method, mismatched } = t.params;
const sourceDevice = mismatched ? t.mismatchedDevice : t.device;
const texture = t.trackForCleanup(
sourceDevice.createTexture({
size: { width: 4, height: 4, depthOrArrayLayers: 1 },
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
})
);
t.testRun(
{ texture },
{ bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize: 1, method, success: !mismatched }
);
});
g.test('usage')
.desc(
`
The texture must have the appropriate COPY_SRC/COPY_DST usage.
- for various copy methods
- for various dimensions
- for various usages
`
)
.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('usage0', kTextureUsages)
.combine('usage1', kTextureUsages)
.unless(
({ usage0, usage1, dimension }) =>
((usage0 | usage1) & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 &&
(dimension === '1d' || dimension === '3d')
)
.unless(({ usage0, usage1 }) => {
return (
usage0 === GPUConst.TextureUsage.TRANSIENT_ATTACHMENT ||
usage1 === GPUConst.TextureUsage.TRANSIENT_ATTACHMENT
);
})
)
.fn(t => {
const { usage0, usage1, method, size, dimension } = t.params;
const usage = usage0 | usage1;
const texture = t.createTextureTracked({
size,
dimension,
format: 'rgba8unorm',
usage,
});
const success =
method === 'CopyT2B'
? (usage & GPUTextureUsage.COPY_SRC) !== 0
: (usage & GPUTextureUsage.COPY_DST) !== 0;
t.testRun(
{ texture },
{ bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize: 1, method, success }
);
});
g.test('sample_count')
.desc(
`
Test that multisampled textures cannot be copied.
- for various copy methods
- multisampled or not
Note: we don't test 1D, 2D array and 3D textures because multisample is not supported them.
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.beginSubcases()
.combine('sampleCount', [1, 4])
)
.fn(t => {
const { sampleCount, method } = t.params;
const texture = t.createTextureTracked({
size: { width: 4, height: 4, depthOrArrayLayers: 1 },
sampleCount,
format: 'rgba8unorm',
usage:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT,
});
const success = sampleCount === 1;
t.testRun(
{ texture },
{ bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize: 1, method, success }
);
});
g.test('mip_level')
.desc(
`
Test that the mipLevel of the copy must be in range of the texture.
- for various copy methods
- for various dimensions
- for several mipLevelCounts
- for several target/source mipLevels`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combineWithParams([
{ dimension: '1d', size: [32, 1, 1] },
{ dimension: '2d', size: [32, 32, 1] },
{ dimension: '2d', size: [32, 32, 3] },
{ dimension: '3d', size: [32, 32, 3] },
] as const)
.beginSubcases()
.combine('mipLevelCount', [1, 3, 5])
.unless(p => p.dimension === '1d' && p.mipLevelCount !== 1)
.combine('mipLevel', [0, 1, 3, 4])
)
.fn(t => {
const { mipLevelCount, mipLevel, method, size, dimension } = t.params;
const texture = t.createTextureTracked({
size,
dimension,
mipLevelCount,
format: 'rgba8unorm',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
const success = mipLevel < mipLevelCount;
t.testRun(
{ texture, mipLevel },
{ bytesPerRow: 0 },
{ width: 0, height: 0, depthOrArrayLayers: 0 },
{ dataSize: 1, method, success }
);
});
g.test('format')
.desc(
`
Test the copy must be a full subresource if the texture's format is depth/stencil format.
- for various copy methods
- for various dimensions
- for all sized formats
- for a couple target/source mipLevels
- for some modifier (or not) for the full copy size
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combineWithParams([
{ depthOrArrayLayers: 1, dimension: '1d' },
{ depthOrArrayLayers: 1, dimension: '2d' },
{ depthOrArrayLayers: 3, dimension: '2d' },
{ depthOrArrayLayers: 32, dimension: '3d' },
] as const)
.combine('format', kSizedTextureFormats)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.filter(formatCopyableWithMethod)
.beginSubcases()
.combine('mipLevel', [0, 2])
.unless(p => p.dimension === '1d' && p.mipLevel !== 0)
.combine('copyWidthModifier', [0, -1])
.combine('copyHeightModifier', [0, -1])
.expand('copyDepthModifier', ({ dimension: d }) => (d === '3d' ? [0, -1] : [0]))
)
.fn(t => {
const {
method,
depthOrArrayLayers,
dimension,
format,
mipLevel,
copyWidthModifier,
copyHeightModifier,
copyDepthModifier,
} = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForSizedTextureFormat(format);
const size = { width: 32 * info.blockWidth, height: 32 * info.blockHeight, depthOrArrayLayers };
if (dimension === '1d') {
size.height = 1;
}
const texture = t.createTextureTracked({
size,
dimension,
format,
mipLevelCount: dimension === '1d' ? 1 : 5,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
let success = true;
if (
isDepthOrStencilTextureFormat(format) &&
(copyWidthModifier !== 0 || copyHeightModifier !== 0 || copyDepthModifier !== 0)
) {
success = false;
}
const levelSize = virtualMipSize(dimension, size, mipLevel);
const copySize = [
levelSize[0] + copyWidthModifier * info.blockWidth,
levelSize[1] + copyHeightModifier * info.blockHeight,
levelSize[2] + copyDepthModifier,
];
t.testRun(
{ texture, mipLevel, aspect: getACopyableAspectWithMethod({ format, method }) },
{ bytesPerRow: 512, rowsPerImage: 32 },
copySize,
{
dataSize: 512 * 32 * 32,
method,
success,
}
);
});
g.test('origin_alignment')
.desc(
`
Test that the texture copy origin must be aligned to the format's block size.
- for various copy methods
- for all color formats (depth stencil formats require a full copy)
- for X, Y and Z coordinates
- for various values for that coordinate depending on the block size
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kColorTextureFormats)
.filter(formatCopyableWithMethod)
.combineWithParams([
{ depthOrArrayLayers: 1, dimension: '1d' },
{ depthOrArrayLayers: 1, dimension: '2d' },
{ depthOrArrayLayers: 3, dimension: '2d' },
{ depthOrArrayLayers: 3, dimension: '3d' },
] as const)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combine('coordinateToTest', ['x', 'y', 'z'] as const)
.unless(p => p.dimension === '1d' && p.coordinateToTest !== 'x')
.expand('valueToCoordinate', texelBlockAlignmentTestExpanderForValueToCoordinate)
)
.fn(t => {
const { valueToCoordinate, coordinateToTest, format, method, depthOrArrayLayers, dimension } =
t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForTextureFormat(format);
const size = { width: 0, height: 0, depthOrArrayLayers };
const origin = { x: 0, y: 0, z: 0 };
let success = true;
origin[coordinateToTest] = valueToCoordinate;
switch (coordinateToTest) {
case 'x': {
success = origin.x % info.blockWidth === 0;
break;
}
case 'y': {
success = origin.y % info.blockHeight === 0;
break;
}
}
const texture = t.createAlignedTexture(format, size, origin, dimension);
t.testRun({ texture, origin }, { bytesPerRow: 0, rowsPerImage: 0 }, size, {
dataSize: 1,
method,
success,
});
});
g.test('size_alignment')
.desc(
`
Test that the copy size must be aligned to the texture's format's block size.
- for various copy methods
- for all formats (depth-stencil formats require a full copy)
- for all texture dimensions
- for the size's parameters to test (width / height / depth)
- for various values for that copy size parameters, depending on the block size
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('format', kColorTextureFormats)
.filter(formatCopyableWithMethod)
.combine('dimension', kTextureDimensions)
.filter(({ dimension, format }) =>
textureFormatAndDimensionPossiblyCompatible(dimension, format)
)
.beginSubcases()
.combine('coordinateToTest', ['width', 'height', 'depthOrArrayLayers'] as const)
.unless(p => p.dimension === '1d' && p.coordinateToTest !== 'width')
.expand('valueToCoordinate', texelBlockAlignmentTestExpanderForValueToCoordinate)
)
.fn(t => {
const { valueToCoordinate, coordinateToTest, dimension, format, method } = t.params;
t.skipIfTextureFormatNotSupported(format);
t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension);
const info = getBlockInfoForColorTextureFormat(format);
const size = { width: 0, height: 0, depthOrArrayLayers: 0 };
const origin = { x: 0, y: 0, z: 0 };
let success = true;
size[coordinateToTest] = valueToCoordinate;
switch (coordinateToTest) {
case 'width': {
success = size.width % info.blockWidth === 0;
break;
}
case 'height': {
success = size.height % info.blockHeight === 0;
break;
}
}
const texture = t.createAlignedTexture(format, size, origin, dimension);
const bytesPerRow = align(
Math.max(1, Math.ceil(size.width / info.blockWidth)) * info.bytesPerBlock,
256
);
const rowsPerImage = Math.ceil(size.height / info.blockHeight);
t.testRun({ texture, origin }, { bytesPerRow, rowsPerImage }, size, {
dataSize: 1,
method,
success,
});
});
g.test('copy_rectangle')
.desc(
`
Test that the max corner of the copy rectangle (origin+copySize) must be inside the texture.
- for various copy methods
- for all dimensions
- for the X, Y and Z dimensions
- for various origin and copy size values (and texture sizes)
- for various mip levels
`
)
.params(u =>
u
.combine('method', kImageCopyTypes)
.combine('dimension', kTextureDimensions)
.beginSubcases()
.combine('originValue', [7, 8])
.combine('copySizeValue', [7, 8])
.combine('textureSizeValue', [14, 15])
.combine('mipLevel', [0, 2])
.combine('coordinateToTest', [0, 1, 2] as const)
.unless(p => p.dimension === '1d' && (p.coordinateToTest !== 0 || p.mipLevel !== 0))
)
.fn(t => {
const {
originValue,
copySizeValue,
textureSizeValue,
mipLevel,
coordinateToTest,
method,
dimension,
} = t.params;
const format = 'rgba8unorm';
const info = getBlockInfoForColorTextureFormat(format);
const origin = [0, 0, 0];
const copySize = [0, 0, 0];
const textureSize = { width: 16 << mipLevel, height: 16 << mipLevel, depthOrArrayLayers: 16 };
if (dimension === '1d') {
textureSize.height = 1;
textureSize.depthOrArrayLayers = 1;
}
const success = originValue + copySizeValue <= textureSizeValue;
origin[coordinateToTest] = originValue;
copySize[coordinateToTest] = copySizeValue;
switch (coordinateToTest) {
case 0: {
textureSize.width = textureSizeValue << mipLevel;
break;
}
case 1: {
textureSize.height = textureSizeValue << mipLevel;
break;
}
case 2: {
textureSize.depthOrArrayLayers =
dimension === '3d' ? textureSizeValue << mipLevel : textureSizeValue;
break;
}
}
const texture = t.createTextureTracked({
size: textureSize,
dimension,
mipLevelCount: dimension === '1d' ? 1 : 3,
format,
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
});
assert(copySize[0] % info.blockWidth === 0);
const bytesPerRow = align(copySize[0] / info.blockWidth, 256);
assert(copySize[1] % info.blockHeight === 0);
const rowsPerImage = copySize[1] / info.blockHeight;
t.testRun({ texture, origin, mipLevel }, { bytesPerRow, rowsPerImage }, copySize, {
dataSize: 1,
method,
success,
});
});