diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts | 1597 |
1 files changed, 1597 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts new file mode 100644 index 0000000000..4c1bff2302 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/command_buffer/copyTextureToTexture.spec.ts @@ -0,0 +1,1597 @@ +export const description = `copyTextureToTexture operation tests`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert, memcpy, unreachable } from '../../../../common/util/util.js'; +import { + kTextureFormatInfo, + kRegularTextureFormats, + SizedTextureFormat, + kCompressedTextureFormats, + depthStencilFormatAspectSize, + DepthStencilFormat, + kBufferSizeAlignment, + kDepthStencilFormats, + kMinDynamicBufferOffsetAlignment, + kTextureDimensions, + textureDimensionAndFormatCompatible, +} from '../../../capability_info.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { makeBufferWithContents } from '../../../util/buffer.js'; +import { checkElementsEqual, checkElementsEqualEither } from '../../../util/check_contents.js'; +import { align } from '../../../util/math.js'; +import { physicalMipSize } from '../../../util/texture/base.js'; +import { DataArrayGenerator } from '../../../util/texture/data_generation.js'; +import { kBytesPerRowAlignment, dataBytesForCopyOrFail } from '../../../util/texture/layout.js'; + +const dataGenerator = new DataArrayGenerator(); + +class F extends GPUTest { + GetInitialDataPerMipLevel( + dimension: GPUTextureDimension, + textureSize: Required<GPUExtent3DDict>, + format: SizedTextureFormat, + mipLevel: number + ): Uint8Array { + const textureSizeAtLevel = physicalMipSize(textureSize, format, dimension, mipLevel); + const bytesPerBlock = kTextureFormatInfo[format].bytesPerBlock; + const blockWidthInTexel = kTextureFormatInfo[format].blockWidth; + const blockHeightInTexel = kTextureFormatInfo[format].blockHeight; + const blocksPerSubresource = + (textureSizeAtLevel.width / blockWidthInTexel) * + (textureSizeAtLevel.height / blockHeightInTexel); + + const byteSize = bytesPerBlock * blocksPerSubresource * textureSizeAtLevel.depthOrArrayLayers; + return dataGenerator.generateView(byteSize); + } + + GetInitialStencilDataPerMipLevel( + textureSize: Required<GPUExtent3DDict>, + format: DepthStencilFormat, + mipLevel: number + ): Uint8Array { + const textureSizeAtLevel = physicalMipSize(textureSize, format, '2d', mipLevel); + const aspectBytesPerBlock = depthStencilFormatAspectSize(format, 'stencil-only'); + const byteSize = + aspectBytesPerBlock * + textureSizeAtLevel.width * + textureSizeAtLevel.height * + textureSizeAtLevel.depthOrArrayLayers; + return dataGenerator.generateView(byteSize); + } + + DoCopyTextureToTextureTest( + dimension: GPUTextureDimension, + srcTextureSize: Required<GPUExtent3DDict>, + dstTextureSize: Required<GPUExtent3DDict>, + srcFormat: SizedTextureFormat, + dstFormat: SizedTextureFormat, + copyBoxOffsets: { + srcOffset: { x: number; y: number; z: number }; + dstOffset: { x: number; y: number; z: number }; + copyExtent: Required<GPUExtent3DDict>; + }, + srcCopyLevel: number, + dstCopyLevel: number + ): void { + const mipLevelCount = dimension === '1d' ? 1 : 4; + + // Create srcTexture and dstTexture + const srcTextureDesc: GPUTextureDescriptor = { + dimension, + size: srcTextureSize, + format: srcFormat, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, + mipLevelCount, + }; + const srcTexture = this.device.createTexture(srcTextureDesc); + this.trackForCleanup(srcTexture); + const dstTextureDesc: GPUTextureDescriptor = { + dimension, + size: dstTextureSize, + format: dstFormat, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, + mipLevelCount, + }; + const dstTexture = this.device.createTexture(dstTextureDesc); + this.trackForCleanup(dstTexture); + + // Fill the whole subresource of srcTexture at srcCopyLevel with initialSrcData. + const initialSrcData = this.GetInitialDataPerMipLevel( + dimension, + srcTextureSize, + srcFormat, + srcCopyLevel + ); + const srcTextureSizeAtLevel = physicalMipSize( + srcTextureSize, + srcFormat, + dimension, + srcCopyLevel + ); + const bytesPerBlock = kTextureFormatInfo[srcFormat].bytesPerBlock; + const blockWidth = kTextureFormatInfo[srcFormat].blockWidth; + const blockHeight = kTextureFormatInfo[srcFormat].blockHeight; + const srcBlocksPerRow = srcTextureSizeAtLevel.width / blockWidth; + const srcBlockRowsPerImage = srcTextureSizeAtLevel.height / blockHeight; + this.device.queue.writeTexture( + { texture: srcTexture, mipLevel: srcCopyLevel }, + initialSrcData, + { + bytesPerRow: srcBlocksPerRow * bytesPerBlock, + rowsPerImage: srcBlockRowsPerImage, + }, + srcTextureSizeAtLevel + ); + + // Copy the region specified by copyBoxOffsets from srcTexture to dstTexture. + const dstTextureSizeAtLevel = physicalMipSize( + dstTextureSize, + dstFormat, + dimension, + dstCopyLevel + ); + const minWidth = Math.min(srcTextureSizeAtLevel.width, dstTextureSizeAtLevel.width); + const minHeight = Math.min(srcTextureSizeAtLevel.height, dstTextureSizeAtLevel.height); + const minDepth = Math.min( + srcTextureSizeAtLevel.depthOrArrayLayers, + dstTextureSizeAtLevel.depthOrArrayLayers + ); + + const appliedSrcOffset = { + x: Math.min(copyBoxOffsets.srcOffset.x * blockWidth, minWidth), + y: Math.min(copyBoxOffsets.srcOffset.y * blockHeight, minHeight), + z: Math.min(copyBoxOffsets.srcOffset.z, minDepth), + }; + const appliedDstOffset = { + x: Math.min(copyBoxOffsets.dstOffset.x * blockWidth, minWidth), + y: Math.min(copyBoxOffsets.dstOffset.y * blockHeight, minHeight), + z: Math.min(copyBoxOffsets.dstOffset.z, minDepth), + }; + + const appliedCopyWidth = Math.max( + minWidth + + copyBoxOffsets.copyExtent.width * blockWidth - + Math.max(appliedSrcOffset.x, appliedDstOffset.x), + 0 + ); + const appliedCopyHeight = Math.max( + minHeight + + copyBoxOffsets.copyExtent.height * blockHeight - + Math.max(appliedSrcOffset.y, appliedDstOffset.y), + 0 + ); + assert(appliedCopyWidth % blockWidth === 0 && appliedCopyHeight % blockHeight === 0); + + const appliedCopyDepth = Math.max( + 0, + minDepth + + copyBoxOffsets.copyExtent.depthOrArrayLayers - + Math.max(appliedSrcOffset.z, appliedDstOffset.z) + ); + assert(appliedCopyDepth >= 0); + + const encoder = this.device.createCommandEncoder(); + encoder.copyTextureToTexture( + { texture: srcTexture, mipLevel: srcCopyLevel, origin: appliedSrcOffset }, + { texture: dstTexture, mipLevel: dstCopyLevel, origin: appliedDstOffset }, + { width: appliedCopyWidth, height: appliedCopyHeight, depthOrArrayLayers: appliedCopyDepth } + ); + + // Copy the whole content of dstTexture at dstCopyLevel to dstBuffer. + const dstBlocksPerRow = dstTextureSizeAtLevel.width / blockWidth; + const dstBlockRowsPerImage = dstTextureSizeAtLevel.height / blockHeight; + const bytesPerDstAlignedBlockRow = align(dstBlocksPerRow * bytesPerBlock, 256); + const dstBufferSize = + (dstBlockRowsPerImage * dstTextureSizeAtLevel.depthOrArrayLayers - 1) * + bytesPerDstAlignedBlockRow + + align(dstBlocksPerRow * bytesPerBlock, 4); + const dstBufferDesc: GPUBufferDescriptor = { + size: dstBufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }; + const dstBuffer = this.device.createBuffer(dstBufferDesc); + this.trackForCleanup(dstBuffer); + + encoder.copyTextureToBuffer( + { texture: dstTexture, mipLevel: dstCopyLevel }, + { + buffer: dstBuffer, + bytesPerRow: bytesPerDstAlignedBlockRow, + rowsPerImage: dstBlockRowsPerImage, + }, + dstTextureSizeAtLevel + ); + this.device.queue.submit([encoder.finish()]); + + // Fill expectedUint8DataWithPadding with the expected data of dstTexture. The other values in + // expectedUint8DataWithPadding are kept 0 to check if the texels untouched by the copy are 0 + // (their previous values). + const expectedUint8DataWithPadding = new Uint8Array(dstBufferSize); + const expectedUint8Data = new Uint8Array(initialSrcData); + + const appliedCopyBlocksPerRow = appliedCopyWidth / blockWidth; + const appliedCopyBlockRowsPerImage = appliedCopyHeight / blockHeight; + const srcCopyOffsetInBlocks = { + x: appliedSrcOffset.x / blockWidth, + y: appliedSrcOffset.y / blockHeight, + z: appliedSrcOffset.z, + }; + const dstCopyOffsetInBlocks = { + x: appliedDstOffset.x / blockWidth, + y: appliedDstOffset.y / blockHeight, + z: appliedDstOffset.z, + }; + + for (let z = 0; z < appliedCopyDepth; ++z) { + const srcOffsetZ = srcCopyOffsetInBlocks.z + z; + const dstOffsetZ = dstCopyOffsetInBlocks.z + z; + for (let y = 0; y < appliedCopyBlockRowsPerImage; ++y) { + const dstOffsetYInBlocks = dstCopyOffsetInBlocks.y + y; + const expectedDataWithPaddingOffset = + bytesPerDstAlignedBlockRow * (dstBlockRowsPerImage * dstOffsetZ + dstOffsetYInBlocks) + + dstCopyOffsetInBlocks.x * bytesPerBlock; + + const srcOffsetYInBlocks = srcCopyOffsetInBlocks.y + y; + const expectedDataOffset = + bytesPerBlock * + srcBlocksPerRow * + (srcBlockRowsPerImage * srcOffsetZ + srcOffsetYInBlocks) + + srcCopyOffsetInBlocks.x * bytesPerBlock; + + const bytesInRow = appliedCopyBlocksPerRow * bytesPerBlock; + memcpy( + { src: expectedUint8Data, start: expectedDataOffset, length: bytesInRow }, + { dst: expectedUint8DataWithPadding, start: expectedDataWithPaddingOffset } + ); + } + } + + let alternateExpectedData = expectedUint8DataWithPadding; + // For 8-byte snorm formats, allow an alternative encoding of -1. + // MAINTENANCE_TODO: Use textureContentIsOKByT2B with TexelView. + if (srcFormat.includes('snorm')) { + switch (srcFormat) { + case 'r8snorm': + case 'rg8snorm': + case 'rgba8snorm': + alternateExpectedData = alternateExpectedData.slice(); + for (let i = 0; i < alternateExpectedData.length; ++i) { + if (alternateExpectedData[i] === 128) { + alternateExpectedData[i] = 129; + } else if (alternateExpectedData[i] === 129) { + alternateExpectedData[i] = 128; + } + } + break; + case 'bc4-r-snorm': + case 'bc5-rg-snorm': + case 'eac-r11snorm': + case 'eac-rg11snorm': + break; + default: + unreachable(); + } + } + + // Verify the content of the whole subresource of dstTexture at dstCopyLevel (in dstBuffer) is expected. + this.expectGPUBufferValuesPassCheck( + dstBuffer, + alternateExpectedData === expectedUint8DataWithPadding + ? vals => checkElementsEqual(vals, expectedUint8DataWithPadding) + : vals => + checkElementsEqualEither(vals, [expectedUint8DataWithPadding, alternateExpectedData]), + { + srcByteOffset: 0, + type: Uint8Array, + typedLength: expectedUint8DataWithPadding.length, + } + ); + } + + InitializeStencilAspect( + sourceTexture: GPUTexture, + initialStencilData: Uint8Array, + srcCopyLevel: number, + srcCopyBaseArrayLayer: number, + copySize: readonly [number, number, number] + ): void { + this.queue.writeTexture( + { + texture: sourceTexture, + mipLevel: srcCopyLevel, + aspect: 'stencil-only', + origin: { x: 0, y: 0, z: srcCopyBaseArrayLayer }, + }, + initialStencilData, + { bytesPerRow: copySize[0], rowsPerImage: copySize[1] }, + copySize + ); + } + + VerifyStencilAspect( + destinationTexture: GPUTexture, + initialStencilData: Uint8Array, + dstCopyLevel: number, + dstCopyBaseArrayLayer: number, + copySize: readonly [number, number, number] + ): void { + const bytesPerRow = align(copySize[0], kBytesPerRowAlignment); + const rowsPerImage = copySize[1]; + const outputBufferSize = align( + dataBytesForCopyOrFail({ + layout: { bytesPerRow, rowsPerImage }, + format: 'stencil8', + copySize, + method: 'CopyT2B', + }), + kBufferSizeAlignment + ); + const outputBuffer = this.device.createBuffer({ + size: outputBufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + this.trackForCleanup(outputBuffer); + const encoder = this.device.createCommandEncoder(); + encoder.copyTextureToBuffer( + { + texture: destinationTexture, + aspect: 'stencil-only', + mipLevel: dstCopyLevel, + origin: { x: 0, y: 0, z: dstCopyBaseArrayLayer }, + }, + { buffer: outputBuffer, bytesPerRow, rowsPerImage }, + copySize + ); + this.queue.submit([encoder.finish()]); + + const expectedStencilData = new Uint8Array(outputBufferSize); + for (let z = 0; z < copySize[2]; ++z) { + const initialOffsetPerLayer = z * copySize[0] * copySize[1]; + const expectedOffsetPerLayer = z * bytesPerRow * rowsPerImage; + for (let y = 0; y < copySize[1]; ++y) { + const initialOffsetPerRow = initialOffsetPerLayer + y * copySize[0]; + const expectedOffsetPerRow = expectedOffsetPerLayer + y * bytesPerRow; + memcpy( + { src: initialStencilData, start: initialOffsetPerRow, length: copySize[0] }, + { dst: expectedStencilData, start: expectedOffsetPerRow } + ); + } + } + this.expectGPUBufferValuesEqual(outputBuffer, expectedStencilData); + } + + GetRenderPipelineForT2TCopyWithDepthTests( + bindGroupLayout: GPUBindGroupLayout, + hasColorAttachment: boolean, + depthStencil: GPUDepthStencilState + ): GPURenderPipeline { + const renderPipelineDescriptor: GPURenderPipelineDescriptor = { + layout: this.device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), + vertex: { + module: this.device.createShaderModule({ + code: ` + struct Params { + copyLayer: f32 + }; + @group(0) @binding(0) var<uniform> param: Params; + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> { + var depthValue = 0.5 + 0.2 * sin(param.copyLayer); + var pos : array<vec3<f32>, 6> = array<vec3<f32>, 6>( + vec3<f32>(-1.0, 1.0, depthValue), + vec3<f32>(-1.0, -1.0, 0.0), + vec3<f32>( 1.0, 1.0, 1.0), + vec3<f32>(-1.0, -1.0, 0.0), + vec3<f32>( 1.0, 1.0, 1.0), + vec3<f32>( 1.0, -1.0, depthValue)); + return vec4<f32>(pos[VertexIndex], 1.0); + }`, + }), + entryPoint: 'main', + }, + depthStencil, + }; + if (hasColorAttachment) { + renderPipelineDescriptor.fragment = { + module: this.device.createShaderModule({ + code: ` + @fragment + fn main() -> @location(0) vec4<f32> { + return vec4<f32>(0.0, 1.0, 0.0, 1.0); + }`, + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm' }], + }; + } + return this.device.createRenderPipeline(renderPipelineDescriptor); + } + + GetBindGroupLayoutForT2TCopyWithDepthTests(): GPUBindGroupLayout { + return this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { + type: 'uniform', + minBindingSize: 4, + hasDynamicOffset: true, + }, + }, + ], + }); + } + + GetBindGroupForT2TCopyWithDepthTests( + bindGroupLayout: GPUBindGroupLayout, + totalCopyArrayLayers: number + ): GPUBindGroup { + // Prepare the uniform buffer that contains all the copy layers to generate different depth + // values for different copy layers. + assert(totalCopyArrayLayers > 0); + const uniformBufferSize = kMinDynamicBufferOffsetAlignment * (totalCopyArrayLayers - 1) + 4; + const uniformBufferData = new Float32Array(uniformBufferSize / 4); + for (let i = 1; i < totalCopyArrayLayers; ++i) { + uniformBufferData[(kMinDynamicBufferOffsetAlignment / 4) * i] = i; + } + const uniformBuffer = makeBufferWithContents( + this.device, + uniformBufferData, + GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM + ); + return this.device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer, + size: 4, + }, + }, + ], + }); + } + + /** Initialize the depth aspect of sourceTexture with draw calls */ + InitializeDepthAspect( + sourceTexture: GPUTexture, + depthFormat: GPUTextureFormat, + srcCopyLevel: number, + srcCopyBaseArrayLayer: number, + copySize: readonly [number, number, number] + ): void { + // Prepare a renderPipeline with depthCompareFunction == 'always' and depthWriteEnabled == true + // for the initializations of the depth attachment. + const bindGroupLayout = this.GetBindGroupLayoutForT2TCopyWithDepthTests(); + const renderPipeline = this.GetRenderPipelineForT2TCopyWithDepthTests(bindGroupLayout, false, { + format: depthFormat, + depthWriteEnabled: true, + depthCompare: 'always', + }); + const bindGroup = this.GetBindGroupForT2TCopyWithDepthTests(bindGroupLayout, copySize[2]); + + const encoder = this.device.createCommandEncoder(); + for (let srcCopyLayer = 0; srcCopyLayer < copySize[2]; ++srcCopyLayer) { + const renderPass = encoder.beginRenderPass({ + colorAttachments: [], + depthStencilAttachment: { + view: sourceTexture.createView({ + baseArrayLayer: srcCopyLayer + srcCopyBaseArrayLayer, + arrayLayerCount: 1, + baseMipLevel: srcCopyLevel, + mipLevelCount: 1, + }), + depthClearValue: 0.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, + }); + renderPass.setBindGroup(0, bindGroup, [srcCopyLayer * kMinDynamicBufferOffsetAlignment]); + renderPass.setPipeline(renderPipeline); + renderPass.draw(6); + renderPass.end(); + } + this.queue.submit([encoder.finish()]); + } + + VerifyDepthAspect( + destinationTexture: GPUTexture, + depthFormat: GPUTextureFormat, + dstCopyLevel: number, + dstCopyBaseArrayLayer: number, + copySize: [number, number, number] + ): void { + // Prepare a renderPipeline with depthCompareFunction == 'equal' and depthWriteEnabled == false + // for the comparison of the depth attachment. + const bindGroupLayout = this.GetBindGroupLayoutForT2TCopyWithDepthTests(); + const renderPipeline = this.GetRenderPipelineForT2TCopyWithDepthTests(bindGroupLayout, true, { + format: depthFormat, + depthWriteEnabled: false, + depthCompare: 'equal', + }); + const bindGroup = this.GetBindGroupForT2TCopyWithDepthTests(bindGroupLayout, copySize[2]); + + const outputColorTexture = this.device.createTexture({ + format: 'rgba8unorm', + size: copySize, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + this.trackForCleanup(outputColorTexture); + const encoder = this.device.createCommandEncoder(); + for (let dstCopyLayer = 0; dstCopyLayer < copySize[2]; ++dstCopyLayer) { + // If the depth value is not expected, the color of outputColorTexture will remain Red after + // the render pass. + const renderPass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: outputColorTexture.createView({ + baseArrayLayer: dstCopyLayer, + arrayLayerCount: 1, + }), + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: destinationTexture.createView({ + baseArrayLayer: dstCopyLayer + dstCopyBaseArrayLayer, + arrayLayerCount: 1, + baseMipLevel: dstCopyLevel, + mipLevelCount: 1, + }), + depthLoadOp: 'load', + depthStoreOp: 'store', + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, + }); + renderPass.setBindGroup(0, bindGroup, [dstCopyLayer * kMinDynamicBufferOffsetAlignment]); + renderPass.setPipeline(renderPipeline); + renderPass.draw(6); + renderPass.end(); + } + this.queue.submit([encoder.finish()]); + + this.expectSingleColor(outputColorTexture, 'rgba8unorm', { + size: copySize, + exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + }); + } +} + +const kCopyBoxOffsetsForWholeDepth = [ + // From (0, 0) of src to (0, 0) of dst. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // From (0, 0) of src to (blockWidth, 0) of dst. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 1, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // From (0, 0) of src to (0, blockHeight) of dst. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 1, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // From (blockWidth, 0) of src to (0, 0) of dst. + { + srcOffset: { x: 1, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // From (0, blockHeight) of src to (0, 0) of dst. + { + srcOffset: { x: 0, y: 1, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // From (blockWidth, 0) of src to (0, 0) of dst, and the copy extent will not cover the last + // texel block column of both source and destination texture. + { + srcOffset: { x: 1, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: -1, height: 0, depthOrArrayLayers: 0 }, + }, + // From (0, blockHeight) of src to (0, 0) of dst, and the copy extent will not cover the last + // texel block row of both source and destination texture. + { + srcOffset: { x: 0, y: 1, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: -1, depthOrArrayLayers: 0 }, + }, +] as const; + +const kCopyBoxOffsetsFor2DArrayTextures = [ + // Copy the whole array slices from the source texture to the destination texture. + // The copy extent will cover the whole subresource of either source or the + // destination texture + ...kCopyBoxOffsetsForWholeDepth, + + // Copy 1 texture slice from the 1st slice of the source texture to the 1st slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -2 }, + }, + // Copy 1 texture slice from the 2nd slice of the source texture to the 2nd slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 1 }, + dstOffset: { x: 0, y: 0, z: 1 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -3 }, + }, + // Copy 1 texture slice from the 1st slice of the source texture to the 2nd slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 1 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 }, + }, + // Copy 1 texture slice from the 2nd slice of the source texture to the 1st slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 1 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 }, + }, + // Copy 2 texture slices from the 1st slice of the source texture to the 1st slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -3 }, + }, + // Copy 3 texture slices from the 2nd slice of the source texture to the 2nd slice of the + // destination texture. + { + srcOffset: { x: 0, y: 0, z: 1 }, + dstOffset: { x: 0, y: 0, z: 1 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -1 }, + }, +] as const; + +export const g = makeTestGroup(F); + +g.test('color_textures,non_compressed,non_array') + .desc( + ` + Validate the correctness of the copy by filling the srcTexture with testable data and any + non-compressed color format supported by WebGPU, doing CopyTextureToTexture() copy, and verifying + the content of the whole dstTexture. + + Copy {1 texel block, part of, the whole} srcTexture to the dstTexture {with, without} a non-zero + valid srcOffset that + - covers the whole dstTexture subresource + - covers the corners of the dstTexture + - doesn't cover any texels that are on the edge of the dstTexture + - covers the mipmap level > 0 + + Tests for all pairs of valid source/destination formats, and all texture dimensions. + ` + ) + .params(u => + u + .combine('srcFormat', kRegularTextureFormats) + .combine('dstFormat', kRegularTextureFormats) + .filter(({ srcFormat, dstFormat }) => { + const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat; + const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat; + return ( + srcFormat === dstFormat || + (srcBaseFormat !== undefined && + dstBaseFormat !== undefined && + srcBaseFormat === dstBaseFormat) + ); + }) + .combine('dimension', kTextureDimensions) + .filter( + ({ dimension, srcFormat, dstFormat }) => + textureDimensionAndFormatCompatible(dimension, srcFormat) && + textureDimensionAndFormatCompatible(dimension, dstFormat) + ) + .beginSubcases() + .expandWithParams(p => { + const params = [ + { + srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 }, + dstTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 }, + }, + { + srcTextureSize: { width: 31, height: 33, depthOrArrayLayers: 1 }, + dstTextureSize: { width: 31, height: 33, depthOrArrayLayers: 1 }, + }, + { + srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 }, + dstTextureSize: { width: 64, height: 64, depthOrArrayLayers: 1 }, + }, + { + srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 1 }, + dstTextureSize: { width: 63, height: 61, depthOrArrayLayers: 1 }, + }, + ]; + if (p.dimension === '1d') { + for (const param of params) { + param.srcTextureSize.height = 1; + param.dstTextureSize.height = 1; + } + } + + return params; + }) + .combine('copyBoxOffsets', kCopyBoxOffsetsForWholeDepth) + .unless( + p => + p.dimension === '1d' && + (p.copyBoxOffsets.copyExtent.height !== 0 || + p.copyBoxOffsets.srcOffset.y !== 0 || + p.copyBoxOffsets.dstOffset.y !== 0) + ) + .combine('srcCopyLevel', [0, 3]) + .combine('dstCopyLevel', [0, 3]) + .unless(p => p.dimension === '1d' && (p.srcCopyLevel !== 0 || p.dstCopyLevel !== 0)) + ) + .fn(async t => { + const { + dimension, + srcTextureSize, + dstTextureSize, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel, + } = t.params; + + t.DoCopyTextureToTextureTest( + dimension, + srcTextureSize, + dstTextureSize, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel + ); + }); + +g.test('color_textures,compressed,non_array') + .desc( + ` + Validate the correctness of the copy by filling the srcTexture with testable data and any + compressed color format supported by WebGPU, doing CopyTextureToTexture() copy, and verifying + the content of the whole dstTexture. + + Tests for all pairs of valid source/destination formats, and all texture dimensions. + ` + ) + .params(u => + u + .combine('srcFormat', kCompressedTextureFormats) + .combine('dstFormat', kCompressedTextureFormats) + .filter(({ srcFormat, dstFormat }) => { + const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat; + const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat; + return ( + srcFormat === dstFormat || + (srcBaseFormat !== undefined && + dstBaseFormat !== undefined && + srcBaseFormat === dstBaseFormat) + ); + }) + .combine('dimension', kTextureDimensions) + .filter( + ({ dimension, srcFormat, dstFormat }) => + textureDimensionAndFormatCompatible(dimension, srcFormat) && + textureDimensionAndFormatCompatible(dimension, dstFormat) + ) + .beginSubcases() + .combine('textureSizeInBlocks', [ + // The heights and widths in blocks are all power of 2 + { src: { width: 16, height: 8 }, dst: { width: 16, height: 8 } }, + // The virtual width of the source texture at mipmap level 2 (15) is not a multiple of 4 blocks + { src: { width: 15, height: 8 }, dst: { width: 16, height: 8 } }, + // The virtual width of the destination texture at mipmap level 2 (15) is not a multiple + // of 4 blocks + { src: { width: 16, height: 8 }, dst: { width: 15, height: 8 } }, + // The virtual height of the source texture at mipmap level 2 (13) is not a multiple of 4 blocks + { src: { width: 16, height: 13 }, dst: { width: 16, height: 8 } }, + // The virtual height of the destination texture at mipmap level 2 (13) is not a + // multiple of 4 blocks + { src: { width: 16, height: 8 }, dst: { width: 16, height: 13 } }, + // None of the widths or heights in blocks are power of 2 + { src: { width: 15, height: 13 }, dst: { width: 15, height: 13 } }, + ]) + .combine('copyBoxOffsets', kCopyBoxOffsetsForWholeDepth) + .combine('srcCopyLevel', [0, 2]) + .combine('dstCopyLevel', [0, 2]) + ) + .beforeAllSubcases(t => { + const { srcFormat, dstFormat } = t.params; + t.selectDeviceOrSkipTestCase([ + kTextureFormatInfo[srcFormat].feature, + kTextureFormatInfo[dstFormat].feature, + ]); + }) + .fn(async t => { + const { + dimension, + textureSizeInBlocks, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel, + } = t.params; + const srcBlockWidth = kTextureFormatInfo[srcFormat].blockWidth; + const srcBlockHeight = kTextureFormatInfo[srcFormat].blockHeight; + const dstBlockWidth = kTextureFormatInfo[dstFormat].blockWidth; + const dstBlockHeight = kTextureFormatInfo[dstFormat].blockHeight; + + t.DoCopyTextureToTextureTest( + dimension, + { + width: textureSizeInBlocks.src.width * srcBlockWidth, + height: textureSizeInBlocks.src.height * srcBlockHeight, + depthOrArrayLayers: 1, + }, + { + width: textureSizeInBlocks.dst.width * dstBlockWidth, + height: textureSizeInBlocks.dst.height * dstBlockHeight, + depthOrArrayLayers: 1, + }, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel + ); + }); + +g.test('color_textures,non_compressed,array') + .desc( + ` + Validate the correctness of the texture-to-texture copy on 2D array textures by filling the + srcTexture with testable data and any non-compressed color format supported by WebGPU, doing + CopyTextureToTexture() copy, and verifying the content of the whole dstTexture. + ` + ) + .params(u => + u + .combine('srcFormat', kRegularTextureFormats) + .combine('dstFormat', kRegularTextureFormats) + .filter(({ srcFormat, dstFormat }) => { + const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat; + const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat; + return ( + srcFormat === dstFormat || + (srcBaseFormat !== undefined && + dstBaseFormat !== undefined && + srcBaseFormat === dstBaseFormat) + ); + }) + .combine('dimension', ['2d', '3d'] as const) + .filter( + ({ dimension, srcFormat, dstFormat }) => + textureDimensionAndFormatCompatible(dimension, srcFormat) && + textureDimensionAndFormatCompatible(dimension, dstFormat) + ) + .beginSubcases() + .combine('textureSize', [ + { + srcTextureSize: { width: 64, height: 32, depthOrArrayLayers: 5 }, + dstTextureSize: { width: 64, height: 32, depthOrArrayLayers: 5 }, + }, + { + srcTextureSize: { width: 31, height: 33, depthOrArrayLayers: 5 }, + dstTextureSize: { width: 31, height: 33, depthOrArrayLayers: 5 }, + }, + { + srcTextureSize: { width: 31, height: 32, depthOrArrayLayers: 33 }, + dstTextureSize: { width: 31, height: 32, depthOrArrayLayers: 33 }, + }, + ]) + + .combine('copyBoxOffsets', kCopyBoxOffsetsFor2DArrayTextures) + .combine('srcCopyLevel', [0, 3]) + .combine('dstCopyLevel', [0, 3]) + ) + .fn(async t => { + const { + dimension, + textureSize, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel, + } = t.params; + + t.DoCopyTextureToTextureTest( + dimension, + textureSize.srcTextureSize, + textureSize.dstTextureSize, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel + ); + }); + +g.test('color_textures,compressed,array') + .desc( + ` + Validate the correctness of the texture-to-texture copy on 2D array textures by filling the + srcTexture with testable data and any compressed color format supported by WebGPU, doing + CopyTextureToTexture() copy, and verifying the content of the whole dstTexture. + + Tests for all pairs of valid source/destination formats, and all texture dimensions. + ` + ) + .params(u => + u + .combine('srcFormat', kCompressedTextureFormats) + .combine('dstFormat', kCompressedTextureFormats) + .filter(({ srcFormat, dstFormat }) => { + const srcBaseFormat = kTextureFormatInfo[srcFormat].baseFormat; + const dstBaseFormat = kTextureFormatInfo[dstFormat].baseFormat; + return ( + srcFormat === dstFormat || + (srcBaseFormat !== undefined && + dstBaseFormat !== undefined && + srcBaseFormat === dstBaseFormat) + ); + }) + .combine('dimension', ['2d', '3d'] as const) + .filter( + ({ dimension, srcFormat, dstFormat }) => + textureDimensionAndFormatCompatible(dimension, srcFormat) && + textureDimensionAndFormatCompatible(dimension, dstFormat) + ) + .beginSubcases() + .combine('textureSizeInBlocks', [ + // The heights and widths in blocks are all power of 2 + { src: { width: 2, height: 2 }, dst: { width: 2, height: 2 } }, + // None of the widths or heights in blocks are power of 2 + { src: { width: 15, height: 13 }, dst: { width: 15, height: 13 } }, + ]) + .combine('copyBoxOffsets', kCopyBoxOffsetsFor2DArrayTextures) + .combine('srcCopyLevel', [0, 2]) + .combine('dstCopyLevel', [0, 2]) + ) + .beforeAllSubcases(t => { + const { srcFormat, dstFormat } = t.params; + + t.selectDeviceOrSkipTestCase([ + kTextureFormatInfo[srcFormat].feature, + kTextureFormatInfo[dstFormat].feature, + ]); + }) + .fn(async t => { + const { + dimension, + textureSizeInBlocks, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel, + } = t.params; + const srcBlockWidth = kTextureFormatInfo[srcFormat].blockWidth; + const srcBlockHeight = kTextureFormatInfo[srcFormat].blockHeight; + const dstBlockWidth = kTextureFormatInfo[dstFormat].blockWidth; + const dstBlockHeight = kTextureFormatInfo[dstFormat].blockHeight; + + t.DoCopyTextureToTextureTest( + dimension, + { + width: textureSizeInBlocks.src.width * srcBlockWidth, + height: textureSizeInBlocks.src.height * srcBlockHeight, + depthOrArrayLayers: 5, + }, + { + width: textureSizeInBlocks.dst.width * dstBlockWidth, + height: textureSizeInBlocks.dst.height * dstBlockHeight, + depthOrArrayLayers: 5, + }, + srcFormat, + dstFormat, + copyBoxOffsets, + srcCopyLevel, + dstCopyLevel + ); + }); + +g.test('zero_sized') + .desc( + ` + Validate the correctness of zero-sized copies (should be no-ops). + + - For each texture dimension. + - Copies that are zero-sized in only one dimension {x, y, z}, each touching the {lower, upper} end + of that dimension. + ` + ) + .paramsSubcasesOnly(u => + u // + .combineWithParams([ + { dimension: '1d', textureSize: { width: 32, height: 1, depthOrArrayLayers: 1 } }, + { dimension: '2d', textureSize: { width: 32, height: 32, depthOrArrayLayers: 5 } }, + { dimension: '3d', textureSize: { width: 32, height: 32, depthOrArrayLayers: 5 } }, + ] as const) + .combine('copyBoxOffset', [ + // copyExtent.width === 0 + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 }, + }, + // copyExtent.width === 0 && srcOffset.x === textureWidth + { + srcOffset: { x: 64, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 }, + }, + // copyExtent.width === 0 && dstOffset.x === textureWidth + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 64, y: 0, z: 0 }, + copyExtent: { width: -64, height: 0, depthOrArrayLayers: 0 }, + }, + // copyExtent.height === 0 + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 }, + }, + // copyExtent.height === 0 && srcOffset.y === textureHeight + { + srcOffset: { x: 0, y: 32, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 }, + }, + // copyExtent.height === 0 && dstOffset.y === textureHeight + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 32, z: 0 }, + copyExtent: { width: 0, height: -32, depthOrArrayLayers: 0 }, + }, + // copyExtent.depthOrArrayLayers === 0 + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: -5 }, + }, + // copyExtent.depthOrArrayLayers === 0 && srcOffset.z === textureDepth + { + srcOffset: { x: 0, y: 0, z: 5 }, + dstOffset: { x: 0, y: 0, z: 0 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + // copyExtent.depthOrArrayLayers === 0 && dstOffset.z === textureDepth + { + srcOffset: { x: 0, y: 0, z: 0 }, + dstOffset: { x: 0, y: 0, z: 5 }, + copyExtent: { width: 0, height: 0, depthOrArrayLayers: 0 }, + }, + ]) + .unless( + p => + p.dimension === '1d' && + (p.copyBoxOffset.copyExtent.height !== 0 || + p.copyBoxOffset.srcOffset.y !== 0 || + p.copyBoxOffset.dstOffset.y !== 0) + ) + .combine('srcCopyLevel', [0, 3]) + .combine('dstCopyLevel', [0, 3]) + .unless(p => p.dimension === '1d' && (p.srcCopyLevel !== 0 || p.dstCopyLevel !== 0)) + ) + .fn(async t => { + const { dimension, textureSize, copyBoxOffset, srcCopyLevel, dstCopyLevel } = t.params; + + const srcFormat = 'rgba8unorm'; + const dstFormat = 'rgba8unorm'; + + t.DoCopyTextureToTextureTest( + dimension, + textureSize, + textureSize, + srcFormat, + dstFormat, + copyBoxOffset, + srcCopyLevel, + dstCopyLevel + ); + }); + +g.test('copy_depth_stencil') + .desc( + ` + Validate the correctness of copyTextureToTexture() with depth and stencil aspect. + + For all the texture formats with stencil aspect: + - Initialize the stencil aspect of the source texture with writeTexture(). + - Copy the stencil aspect from the source texture into the destination texture + - Copy the stencil aspect of the destination texture into another staging buffer and check its + content + - Test the copies from / into zero / non-zero array layer / mipmap levels + - Test copying multiple array layers + + For all the texture formats with depth aspect: + - Initialize the depth aspect of the source texture with a draw call + - Copy the depth aspect from the source texture into the destination texture + - Validate the content in the destination texture with the depth comparison function 'equal' + ` + ) + .params(u => + u + .combine('format', kDepthStencilFormats) + .beginSubcases() + .combine('srcTextureSize', [ + { width: 32, height: 16, depthOrArrayLayers: 1 }, + { width: 32, height: 16, depthOrArrayLayers: 4 }, + { width: 24, height: 48, depthOrArrayLayers: 5 }, + ]) + .combine('srcCopyLevel', [0, 2]) + .combine('dstCopyLevel', [0, 2]) + .combine('srcCopyBaseArrayLayer', [0, 1]) + .combine('dstCopyBaseArrayLayer', [0, 1]) + .filter(t => { + return ( + t.srcTextureSize.depthOrArrayLayers > t.srcCopyBaseArrayLayer && + t.srcTextureSize.depthOrArrayLayers > t.dstCopyBaseArrayLayer + ); + }) + ) + .beforeAllSubcases(t => { + const { format } = t.params; + t.selectDeviceForTextureFormatOrSkipTestCase(format); + }) + .fn(async t => { + const { + format, + srcTextureSize, + srcCopyLevel, + dstCopyLevel, + srcCopyBaseArrayLayer, + dstCopyBaseArrayLayer, + } = t.params; + + const copySize: [number, number, number] = [ + srcTextureSize.width >> srcCopyLevel, + srcTextureSize.height >> srcCopyLevel, + srcTextureSize.depthOrArrayLayers - Math.max(srcCopyBaseArrayLayer, dstCopyBaseArrayLayer), + ]; + const sourceTexture = t.device.createTexture({ + format, + size: srcTextureSize, + usage: + GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + mipLevelCount: srcCopyLevel + 1, + }); + t.trackForCleanup(sourceTexture); + const destinationTexture = t.device.createTexture({ + format, + size: [ + copySize[0] << dstCopyLevel, + copySize[1] << dstCopyLevel, + srcTextureSize.depthOrArrayLayers, + ] as const, + usage: + GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + mipLevelCount: dstCopyLevel + 1, + }); + t.trackForCleanup(destinationTexture); + + let initialStencilData: undefined | Uint8Array = undefined; + if (kTextureFormatInfo[format].stencil) { + initialStencilData = t.GetInitialStencilDataPerMipLevel(srcTextureSize, format, srcCopyLevel); + t.InitializeStencilAspect( + sourceTexture, + initialStencilData, + srcCopyLevel, + srcCopyBaseArrayLayer, + copySize + ); + } + if (kTextureFormatInfo[format].depth) { + t.InitializeDepthAspect(sourceTexture, format, srcCopyLevel, srcCopyBaseArrayLayer, copySize); + } + + const encoder = t.device.createCommandEncoder(); + encoder.copyTextureToTexture( + { + texture: sourceTexture, + mipLevel: srcCopyLevel, + origin: { x: 0, y: 0, z: srcCopyBaseArrayLayer }, + }, + { + texture: destinationTexture, + mipLevel: dstCopyLevel, + origin: { x: 0, y: 0, z: dstCopyBaseArrayLayer }, + }, + copySize + ); + t.queue.submit([encoder.finish()]); + + if (kTextureFormatInfo[format].stencil) { + assert(initialStencilData !== undefined); + t.VerifyStencilAspect( + destinationTexture, + initialStencilData, + dstCopyLevel, + dstCopyBaseArrayLayer, + copySize + ); + } + if (kTextureFormatInfo[format].depth) { + t.VerifyDepthAspect( + destinationTexture, + format, + dstCopyLevel, + dstCopyBaseArrayLayer, + copySize + ); + } + }); + +g.test('copy_multisampled_color') + .desc( + ` + Validate the correctness of copyTextureToTexture() with multisampled color formats. + + - Initialize the source texture with a triangle in a render pass. + - Copy from the source texture into the destination texture with CopyTextureToTexture(). + - Compare every sub-pixel of source texture and destination texture in another render pass: + - If they are different, then output RED; otherwise output GREEN + - Verify the pixels in the output texture are all GREEN. + - Note that in current WebGPU SPEC the mipmap level count and array layer count of a multisampled + texture can only be 1. + ` + ) + .fn(async t => { + const textureSize = [32, 16, 1] as const; + const kColorFormat = 'rgba8unorm'; + const kSampleCount = 4; + + const sourceTexture = t.device.createTexture({ + format: kColorFormat, + size: textureSize, + usage: + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: kSampleCount, + }); + t.trackForCleanup(sourceTexture); + const destinationTexture = t.device.createTexture({ + format: kColorFormat, + size: textureSize, + usage: + GPUTextureUsage.COPY_DST | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: kSampleCount, + }); + t.trackForCleanup(destinationTexture); + + // Initialize sourceTexture with a draw call. + const renderPipelineForInit = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: ` + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> { + var pos = array<vec2<f32>, 3>( + vec2<f32>(-1.0, 1.0), + vec2<f32>( 1.0, 1.0), + vec2<f32>( 1.0, -1.0) + ); + return vec4<f32>(pos[VertexIndex], 0.0, 1.0); + }`, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: ` + @fragment + fn main() -> @location(0) vec4<f32> { + return vec4<f32>(0.3, 0.5, 0.8, 1.0); + }`, + }), + entryPoint: 'main', + targets: [{ format: kColorFormat }], + }, + multisample: { + count: kSampleCount, + }, + }); + const initEncoder = t.device.createCommandEncoder(); + const renderPassForInit = initEncoder.beginRenderPass({ + colorAttachments: [ + { + view: sourceTexture.createView(), + clearValue: [1.0, 0.0, 0.0, 1.0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + renderPassForInit.setPipeline(renderPipelineForInit); + renderPassForInit.draw(3); + renderPassForInit.end(); + t.queue.submit([initEncoder.finish()]); + + // Do the texture-to-texture copy + const copyEncoder = t.device.createCommandEncoder(); + copyEncoder.copyTextureToTexture( + { + texture: sourceTexture, + }, + { + texture: destinationTexture, + }, + textureSize + ); + t.queue.submit([copyEncoder.finish()]); + + // Verify if all the sub-pixel values at the same location of sourceTexture and + // destinationTexture are equal. + const renderPipelineForValidation = t.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: t.device.createShaderModule({ + code: ` + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> { + var pos = array<vec2<f32>, 6>( + vec2<f32>(-1.0, 1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>( 1.0, 1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>( 1.0, 1.0), + vec2<f32>( 1.0, -1.0)); + return vec4<f32>(pos[VertexIndex], 0.0, 1.0); + }`, + }), + entryPoint: 'main', + }, + fragment: { + module: t.device.createShaderModule({ + code: ` + @group(0) @binding(0) var sourceTexture : texture_multisampled_2d<f32>; + @group(0) @binding(1) var destinationTexture : texture_multisampled_2d<f32>; + @fragment + fn main(@builtin(position) coord_in: vec4<f32>) -> @location(0) vec4<f32> { + var coord_in_vec2 = vec2<i32>(i32(coord_in.x), i32(coord_in.y)); + for (var sampleIndex = 0; sampleIndex < ${kSampleCount}; + sampleIndex = sampleIndex + 1) { + var sourceSubPixel : vec4<f32> = + textureLoad(sourceTexture, coord_in_vec2, sampleIndex); + var destinationSubPixel : vec4<f32> = + textureLoad(destinationTexture, coord_in_vec2, sampleIndex); + if (!all(sourceSubPixel == destinationSubPixel)) { + return vec4<f32>(1.0, 0.0, 0.0, 1.0); + } + } + return vec4<f32>(0.0, 1.0, 0.0, 1.0); + }`, + }), + entryPoint: 'main', + targets: [{ format: kColorFormat }], + }, + }); + const bindGroup = t.device.createBindGroup({ + layout: renderPipelineForValidation.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: sourceTexture.createView(), + }, + { + binding: 1, + resource: destinationTexture.createView(), + }, + ], + }); + const expectedOutputTexture = t.device.createTexture({ + format: kColorFormat, + size: textureSize, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + t.trackForCleanup(expectedOutputTexture); + const validationEncoder = t.device.createCommandEncoder(); + const renderPassForValidation = validationEncoder.beginRenderPass({ + colorAttachments: [ + { + view: expectedOutputTexture.createView(), + clearValue: [1.0, 0.0, 0.0, 1.0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + renderPassForValidation.setPipeline(renderPipelineForValidation); + renderPassForValidation.setBindGroup(0, bindGroup); + renderPassForValidation.draw(6); + renderPassForValidation.end(); + t.queue.submit([validationEncoder.finish()]); + + t.expectSingleColor(expectedOutputTexture, 'rgba8unorm', { + size: [textureSize[0], textureSize[1], textureSize[2]], + exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + }); + }); + +g.test('copy_multisampled_depth') + .desc( + ` + Validate the correctness of copyTextureToTexture() with multisampled depth formats. + + - Initialize the source texture with a triangle in a render pass. + - Copy from the source texture into the destination texture with CopyTextureToTexture(). + - Validate the content in the destination texture with the depth comparison function 'equal'. + - Note that in current WebGPU SPEC the mipmap level count and array layer count of a multisampled + texture can only be 1. + ` + ) + .fn(async t => { + const textureSize = [32, 16, 1] as const; + const kDepthFormat = 'depth24plus'; + const kSampleCount = 4; + + const sourceTexture = t.device.createTexture({ + format: kDepthFormat, + size: textureSize, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: kSampleCount, + }); + t.trackForCleanup(sourceTexture); + const destinationTexture = t.device.createTexture({ + format: kDepthFormat, + size: textureSize, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: kSampleCount, + }); + t.trackForCleanup(destinationTexture); + + const vertexState: GPUVertexState = { + module: t.device.createShaderModule({ + code: ` + @vertex + fn main(@builtin(vertex_index) VertexIndex : u32)-> @builtin(position) vec4<f32> { + var pos : array<vec3<f32>, 6> = array<vec3<f32>, 6>( + vec3<f32>(-1.0, 1.0, 0.5), + vec3<f32>(-1.0, -1.0, 0.0), + vec3<f32>( 1.0, 1.0, 1.0), + vec3<f32>(-1.0, -1.0, 0.0), + vec3<f32>( 1.0, 1.0, 1.0), + vec3<f32>( 1.0, -1.0, 0.5)); + return vec4<f32>(pos[VertexIndex], 1.0); + }`, + }), + entryPoint: 'main', + }; + + // Initialize the depth aspect of source texture with a draw call + const renderPipelineForInit = t.device.createRenderPipeline({ + layout: 'auto', + vertex: vertexState, + depthStencil: { + format: kDepthFormat, + depthCompare: 'always', + depthWriteEnabled: true, + }, + multisample: { + count: kSampleCount, + }, + }); + + const encoderForInit = t.device.createCommandEncoder(); + const renderPassForInit = encoderForInit.beginRenderPass({ + colorAttachments: [], + depthStencilAttachment: { + view: sourceTexture.createView(), + depthClearValue: 0.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }); + renderPassForInit.setPipeline(renderPipelineForInit); + renderPassForInit.draw(6); + renderPassForInit.end(); + t.queue.submit([encoderForInit.finish()]); + + // Do the texture-to-texture copy + const copyEncoder = t.device.createCommandEncoder(); + copyEncoder.copyTextureToTexture( + { + texture: sourceTexture, + }, + { + texture: destinationTexture, + }, + textureSize + ); + t.queue.submit([copyEncoder.finish()]); + + // Verify the depth values in destinationTexture are what we expected with + // depthCompareFunction == 'equal' and depthWriteEnabled == false in the render pipeline + const kColorFormat = 'rgba8unorm'; + const renderPipelineForVerify = t.device.createRenderPipeline({ + layout: 'auto', + vertex: vertexState, + fragment: { + module: t.device.createShaderModule({ + code: ` + @fragment + fn main() -> @location(0) vec4<f32> { + return vec4<f32>(0.0, 1.0, 0.0, 1.0); + }`, + }), + entryPoint: 'main', + targets: [{ format: kColorFormat }], + }, + depthStencil: { + format: kDepthFormat, + depthCompare: 'equal', + depthWriteEnabled: false, + }, + multisample: { + count: kSampleCount, + }, + }); + const multisampledColorTexture = t.device.createTexture({ + format: kColorFormat, + size: textureSize, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: kSampleCount, + }); + t.trackForCleanup(multisampledColorTexture); + const colorTextureAsResolveTarget = t.device.createTexture({ + format: kColorFormat, + size: textureSize, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + t.trackForCleanup(colorTextureAsResolveTarget); + + const encoderForVerify = t.device.createCommandEncoder(); + const renderPassForVerify = encoderForVerify.beginRenderPass({ + colorAttachments: [ + { + view: multisampledColorTexture.createView(), + clearValue: { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'discard', + resolveTarget: colorTextureAsResolveTarget.createView(), + }, + ], + depthStencilAttachment: { + view: destinationTexture.createView(), + depthLoadOp: 'load', + depthStoreOp: 'store', + }, + }); + renderPassForVerify.setPipeline(renderPipelineForVerify); + renderPassForVerify.draw(6); + renderPassForVerify.end(); + t.queue.submit([encoderForVerify.finish()]); + + t.expectSingleColor(colorTextureAsResolveTarget, kColorFormat, { + size: [textureSize[0], textureSize[1], textureSize[2]], + exp: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + }); + }); |