Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ class F extends AllFeaturesMaxLimitsGPUTest {
copyExtent: Required<GPUExtent3DDict>;
},
srcCopyLevel: number,
dstCopyLevel: number
dstCopyLevel: number,
requestedMipLevelCount?: number
): void {
this.skipIfTextureFormatNotSupported(srcFormat, dstFormat);
this.skipIfCopyTextureToTextureNotSupportedForFormat(srcFormat, dstFormat);
Expand All @@ -107,7 +108,7 @@ class F extends AllFeaturesMaxLimitsGPUTest {
isCompressedTextureFormat(dstFormat) && this.isCompatibility
? GPUTextureUsage.TEXTURE_BINDING
: 0;
const mipLevelCount = dimension === '1d' ? 1 : 4;
const mipLevelCount = requestedMipLevelCount ?? (dimension === '1d' ? 1 : 4);

// Create srcTexture and dstTexture
const srcTextureDesc: GPUTextureDescriptor = applyTextureBindingViewDimensionForTest({
Expand Down Expand Up @@ -972,6 +973,68 @@ g.test('color_textures,compressed,non_array')
);
});

g.test('color_textures,compressed,unaligned,non_array')
.desc(
`
Validate the correctness of copyTextureToTexture for block-compressed textures whose mip level 0
size is NOT a multiple of the texel block size, using the 'texture-compression-unaligned' feature.

This mirrors color_textures,compressed,non_array but creates textures with an unaligned mip level 0
(so the top mip level itself has partial edge blocks) and copies at mip level 0. As with non-zero
mip levels, the copy is validated and performed against the physical (rounded-up) size, so the copy
accesses the texture blocks at the edge which are not fully inside the texture.

Tests for all pairs of valid source/destination formats, with the partial edge block in the width,
height, or both dimensions.
`
)
.params(u =>
u
.combine('srcFormat', kCompressedTextureFormats)
.combine('dstFormat', kCompressedTextureFormats)
.filter(({ srcFormat, dstFormat }) => {
const srcBaseFormat = getBaseFormatForTextureFormat(srcFormat);
const dstBaseFormat = getBaseFormatForTextureFormat(dstFormat);
return (
srcFormat === dstFormat ||
(srcBaseFormat !== undefined &&
dstBaseFormat !== undefined &&
srcBaseFormat === dstBaseFormat)
);
})
.beginSubcases()
// Which dimension(s) of mip level 0 have a partial edge block.
.combine('partialEdge', ['width', 'height', 'both'] as const)
.combine('copyBoxOffsets', kCopyBoxOffsetsForWholeDepth)
)
.fn(t => {
const { partialEdge, srcFormat, dstFormat, copyBoxOffsets } = t.params;
t.skipIfDeviceDoesNotHaveFeature('texture-compression-unaligned' as GPUFeatureName);
t.skipIfCopyTextureToTextureNotSupportedForFormat(srcFormat, dstFormat);

// The source and destination formats share the same base format, so they have the same texel
// block size.
const { blockWidth, blockHeight } = getBlockInfoForColorTextureFormat(srcFormat);

// Mip level 0 size: a few full blocks plus a one-texel partial edge block in the selected
// dimension(s). An unaligned mip level 0 is only valid with 'texture-compression-unaligned'.
const width = partialEdge === 'height' ? 4 * blockWidth : 3 * blockWidth + 1;
const height = partialEdge === 'width' ? 4 * blockHeight : 3 * blockHeight + 1;
const size = { width, height, depthOrArrayLayers: 1 };

t.doCopyTextureToTextureTest(
'2d',
size,
size,
srcFormat,
dstFormat,
copyBoxOffsets,
0, // srcCopyLevel: the top (and only) mip level.
0, // dstCopyLevel
1 // mipLevelCount: a single, unaligned mip level.
);
});

g.test('color_textures,non_compressed,array')
.desc(
`
Expand Down
82 changes: 82 additions & 0 deletions src/webgpu/api/operation/command_buffer/image_copy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,88 @@ TODO: Make a variant for depth-stencil formats.
});
});

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()
// Copy box in block units. The mip level 0 size below is 3 full blocks + 1 partial-edge block
// (physical size 4 blocks) in each dimension, so x/y of 3 selects the partial edge block.
.combine('copyCase', [
// Full copy of the physical extent (touches every partial edge block).
{ originInBlocks: { x: 0, y: 0 }, copySizeInBlocks: { width: 4, height: 4 } },
// Just the partial edge column / row / corner.
{ 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 } },
// Interior block that does not touch a partial edge block.
{ 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);

// Unaligned mip level 0: three full blocks plus a one-texel partial edge block in each dimension
// (physical size four blocks). This is only valid with 'texture-compression-unaligned'.
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
export const description = `
Tests for capability checking for the 'texture-compression-unaligned' feature.

When the feature is not enabled, the behavior is unchanged: the size of mip level 0 of a
block-compressed texture must be a multiple of the texel block size. When the feature is enabled,
mip level 0 is allowed to have a size that is not a multiple of the texel block size (i.e. partial
edge blocks).
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import {
kCompressedTextureFormats,
getBlockInfoForTextureFormat,
getRequiredFeatureForTextureFormat,
} from '../../../../format_info.js';
import { UniqueFeaturesOrLimitsGPUTest } from '../../../../gpu_test.js';

export const g = makeTestGroup(UniqueFeaturesOrLimitsGPUTest);

const kTextureCompressionUnaligned = 'texture-compression-unaligned' as GPUFeatureName;

g.test('createTexture,unaligned_size')
.desc(
`Test that creating a compressed texture whose mip level 0 size is not a multiple of the texel
block size succeeds if and only if 'texture-compression-unaligned' is enabled.`
)
.params(u =>
u
.combine('format', kCompressedTextureFormats)
.combine('enable_feature', [true, false])
// The unaligned dimension(s) of mip level 0 being tested.
.combine('sizeCase', ['width', 'height', 'both', 'single'] as const)
)
.beforeAllSubcases(t => {
const { format, enable_feature } = t.params;

const requiredFeatures: GPUFeatureName[] = [];
const formatFeature = getRequiredFeatureForTextureFormat(format);
if (formatFeature) {
requiredFeatures.push(formatFeature);
}
if (enable_feature) {
requiredFeatures.push(kTextureCompressionUnaligned);
}

t.selectDeviceOrSkipTestCase({ requiredFeatures });
})
.fn(t => {
const { format, enable_feature, sizeCase } = t.params;
t.skipIfTextureFormatNotSupported(format);

const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(format);

// Construct a mip level 0 size that is not a multiple of the texel block size.
const size = (() => {
switch (sizeCase) {
case 'width':
return [blockWidth + 1, blockHeight, 1];
case 'height':
return [blockWidth, blockHeight + 1, 1];
case 'both':
return [blockWidth + 1, blockHeight + 1, 1];
case 'single':
// A single partial edge block in both dimensions.
return [1, 1, 1];
}
})();

const descriptor: GPUTextureDescriptor = {
size,
format,
usage: GPUTextureUsage.TEXTURE_BINDING,
};

// Without the feature, an unaligned mip level 0 size is a validation error.
t.expectValidationError(() => {
t.createTextureTracked(descriptor);
}, !enable_feature);
});
32 changes: 26 additions & 6 deletions src/webgpu/api/validation/createTexture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export const description = `createTexture validation tests.`;

import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { assert, makeValueTestVariant } from '../../../common/util/util.js';
import { assert, hasFeature, makeValueTestVariant } from '../../../common/util/util.js';
import {
kTextureDimensions,
kTextureUsages,
Expand Down Expand Up @@ -494,9 +494,17 @@ g.test('texture_size,default_value_and_smallest_size,compressed_format')
usage: GPUTextureUsage.TEXTURE_BINDING,
};

// With 'texture-compression-unaligned', mip level 0 is no longer required to be a multiple of
Comment thread
Kangz marked this conversation as resolved.
// the texel block size, so every (small, in-range) size in this test becomes valid.
const supportsUnaligned = hasFeature(
t.device.features,
'texture-compression-unaligned' as GPUFeatureName
);
const success = supportsUnaligned || _success;

t.expectValidationError(() => {
t.createTextureTracked(descriptor);
}, !_success);
}, !success);
});

