From 16a83e2eaf4728bdd7c2fba80a95e801d3155c22 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Thu, 25 Jun 2026 10:11:26 -0700 Subject: [PATCH 1/6] Add 'texture-compression-unaligned' feature name. --- src/webgpu/capability_info.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts index 99c1dd29c06c..98ec04088291 100644 --- a/src/webgpu/capability_info.ts +++ b/src/webgpu/capability_info.ts @@ -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); From e3a9ef9c9a78d6e79d8f6654131d249a28cb5604 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Thu, 25 Jun 2026 10:35:29 -0700 Subject: [PATCH 2/6] createTexture tests pass when texture-compression-unaligned feature is supported. --- .../api/validation/createTexture.spec.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/webgpu/api/validation/createTexture.spec.ts b/src/webgpu/api/validation/createTexture.spec.ts index b1bf2cd9725d..e23fbc38862c 100644 --- a/src/webgpu/api/validation/createTexture.spec.ts +++ b/src/webgpu/api/validation/createTexture.spec.ts @@ -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, @@ -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 + // 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') @@ -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; From 64940060ee5e33ce4487b3e80d8bf1f7c0180e6c Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Thu, 25 Jun 2026 10:40:05 -0700 Subject: [PATCH 3/6] Add test to ensure createTexture with unaligned extents fails without texture-compression-unaligned, but succeeds otherwise. --- .../texture_compression_unaligned.spec.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/webgpu/api/validation/capability_checks/features/texture_compression_unaligned.spec.ts diff --git a/src/webgpu/api/validation/capability_checks/features/texture_compression_unaligned.spec.ts b/src/webgpu/api/validation/capability_checks/features/texture_compression_unaligned.spec.ts new file mode 100644 index 000000000000..e9e532147447 --- /dev/null +++ b/src/webgpu/api/validation/capability_checks/features/texture_compression_unaligned.spec.ts @@ -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); + }); From 367f96907b960f6bee92c311e62d9b9b5776e953 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Fri, 26 Jun 2026 00:26:58 -0700 Subject: [PATCH 4/6] Add test to validate correctness of copyTextureToTexture for block compressed textures with unaligned sizes. --- .../copyTextureToTexture.spec.ts | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts b/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts index 30e66b5d9ab6..f9d2fdfbbe6f 100644 --- a/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts +++ b/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts @@ -94,7 +94,8 @@ class F extends AllFeaturesMaxLimitsGPUTest { copyExtent: Required; }, srcCopyLevel: number, - dstCopyLevel: number + dstCopyLevel: number, + requestedMipLevelCount?: number ): void { this.skipIfTextureFormatNotSupported(srcFormat, dstFormat); this.skipIfCopyTextureToTextureNotSupportedForFormat(srcFormat, dstFormat); @@ -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({ @@ -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( ` From 982d796f1e68102f179007fd38a9d6c891607e96 Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Fri, 26 Jun 2026 00:37:42 -0700 Subject: [PATCH 5/6] Validate correctness of image copies of compressed textures with unaligned dimensions. --- .../command_buffer/image_copy.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/webgpu/api/operation/command_buffer/image_copy.spec.ts b/src/webgpu/api/operation/command_buffer/image_copy.spec.ts index 8f18da8d053e..88813731d147 100644 --- a/src/webgpu/api/operation/command_buffer/image_copy.spec.ts +++ b/src/webgpu/api/operation/command_buffer/image_copy.spec.ts @@ -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( From 520249ef8ad690db61018a20e6391e6271b688bd Mon Sep 17 00:00:00 2001 From: Ignacio Castano Date: Fri, 26 Jun 2026 10:28:07 -0700 Subject: [PATCH 6/6] Fix test `texture_size,3d_texture,compressed_format`. --- src/webgpu/api/validation/createTexture.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/webgpu/api/validation/createTexture.spec.ts b/src/webgpu/api/validation/createTexture.spec.ts index e23fbc38862c..79d59de5e00e 100644 --- a/src/webgpu/api/validation/createTexture.spec.ts +++ b/src/webgpu/api/validation/createTexture.spec.ts @@ -1003,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 &&