| export const description = `copyTextureToTexture operation tests`; |
| |
| import { makeTestGroup } from '../../../../common/framework/test_group.js'; |
| import { assert, ErrorWithExtra, memcpy } from '../../../../common/util/util.js'; |
| import { |
| kBufferSizeAlignment, |
| kMinDynamicBufferOffsetAlignment, |
| kTextureDimensions, |
| } from '../../../capability_info.js'; |
| import { |
| ColorTextureFormat, |
| DepthStencilFormat, |
| depthStencilFormatAspectSize, |
| getBaseFormatForTextureFormat, |
| getBlockInfoForColorTextureFormat, |
| isCompressedTextureFormat, |
| isDepthTextureFormat, |
| isRegularTextureFormat, |
| isStencilTextureFormat, |
| kCompressedTextureFormats, |
| kDepthStencilFormats, |
| kRegularTextureFormats, |
| RegularTextureFormat, |
| textureFormatAndDimensionPossiblyCompatible, |
| textureFormatsAreViewCompatible, |
| } from '../../../format_info.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js'; |
| import * as ttu from '../../../texture_test_utils.js'; |
| import { checkElementsEqual } 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'; |
| import { TexelView } from '../../../util/texture/texel_view.js'; |
| import { findFailedPixels } from '../../../util/texture/texture_ok.js'; |
| import { reifyExtent3D } from '../../../util/unions.js'; |
| |
| const dataGenerator = new DataArrayGenerator(); |
| |
| // If a texture could be textureBindingViewDimension: 'cube' then set it to 'cube' |
| function applyTextureBindingViewDimensionForTest(descriptor: GPUTextureDescriptor) { |
| const size = reifyExtent3D(descriptor.size); |
| if ( |
| descriptor.textureBindingViewDimension === undefined && |
| descriptor.dimension === '2d' && |
| size.width === size.height && |
| size.depthOrArrayLayers === 6 |
| ) { |
| descriptor.textureBindingViewDimension = 'cube'; |
| } |
| return descriptor; |
| } |
| |
| class F extends AllFeaturesMaxLimitsGPUTest { |
| getInitialDataPerMipLevel( |
| dimension: GPUTextureDimension, |
| textureSize: Required<GPUExtent3DDict>, |
| format: ColorTextureFormat, |
| mipLevel: number |
| ): Uint8Array { |
| const textureSizeAtLevel = physicalMipSize(textureSize, format, dimension, mipLevel); |
| const { bytesPerBlock, blockWidth, blockHeight } = getBlockInfoForColorTextureFormat(format); |
| const blocksPerSubresource = |
| (textureSizeAtLevel.width / blockWidth) * (textureSizeAtLevel.height / blockHeight); |
| |
| 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: ColorTextureFormat, |
| dstFormat: ColorTextureFormat, |
| copyBoxOffsets: { |
| srcOffset: { x: number; y: number; z: number }; |
| dstOffset: { x: number; y: number; z: number }; |
| copyExtent: Required<GPUExtent3DDict>; |
| }, |
| srcCopyLevel: number, |
| dstCopyLevel: number |
| ): void { |
| this.skipIfTextureFormatNotSupported(srcFormat, dstFormat); |
| this.skipIfCopyTextureToTextureNotSupportedForFormat(srcFormat, dstFormat); |
| this.skipIfTextureFormatAndDimensionNotCompatible(srcFormat, dimension); |
| this.skipIfTextureFormatAndDimensionNotCompatible(dstFormat, dimension); |
| |
| // If we're in compatibility mode and it's a compressed texture |
| // then we need to render the texture to test the results of the copy. |
| const extraTextureUsageFlags = |
| isCompressedTextureFormat(dstFormat) && this.isCompatibility |
| ? GPUTextureUsage.TEXTURE_BINDING |
| : 0; |
| const mipLevelCount = dimension === '1d' ? 1 : 4; |
| |
| // Create srcTexture and dstTexture |
| const srcTextureDesc: GPUTextureDescriptor = applyTextureBindingViewDimensionForTest({ |
| dimension, |
| size: srcTextureSize, |
| format: srcFormat, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, |
| mipLevelCount, |
| }); |
| const srcTexture = this.createTextureTracked(srcTextureDesc); |
| const dstTextureDesc: GPUTextureDescriptor = applyTextureBindingViewDimensionForTest({ |
| dimension, |
| size: dstTextureSize, |
| format: dstFormat, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | extraTextureUsageFlags, |
| mipLevelCount, |
| }); |
| const dstTexture = this.createTextureTracked(dstTextureDesc); |
| |
| // 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, blockWidth, blockHeight } = getBlockInfoForColorTextureFormat(srcFormat); |
| 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 appliedSize = { |
| width: appliedCopyWidth, |
| height: appliedCopyHeight, |
| depthOrArrayLayers: appliedCopyDepth, |
| }; |
| |
| { |
| const encoder = this.device.createCommandEncoder(); |
| encoder.copyTextureToTexture( |
| { texture: srcTexture, mipLevel: srcCopyLevel, origin: appliedSrcOffset }, |
| { texture: dstTexture, mipLevel: dstCopyLevel, origin: appliedDstOffset }, |
| appliedSize |
| ); |
| this.device.queue.submit([encoder.finish()]); |
| } |
| |
| 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); |
| |
| if (isCompressedTextureFormat(dstTexture.format) && this.isCompatibility) { |
| assert(textureFormatsAreViewCompatible(this.device.features, srcFormat, dstFormat)); |
| // compare by rendering. We need the expected texture to match |
| // the dstTexture so we'll create a texture where we supply |
| // all of the data in JavaScript. |
| const expectedTexture = this.createTextureTracked({ |
| size: [dstTexture.width, dstTexture.height, dstTexture.depthOrArrayLayers], |
| mipLevelCount: dstTexture.mipLevelCount, |
| format: dstTexture.format, |
| usage: dstTexture.usage, |
| }); |
| const expectedData = new Uint8Array(dstBufferSize); |
| |
| // Execute the equivalent of `copyTextureToTexture`, copying |
| // from `initialSrcData` to `expectedData`. |
| ttu.updateLinearTextureDataSubBox(this, dstFormat, appliedSize, { |
| src: { |
| dataLayout: { |
| bytesPerRow: srcBlocksPerRow * bytesPerBlock, |
| rowsPerImage: srcBlockRowsPerImage, |
| offset: 0, |
| }, |
| origin: appliedSrcOffset, |
| data: initialSrcData, |
| }, |
| dest: { |
| dataLayout: { |
| bytesPerRow: dstBlocksPerRow * bytesPerBlock, |
| rowsPerImage: dstBlockRowsPerImage, |
| offset: 0, |
| }, |
| origin: appliedDstOffset, |
| data: expectedData, |
| }, |
| }); |
| |
| // Upload `expectedData` to `expectedTexture`. If `copyTextureToTexture` |
| // worked then the contents of `dstTexture` should match `expectedTexture` |
| this.queue.writeTexture( |
| { texture: expectedTexture, mipLevel: dstCopyLevel }, |
| expectedData, |
| { |
| bytesPerRow: dstBlocksPerRow * bytesPerBlock, |
| rowsPerImage: dstBlockRowsPerImage, |
| }, |
| dstTextureSizeAtLevel |
| ); |
| |
| ttu.expectTexturesToMatchByRendering( |
| this, |
| dstTexture, |
| expectedTexture, |
| dstCopyLevel, |
| appliedDstOffset, |
| appliedSize |
| ); |
| return; |
| } |
| |
| // Copy the whole content of dstTexture at dstCopyLevel to dstBuffer. |
| const dstBufferDesc: GPUBufferDescriptor = { |
| size: dstBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, |
| }; |
| const dstBuffer = this.createBufferTracked(dstBufferDesc); |
| |
| { |
| const encoder = this.device.createCommandEncoder(); |
| 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, |
| }; |
| const bytesInRow = appliedCopyBlocksPerRow * bytesPerBlock; |
| |
| 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; |
| |
| memcpy( |
| { src: expectedUint8Data, start: expectedDataOffset, length: bytesInRow }, |
| { dst: expectedUint8DataWithPadding, start: expectedDataWithPaddingOffset } |
| ); |
| } |
| } |
| |
| if (isCompressedTextureFormat(dstFormat)) { |
| this.expectGPUBufferValuesPassCheck( |
| dstBuffer, |
| vals => checkElementsEqual(vals, expectedUint8DataWithPadding), |
| { |
| srcByteOffset: 0, |
| type: Uint8Array, |
| typedLength: expectedUint8DataWithPadding.length, |
| } |
| ); |
| return; |
| } |
| |
| assert(isRegularTextureFormat(dstFormat)); |
| const regularDstFormat = dstFormat as RegularTextureFormat; |
| |
| // Verify the content of the whole subresource of dstTexture at dstCopyLevel (in dstBuffer) is expected. |
| const checkByTextureFormat = (actual: Uint8Array) => { |
| const zero = { x: 0, y: 0, z: 0 }; |
| |
| const actTexelView = TexelView.fromTextureDataByReference(regularDstFormat, actual, { |
| bytesPerRow: bytesInRow, |
| rowsPerImage: dstBlockRowsPerImage, |
| subrectOrigin: zero, |
| subrectSize: dstTextureSizeAtLevel, |
| }); |
| const expTexelView = TexelView.fromTextureDataByReference( |
| regularDstFormat, |
| expectedUint8DataWithPadding, |
| { |
| bytesPerRow: bytesInRow, |
| rowsPerImage: dstBlockRowsPerImage, |
| subrectOrigin: zero, |
| subrectSize: dstTextureSizeAtLevel, |
| } |
| ); |
| |
| const failedPixelsMessage = findFailedPixels( |
| regularDstFormat, |
| zero, |
| dstTextureSizeAtLevel, |
| { actTexelView, expTexelView }, |
| { |
| maxFractionalDiff: 0, |
| } |
| ); |
| |
| if (failedPixelsMessage !== undefined) { |
| const msg = 'Texture level had unexpected contents:\n' + failedPixelsMessage; |
| return new ErrorWithExtra(msg, () => ({ |
| expTexelView, |
| actTexelView, |
| })); |
| } |
| |
| return undefined; |
| }; |
| |
| this.expectGPUBufferValuesPassCheck(dstBuffer, checkByTextureFormat, { |
| 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.createBufferTracked({ |
| size: outputBufferSize, |
| usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, |
| }); |
| 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 = this.makeBufferWithContents( |
| 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 hasStencil = isStencilTextureFormat(sourceTexture.format); |
| 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: hasStencil ? 'load' : undefined, |
| stencilStoreOp: hasStencil ? 'store' : undefined, |
| }, |
| }); |
| 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.createTextureTracked({ |
| format: 'rgba8unorm', |
| size: copySize, |
| usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, |
| }); |
| const hasStencil = isStencilTextureFormat(destinationTexture.format); |
| 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: hasStencil ? 'load' : undefined, |
| stencilStoreOp: hasStencil ? 'store' : undefined, |
| }, |
| }); |
| 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 = getBaseFormatForTextureFormat(srcFormat); |
| const dstBaseFormat = getBaseFormatForTextureFormat(dstFormat); |
| return ( |
| srcFormat === dstFormat || |
| (srcBaseFormat !== undefined && |
| dstBaseFormat !== undefined && |
| srcBaseFormat === dstBaseFormat) |
| ); |
| }) |
| .combine('dimension', kTextureDimensions) |
| .filter( |
| ({ dimension, srcFormat, dstFormat }) => |
| textureFormatAndDimensionPossiblyCompatible(dimension, srcFormat) && |
| textureFormatAndDimensionPossiblyCompatible(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(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 = getBaseFormatForTextureFormat(srcFormat); |
| const dstBaseFormat = getBaseFormatForTextureFormat(dstFormat); |
| return ( |
| srcFormat === dstFormat || |
| (srcBaseFormat !== undefined && |
| dstBaseFormat !== undefined && |
| srcBaseFormat === dstBaseFormat) |
| ); |
| }) |
| .combine('dimension', kTextureDimensions) |
| .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]) |
| ) |
| .fn(t => { |
| const { |
| dimension, |
| textureSizeInBlocks, |
| srcFormat, |
| dstFormat, |
| copyBoxOffsets, |
| srcCopyLevel, |
| dstCopyLevel, |
| } = t.params; |
| t.skipIfTextureFormatAndDimensionNotCompatible(srcFormat, dimension); |
| t.skipIfTextureFormatAndDimensionNotCompatible(dstFormat, dimension); |
| t.skipIfCopyTextureToTextureNotSupportedForFormat(srcFormat, dstFormat); |
| const { blockWidth: srcBlockWidth, blockHeight: srcBlockHeight } = |
| getBlockInfoForColorTextureFormat(srcFormat); |
| const { blockWidth: dstBlockWidth, blockHeight: dstBlockHeight } = |
| getBlockInfoForColorTextureFormat(dstFormat); |
| |
| 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 = getBaseFormatForTextureFormat(srcFormat); |
| const dstBaseFormat = getBaseFormatForTextureFormat(dstFormat); |
| return ( |
| srcFormat === dstFormat || |
| (srcBaseFormat !== undefined && |
| dstBaseFormat !== undefined && |
| srcBaseFormat === dstBaseFormat) |
| ); |
| }) |
| .combine('dimension', ['2d', '3d'] as const) |
| .filter( |
| ({ dimension, srcFormat, dstFormat }) => |
| textureFormatAndDimensionPossiblyCompatible(dimension, srcFormat) && |
| textureFormatAndDimensionPossiblyCompatible(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 }, |
| }, |
| // Maybe used with textureBindingViewDimension: 'cube' |
| { |
| srcTextureSize: { width: 32, height: 32, depthOrArrayLayers: 6 }, |
| dstTextureSize: { width: 32, height: 32, depthOrArrayLayers: 6 }, |
| }, |
| ]) |
| .combine('copyBoxOffsets', kCopyBoxOffsetsFor2DArrayTextures) |
| .combine('srcCopyLevel', [0, 3]) |
| .combine('dstCopyLevel', [0, 3]) |
| ) |
| .fn(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 = getBaseFormatForTextureFormat(srcFormat); |
| const dstBaseFormat = getBaseFormatForTextureFormat(dstFormat); |
| return ( |
| srcFormat === dstFormat || |
| (srcBaseFormat !== undefined && |
| dstBaseFormat !== undefined && |
| srcBaseFormat === dstBaseFormat) |
| ); |
| }) |
| .combine('dimension', ['2d', '3d'] as const) |
| .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]) |
| ) |
| .fn(t => { |
| const { |
| dimension, |
| textureSizeInBlocks, |
| srcFormat, |
| dstFormat, |
| copyBoxOffsets, |
| srcCopyLevel, |
| dstCopyLevel, |
| } = t.params; |
| const { blockWidth: srcBlockWidth, blockHeight: srcBlockHeight } = |
| getBlockInfoForColorTextureFormat(srcFormat); |
| const { blockWidth: dstBlockWidth, blockHeight: dstBlockHeight } = |
| getBlockInfoForColorTextureFormat(dstFormat); |
| |
| 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(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 |
| ); |
| }) |
| ) |
| .fn(t => { |
| const { |
| format, |
| srcTextureSize, |
| srcCopyLevel, |
| dstCopyLevel, |
| srcCopyBaseArrayLayer, |
| dstCopyBaseArrayLayer, |
| } = t.params; |
| t.skipIfTextureFormatNotSupported(format); |
| |
| const copySize: [number, number, number] = [ |
| srcTextureSize.width >> srcCopyLevel, |
| srcTextureSize.height >> srcCopyLevel, |
| srcTextureSize.depthOrArrayLayers - Math.max(srcCopyBaseArrayLayer, dstCopyBaseArrayLayer), |
| ]; |
| const sourceTexture = t.createTextureTracked({ |
| format, |
| size: srcTextureSize, |
| usage: |
| GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, |
| mipLevelCount: srcCopyLevel + 1, |
| }); |
| const destinationTexture = t.createTextureTracked({ |
| 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, |
| }); |
| |
| let initialStencilData: undefined | Uint8Array = undefined; |
| if (isStencilTextureFormat(format)) { |
| initialStencilData = t.getInitialStencilDataPerMipLevel(srcTextureSize, format, srcCopyLevel); |
| t.initializeStencilAspect( |
| sourceTexture, |
| initialStencilData, |
| srcCopyLevel, |
| srcCopyBaseArrayLayer, |
| copySize |
| ); |
| } |
| if (isDepthTextureFormat(format)) { |
| 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 (isStencilTextureFormat(format)) { |
| assert(initialStencilData !== undefined); |
| t.verifyStencilAspect( |
| destinationTexture, |
| initialStencilData, |
| dstCopyLevel, |
| dstCopyBaseArrayLayer, |
| copySize |
| ); |
| } |
| if (isDepthTextureFormat(format)) { |
| 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(t => { |
| t.skipIf(t.isCompatibility, 'multisample textures are not copyable in compatibility mode'); |
| const textureSize = [32, 16, 1] as const; |
| const kColorFormat = 'rgba8unorm'; |
| const kSampleCount = 4; |
| |
| const sourceTexture = t.createTextureTracked({ |
| format: kColorFormat, |
| size: textureSize, |
| usage: |
| GPUTextureUsage.COPY_SRC | |
| GPUTextureUsage.TEXTURE_BINDING | |
| GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount: kSampleCount, |
| }); |
| const destinationTexture = t.createTextureTracked({ |
| format: kColorFormat, |
| size: textureSize, |
| usage: |
| GPUTextureUsage.COPY_DST | |
| GPUTextureUsage.TEXTURE_BINDING | |
| GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount: kSampleCount, |
| }); |
| |
| // 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.createTextureTracked({ |
| format: kColorFormat, |
| size: textureSize, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| 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. |
| ` |
| ) |
| .params(u => |
| u.combine('format', kDepthStencilFormats).filter(t => isDepthTextureFormat(t.format)) |
| ) |
| .fn(t => { |
| const { format } = t.params; |
| |
| t.skipIf(t.isCompatibility, 'multisample textures are not copyable in compatibility mode'); |
| t.skipIfTextureFormatNotSupported(format); |
| |
| const textureSize = [32, 16, 1] as const; |
| const kSampleCount = 4; |
| |
| const sourceTexture = t.createTextureTracked({ |
| format, |
| size: textureSize, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount: kSampleCount, |
| }); |
| const destinationTexture = t.createTextureTracked({ |
| format, |
| size: textureSize, |
| usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount: kSampleCount, |
| }); |
| |
| 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, |
| 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', |
| ...(isStencilTextureFormat(format) && { |
| stencilLoadOp: 'clear', |
| stencilStoreOp: '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, |
| depthCompare: 'equal', |
| depthWriteEnabled: false, |
| }, |
| multisample: { |
| count: kSampleCount, |
| }, |
| }); |
| const multisampledColorTexture = t.createTextureTracked({ |
| format: kColorFormat, |
| size: textureSize, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount: kSampleCount, |
| }); |
| const colorTextureAsResolveTarget = t.createTextureTracked({ |
| format: kColorFormat, |
| size: textureSize, |
| usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, |
| }); |
| |
| 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', |
| ...(isStencilTextureFormat(format) && { |
| stencilLoadOp: 'clear', |
| stencilStoreOp: '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 }, |
| }); |
| }); |
| |
| g.test('copy_multisampled_stencil') |
| .desc( |
| ` |
| Validate the correctness of copyTextureToTexture() with multisampled stencil formats. |
| ` |
| ) |
| .unimplemented(); |