g.test('texture_size,1d_texture')
Expand Down Expand Up @@ -755,9 +763,15 @@ g.test('texture_size,2d_texture,compressed_format')
usage: GPUTextureUsage.TEXTURE_BINDING,
};

// With 'texture-compression-unaligned', mip level 0 of a compressed texture is no longer
// required to be a multiple of the texel block size, so unaligned widths/heights are valid.
const supportsUnaligned = hasFeature(
t.device.features,
'texture-compression-unaligned' as GPUFeatureName
);
const success =
size[0] % info.blockWidth === 0 &&
size[1] % info.blockHeight === 0 &&
(supportsUnaligned ||
(size[0] % info.blockWidth === 0 && size[1] % info.blockHeight === 0)) &&
size[0] <= t.device.limits.maxTextureDimension2D &&
size[1] <= t.device.limits.maxTextureDimension2D &&
size[2] <= t.device.limits.maxTextureArrayLayers;
Expand Down Expand Up @@ -989,9 +1003,15 @@ g.test('texture_size,3d_texture,compressed_format')
usage: GPUTextureUsage.TEXTURE_BINDING,
};

// With 'texture-compression-unaligned', mip level 0 of a compressed texture is no longer
// required to be a multiple of the texel block size, so unaligned widths/heights are valid.
const supportsUnaligned = hasFeature(
t.device.features,
'texture-compression-unaligned' as GPUFeatureName
);
const success =
size[0] % info.blockWidth === 0 &&
size[1] % info.blockHeight === 0 &&
(supportsUnaligned ||
(size[0] % info.blockWidth === 0 && size[1] % info.blockHeight === 0)) &&
size[0] <= maxTextureDimension3D &&
size[1] <= maxTextureDimension3D &&
size[2] <= maxTextureDimension3D &&
Expand Down
1 change: 1 addition & 0 deletions src/webgpu/capability_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ export const kFeatureNameInfo: {
'texture-component-swizzle': {},
'subgroup-size-control': {},
['atomic-vec2u-min-max' as GPUFeatureName]: {},
['texture-compression-unaligned' as GPUFeatureName]: {},
};
/** List of all GPUFeatureName values. */
export const kFeatureNames = keysOf(kFeatureNameInfo);
Expand Down
Loading