From 6c9228a3b744534a167f828a53ac605b0ff72b91 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Wed, 9 Jul 2025 11:08:07 -0700 Subject: [PATCH] Add copyBufferToTexture and copyTextureToBuffer validation tests In particular test that for copyBufferToTexture, and copyTextureToBuffer * bytesPerRow must be a multiple of 256 * offset must be a multiple of bytesPerBlock * the last row does not need to be a multiple of 256 In other words, If the copy size is 4x2 of a r8unorm texture that's 4 bytes per row. To get from row 0 to row 1 in the buffer, bytesPerRow must be a multiple of 256. But, the size requirement for the buffer is only 256 + 4, not 256 * 2 * origin.x must be a multiple of blockWidth * origin.y must be a multiple of blockHeight * copySize.width must be a multiple of blockWidth * copySize.height must be a multiple of blockHeight --- .../image_copy/buffer_texture_copies.spec.ts | 162 +++++++++++++++++- src/webgpu/gpu_test.ts | 7 + 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts b/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts index e2bf92916757..6de45ac063ff 100644 --- a/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts +++ b/src/webgpu/api/validation/image_copy/buffer_texture_copies.spec.ts @@ -5,12 +5,17 @@ the general image_copy tests, or by destroyed,*. import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { assert, unreachable } from '../../../../common/util/util.js'; -import { kBufferUsages, kTextureUsages } from '../../../capability_info.js'; +import { kBufferUsages, kTextureDimensions, kTextureUsages } from '../../../capability_info.js'; import { GPUConst } from '../../../constants.js'; import { kDepthStencilFormats, depthStencilBufferTextureCopySupported, depthStencilFormatAspectSize, + kColorTextureFormats, + canCopyFromAllAspectsOfTextureFormat, + canCopyToAllAspectsOfTextureFormat, + textureFormatAndDimensionPossiblyCompatible, + getBlockInfoForColorTextureFormat, } from '../../../format_info.js'; import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; import { align } from '../../../util/math.js'; @@ -442,3 +447,158 @@ g.test('device_mismatch') t.testCopyTextureToBuffer({ texture }, { buffer }, textureSize, isValid); } }); + +g.test('offset_and_bytesPerRow') + .desc( + `Test that for copyBufferToTexture, and copyTextureToBuffer + * bytesPerRow must be a multiple of 256 + * offset must be a multiple of bytesPerBlock + * the last row does not need to be a multiple of 256 + In other words, If the copy size is 4x2 of a r8unorm texture that's 4 bytes per row. + To get from row 0 to row 1 in the buffer, bytesPerRow must be a multiple of 256. + But, the size requirement for the buffer is only 256 + 4, not 256 * 2 + * origin.x must be a multiple of blockWidth + * origin.y must be a multiple of blockHeight + * copySize.width must be a multiple of blockWidth + * copySize.height must be a multiple of blockHeight +` + ) + .params(u => + u + .combine('format', kColorTextureFormats) + .combine('copyType', ['CopyB2T', 'CopyT2B'] as const) + .filter( + ({ format }) => + canCopyToAllAspectsOfTextureFormat(format) && canCopyFromAllAspectsOfTextureFormat(format) + ) + .combine('dimension', kTextureDimensions) + .filter(({ dimension, format }) => + textureFormatAndDimensionPossiblyCompatible(dimension, format) + ) + .beginSubcases() + .combineWithParams( + /* prettier-ignore */ [ + { xInBlocks: 1 , yInBlocks: 1 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 1 , bytesPerRowAlign: 256 }, // good + { xInBlocks: 0 , yInBlocks: 0 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 1.5, bytesPerRowAlign: 256 }, // bad as offset is not blockSize + { xInBlocks: 0 , yInBlocks: 0 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 0 , bytesPerRowAlign: 128 }, // bad as bytesPerBlock is not multiple of 256 + { xInBlocks: 0 , yInBlocks: 0 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 0 , bytesPerRowAlign: 384 }, // bad as bytesPerBlock is not multiple of 256 + { xInBlocks: 1.5, yInBlocks: 0 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 0 , bytesPerRowAlign: 256 }, // bad as origin.x is not multiple of blockSize + { xInBlocks: 0 , yInBlocks: 1.5, copyWidthInBlocks: 64 , copyHeightInBlocks: 2 , offsetInBlocks: 0 , bytesPerRowAlign: 256 }, // bad as origin.y is not multiple of blockSize + { xInBlocks: 0 , yInBlocks: 0 , copyWidthInBlocks: 64.5, copyHeightInBlocks: 2 , offsetInBlocks: 0 , bytesPerRowAlign: 256 }, // bad as copySize.width is not multiple of blockSize + { xInBlocks: 0 , yInBlocks: 0 , copyWidthInBlocks: 64 , copyHeightInBlocks: 2.5, offsetInBlocks: 0 , bytesPerRowAlign: 256 }, // bad as copySize.height is not multiple of blockSize + ] as const + ) + // Remove non-integer offsetInBlocks, copyWidthInBlocks, copyHeightInBlocks if bytesPerBlock === 1 + .unless( + t => + (t.offsetInBlocks % 1 !== 0 || + t.copyWidthInBlocks % 1 !== 0 || + t.copyHeightInBlocks % 1 !== 0) && + getBlockInfoForColorTextureFormat(t.format).bytesPerBlock > 1 + ) + // Remove yInBlocks > 0 if dimension is 1d + .unless(t => t.dimension === '1d' && t.yInBlocks > 0) + ) + .fn(t => { + const { + copyType, + format, + dimension, + xInBlocks, + yInBlocks, + offsetInBlocks, + copyWidthInBlocks, + copyHeightInBlocks, + bytesPerRowAlign, + } = t.params; + t.skipIfTextureFormatNotSupported(format); + t.skipIfTextureFormatAndDimensionNotCompatible(format, dimension); + if (copyType === 'CopyT2B') { + t.skipIfTextureFormatDoesNotSupportCopyTextureToBuffer(format); + } + + const info = getBlockInfoForColorTextureFormat(format); + + // make a texture big enough that we have room for our copySize and our origin. + // Note that xxxInBlocks may be factional so that we test origins and sizes not aligned to blocks. + const widthBlocks = Math.ceil(xInBlocks) + Math.ceil(copyWidthInBlocks); + const heightBlocks = Math.ceil(yInBlocks) + Math.ceil(copyHeightInBlocks); + let copySizeBlocks = [copyWidthInBlocks, copyHeightInBlocks, 1]; + let texSizeBlocks = [widthBlocks, heightBlocks, 1]; + if (dimension === '1d') { + copySizeBlocks = [copyWidthInBlocks, 1, 1]; + texSizeBlocks = [widthBlocks, 1, 1]; + } + + const origin = [ + Math.ceil(xInBlocks * info.blockWidth), + Math.ceil(yInBlocks * info.blockHeight), + 0, + ]; + const copySize = [ + Math.ceil(copySizeBlocks[0] * info.blockWidth), + Math.ceil(copySizeBlocks[1] * info.blockHeight), + copySizeBlocks[2], + ]; + const textureSize = [ + texSizeBlocks[0] * info.blockWidth, + texSizeBlocks[1] * info.blockHeight, + texSizeBlocks[2], + ] as const; + const textureBytePerRow = info.bytesPerBlock * texSizeBlocks[0]; + const rowsPerImage = Math.ceil(copySizeBlocks[1]); + const offset = Math.ceil(offsetInBlocks * info.bytesPerBlock); + const bytesPerRow = align(textureBytePerRow, bytesPerRowAlign); + + // Make sure our buffer is big enough for the required alignment + // and offset but no bigger. + const totalRows = rowsPerImage * copySizeBlocks[2]; + const bufferSize = offset + (totalRows - 1) * bytesPerRow + textureBytePerRow; + + const buffer = t.createBufferTracked({ + label: `buffer(${bufferSize})`, + size: bufferSize, + usage: copyType === 'CopyB2T' ? GPUBufferUsage.COPY_SRC : GPUBufferUsage.COPY_DST, + }); + + const texture = t.createTextureTracked({ + size: textureSize, + format, + dimension, + usage: copyType === 'CopyB2T' ? GPUTextureUsage.COPY_DST : GPUTextureUsage.COPY_SRC, + }); + + const shouldSucceed = + offset % info.bytesPerBlock === 0 && + bytesPerRow % 256 === 0 && + origin[0] % info.blockWidth === 0 && + origin[1] % info.blockHeight === 0 && + copySize[0] % info.blockWidth === 0 && + copySize[1] % info.blockHeight === 0; + + t.debug( + () => + `offset: ${offset}, bytesPerRow: ${bytesPerRow}, copySize: ${copySize}, origin: ${origin}` + ); + + switch (copyType) { + case 'CopyB2T': { + t.testCopyBufferToTexture( + { buffer, offset, bytesPerRow }, + { texture, origin }, + copySize, + shouldSucceed + ); + break; + } + case 'CopyT2B': { + t.testCopyTextureToBuffer( + { texture, origin }, + { buffer, offset, bytesPerRow }, + copySize, + shouldSucceed + ); + break; + } + } + }); diff --git a/src/webgpu/gpu_test.ts b/src/webgpu/gpu_test.ts index dd47246281c6..cb3da556a2da 100644 --- a/src/webgpu/gpu_test.ts +++ b/src/webgpu/gpu_test.ts @@ -637,6 +637,13 @@ export class GPUTestBase extends Fixture { } } + skipIfTextureFormatDoesNotSupportCopyTextureToBuffer(format: GPUTextureFormat) { + this.skipIf( + !this.canCallCopyTextureToBufferWithTextureFormat(format), + `can not use copyTextureToBuffer with ${format}` + ); + } + /** Skips this test case if the `langFeature` is *not* supported. */ skipIfLanguageFeatureNotSupported(langFeature: WGSLLanguageFeature) { if (!this.hasLanguageFeature(langFeature)) {