From 138b023bface374d4dcab5b787adb3a662f3c241 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Wed, 24 Jan 2024 17:45:56 -0800 Subject: [PATCH] Put common parts of texture_zero.spec.ts into a separate file Saw this MAINTENANCE_TODO so did it --- .../resource_init/check_texture/by_copy.ts | 3 +- .../resource_init/check_texture/by_ds_test.ts | 3 +- .../check_texture/by_sampling.ts | 3 +- .../check_texture/texture_zero_init_test.ts | 543 +++++++++++++++++ .../resource_init/texture_zero.spec.ts | 554 +----------------- 5 files changed, 560 insertions(+), 546 deletions(-) create mode 100644 src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts diff --git a/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts b/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts index 2a4ca5e6a4af..546db16c7ea9 100644 --- a/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts +++ b/src/webgpu/api/operation/resource_init/check_texture/by_copy.ts @@ -1,7 +1,8 @@ import { assert } from '../../../../../common/util/util.js'; import { kTextureFormatInfo, EncodableTextureFormat } from '../../../../format_info.js'; import { virtualMipSize } from '../../../../util/texture/base.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; export const checkContentsByBufferCopy: CheckContents = ( t, diff --git a/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts b/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts index 86460624526a..72ef2909f2a4 100644 --- a/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts +++ b/src/webgpu/api/operation/resource_init/check_texture/by_ds_test.ts @@ -2,7 +2,8 @@ import { assert } from '../../../../../common/util/util.js'; import { kTextureFormatInfo } from '../../../../format_info.js'; import { GPUTest } from '../../../../gpu_test.js'; import { virtualMipSize } from '../../../../util/texture/base.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; function makeFullscreenVertexModule(device: GPUDevice) { return device.createShaderModule({ diff --git a/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts b/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts index 284b8c4f0f79..fd268be3b987 100644 --- a/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts +++ b/src/webgpu/api/operation/resource_init/check_texture/by_sampling.ts @@ -6,7 +6,8 @@ import { getSingleDataType, getComponentReadbackTraits, } from '../../../../util/texture/texel_data.js'; -import { CheckContents } from '../texture_zero.spec.js'; + +import { CheckContents } from './texture_zero_init_test.js'; export const checkContentsBySampling: CheckContents = ( t, diff --git a/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts b/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts new file mode 100644 index 000000000000..22ffe0a3e518 --- /dev/null +++ b/src/webgpu/api/operation/resource_init/check_texture/texture_zero_init_test.ts @@ -0,0 +1,543 @@ +import { TestCaseRecorder, TestParams } from '../../../../../common/framework/fixture.js'; +import { + kUnitCaseParamsBuilder, + ParamTypeOf, +} from '../../../../../common/framework/params_builder.js'; +import { assert, unreachable } from '../../../../../common/util/util.js'; +import { kTextureAspects, kTextureDimensions } from '../../../../capability_info.js'; +import { GPUConst } from '../../../../constants.js'; +import { + kTextureFormatInfo, + kUncompressedTextureFormats, + textureDimensionAndFormatCompatible, + UncompressedTextureFormat, + EncodableTextureFormat, +} from '../../../../format_info.js'; +import { GPUTest, GPUTestSubcaseBatchState } from '../../../../gpu_test.js'; +import { virtualMipSize } from '../../../../util/texture/base.js'; +import { createTextureUploadBuffer } from '../../../../util/texture/layout.js'; +import { BeginEndRange, SubresourceRange } from '../../../../util/texture/subresource.js'; +import { + PerTexelComponent, + kTexelRepresentationInfo, +} from '../../../../util/texture/texel_data.js'; + +export enum UninitializeMethod { + Creation = 'Creation', // The texture was just created. It is uninitialized. + StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear" +} +const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[]; + +export const enum ReadMethod { + Sample = 'Sample', // The texture is sampled from + CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer + CopyToTexture = 'CopyToTexture', // The texture is copied to another texture + DepthTest = 'DepthTest', // The texture is read as a depth buffer + StencilTest = 'StencilTest', // The texture is read as a stencil buffer + ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment + Storage = 'Storage', // Read the texture as a storage texture +} + +// Test with these mip level counts +type MipLevels = 1 | 5; +const kMipLevelCounts: MipLevels[] = [1, 5]; + +// For each mip level count, define the mip ranges to leave uninitialized. +const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = { + 1: [{ begin: 0, end: 1 }], // Test the only mip + 5: [ + { begin: 0, end: 2 }, + { begin: 3, end: 4 }, + ], // Test a range and a single mip +}; + +// Test with these sample counts. +const kSampleCounts: number[] = [1, 4]; + +// Test with these layer counts. +type LayerCounts = 1 | 7; + +// For each layer count, define the layers to leave uninitialized. +const kUninitializedLayerRangesToTest: { [k in LayerCounts]: BeginEndRange[] } = { + 1: [{ begin: 0, end: 1 }], // Test the only layer + 7: [ + { begin: 2, end: 4 }, + { begin: 6, end: 7 }, + ], // Test a range and a single layer +}; + +// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format, +// the data for each value may have a different representation. These enums are converted to a +// representation such that their values can be compared. ex.) An integer is needed to upload to an +// unsigned normalized format, but its value is read as a float in the shader. +export const enum InitializedState { + Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero. + Zero, // We check that uninitialized subresources are in this state when read back. +} + +const initializedStateAsFloat = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 1, +}; + +const initializedStateAsUint = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 1, +}; + +const initializedStateAsSint = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: -1, +}; + +function initializedStateAsColor( + state: InitializedState, + format: GPUTextureFormat +): [number, number, number, number] { + let value; + if (format.indexOf('uint') !== -1) { + value = initializedStateAsUint[state]; + } else if (format.indexOf('sint') !== -1) { + value = initializedStateAsSint[state]; + } else { + value = initializedStateAsFloat[state]; + } + return [value, value, value, value]; +} + +const initializedStateAsDepth = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 0.8, +}; + +const initializedStateAsStencil = { + [InitializedState.Zero]: 0, + [InitializedState.Canary]: 42, +}; + +export function getRequiredTextureUsage( + format: UncompressedTextureFormat, + sampleCount: number, + uninitializeMethod: UninitializeMethod, + readMethod: ReadMethod +): GPUTextureUsageFlags { + let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST; + + switch (uninitializeMethod) { + case UninitializeMethod.Creation: + break; + case UninitializeMethod.StoreOpClear: + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + break; + default: + unreachable(); + } + + switch (readMethod) { + case ReadMethod.CopyToBuffer: + case ReadMethod.CopyToTexture: + usage |= GPUConst.TextureUsage.COPY_SRC; + break; + case ReadMethod.Sample: + usage |= GPUConst.TextureUsage.TEXTURE_BINDING; + break; + case ReadMethod.Storage: + usage |= GPUConst.TextureUsage.STORAGE_BINDING; + break; + case ReadMethod.DepthTest: + case ReadMethod.StencilTest: + case ReadMethod.ColorBlending: + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + break; + default: + unreachable(); + } + + if (sampleCount > 1) { + // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize + // canary data in multisampled textures. + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + } + + if (!kTextureFormatInfo[format].copyDst) { + // Copies are not possible. We need OutputAttachment to initialize + // canary data. + assert(kTextureFormatInfo[format].renderable); + usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; + } + + return usage; +} + +export class TextureZeroInitTest extends GPUTest { + readonly stateToTexelComponents: { [k in InitializedState]: PerTexelComponent }; + + private p: TextureZeroParams; + constructor(sharedState: GPUTestSubcaseBatchState, rec: TestCaseRecorder, params: TestParams) { + super(sharedState, rec, params); + this.p = params as TextureZeroParams; + + const stateToTexelComponents = (state: InitializedState) => { + const [R, G, B, A] = initializedStateAsColor(state, this.p.format); + return { + R, + G, + B, + A, + Depth: initializedStateAsDepth[state], + Stencil: initializedStateAsStencil[state], + }; + }; + + this.stateToTexelComponents = { + [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero), + [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary), + }; + } + + get textureWidth(): number { + let width = 1 << this.p.mipLevelCount; + if (this.p.nonPowerOfTwo) { + width = 2 * width - 1; + } + return width; + } + + get textureHeight(): number { + if (this.p.dimension === '1d') { + return 1; + } + + let height = 1 << this.p.mipLevelCount; + if (this.p.nonPowerOfTwo) { + height = 2 * height - 1; + } + return height; + } + + get textureDepth(): number { + return this.p.dimension === '3d' ? 11 : 1; + } + + get textureDepthOrArrayLayers(): number { + return this.p.dimension === '2d' ? this.p.layerCount : this.textureDepth; + } + + // Used to iterate subresources and check that their uninitialized contents are zero when accessed + *iterateUninitializedSubresources(): Generator { + for (const mipRange of kUninitializedMipRangesToTest[this.p.mipLevelCount]) { + for (const layerRange of kUninitializedLayerRangesToTest[this.p.layerCount]) { + yield new SubresourceRange({ mipRange, layerRange }); + } + } + } + + // Used to iterate and initialize other subresources not checked for zero-initialization. + // Zero-initialization of uninitialized subresources should not have side effects on already + // initialized subresources. + *iterateInitializedSubresources(): Generator { + const uninitialized: boolean[][] = new Array(this.p.mipLevelCount); + for (let level = 0; level < uninitialized.length; ++level) { + uninitialized[level] = new Array(this.p.layerCount); + } + for (const subresources of this.iterateUninitializedSubresources()) { + for (const { level, layer } of subresources.each()) { + uninitialized[level][layer] = true; + } + } + for (let level = 0; level < uninitialized.length; ++level) { + for (let layer = 0; layer < uninitialized[level].length; ++layer) { + if (!uninitialized[level][layer]) { + yield new SubresourceRange({ + mipRange: { begin: level, count: 1 }, + layerRange: { begin: layer, count: 1 }, + }); + } + } + } + } + + *generateTextureViewDescriptorsForRendering( + aspect: GPUTextureAspect, + subresourceRange?: SubresourceRange + ): Generator { + const viewDescriptor: GPUTextureViewDescriptor = { + dimension: '2d', + aspect, + }; + + if (subresourceRange === undefined) { + return viewDescriptor; + } + + for (const { level, layer } of subresourceRange.each()) { + yield { + ...viewDescriptor, + baseMipLevel: level, + mipLevelCount: 1, + baseArrayLayer: layer, + arrayLayerCount: 1, + }; + } + } + + private initializeWithStoreOp( + state: InitializedState, + texture: GPUTexture, + subresourceRange?: SubresourceRange + ): void { + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.pushDebugGroup('initializeWithStoreOp'); + + for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering( + 'all', + subresourceRange + )) { + if (kTextureFormatInfo[this.p.format].color) { + commandEncoder + .beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(viewDescriptor), + storeOp: 'store', + clearValue: initializedStateAsColor(state, this.p.format), + loadOp: 'clear', + }, + ], + }) + .end(); + } else { + const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { + view: texture.createView(viewDescriptor), + }; + if (kTextureFormatInfo[this.p.format].depth) { + depthStencilAttachment.depthClearValue = initializedStateAsDepth[state]; + depthStencilAttachment.depthLoadOp = 'clear'; + depthStencilAttachment.depthStoreOp = 'store'; + } + if (kTextureFormatInfo[this.p.format].stencil) { + depthStencilAttachment.stencilClearValue = initializedStateAsStencil[state]; + depthStencilAttachment.stencilLoadOp = 'clear'; + depthStencilAttachment.stencilStoreOp = 'store'; + } + commandEncoder + .beginRenderPass({ + colorAttachments: [], + depthStencilAttachment, + }) + .end(); + } + } + + commandEncoder.popDebugGroup(); + this.queue.submit([commandEncoder.finish()]); + } + + private initializeWithCopy( + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange + ): void { + assert(this.p.format in kTextureFormatInfo); + const format = this.p.format as EncodableTextureFormat; + + const firstSubresource = subresourceRange.each().next().value; + assert(typeof firstSubresource !== 'undefined'); + + const [largestWidth, largestHeight, largestDepth] = virtualMipSize( + this.p.dimension, + [this.textureWidth, this.textureHeight, this.textureDepth], + firstSubresource.level + ); + + const rep = kTexelRepresentationInfo[format]; + const texelData = new Uint8Array(rep.pack(rep.encode(this.stateToTexelComponents[state]))); + const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer( + texelData, + this.device, + format, + this.p.dimension, + [largestWidth, largestHeight, largestDepth] + ); + + const commandEncoder = this.device.createCommandEncoder(); + + for (const { level, layer } of subresourceRange.each()) { + const [width, height, depth] = virtualMipSize( + this.p.dimension, + [this.textureWidth, this.textureHeight, this.textureDepth], + level + ); + + commandEncoder.copyBufferToTexture( + { + buffer, + bytesPerRow, + rowsPerImage, + }, + { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } }, + { width, height, depthOrArrayLayers: depth } + ); + } + this.queue.submit([commandEncoder.finish()]); + buffer.destroy(); + } + + initializeTexture( + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange + ): void { + if (this.p.sampleCount > 1 || !kTextureFormatInfo[this.p.format].copyDst) { + // Copies to multisampled textures not yet specified. + // Use a storeOp for now. + assert(kTextureFormatInfo[this.p.format].renderable); + this.initializeWithStoreOp(state, texture, subresourceRange); + } else { + this.initializeWithCopy(texture, state, subresourceRange); + } + } + + discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void { + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.pushDebugGroup('discardTexture'); + + for (const desc of this.generateTextureViewDescriptorsForRendering('all', subresourceRange)) { + if (kTextureFormatInfo[this.p.format].color) { + commandEncoder + .beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(desc), + storeOp: 'discard', + loadOp: 'load', + }, + ], + }) + .end(); + } else { + const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { + view: texture.createView(desc), + }; + if (kTextureFormatInfo[this.p.format].depth) { + depthStencilAttachment.depthLoadOp = 'load'; + depthStencilAttachment.depthStoreOp = 'discard'; + } + if (kTextureFormatInfo[this.p.format].stencil) { + depthStencilAttachment.stencilLoadOp = 'load'; + depthStencilAttachment.stencilStoreOp = 'discard'; + } + commandEncoder + .beginRenderPass({ + colorAttachments: [], + depthStencilAttachment, + }) + .end(); + } + } + + commandEncoder.popDebugGroup(); + this.queue.submit([commandEncoder.finish()]); + } +} + +export const kTestParams = kUnitCaseParamsBuilder + .combine('dimension', kTextureDimensions) + .combine('readMethod', [ + ReadMethod.CopyToBuffer, + ReadMethod.CopyToTexture, + ReadMethod.Sample, + ReadMethod.DepthTest, + ReadMethod.StencilTest, + ]) + // [3] compressed formats + .combine('format', kUncompressedTextureFormats) + .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format)) + .beginSubcases() + .combine('aspect', kTextureAspects) + .unless(({ readMethod, format, aspect }) => { + const info = kTextureFormatInfo[format]; + return ( + (readMethod === ReadMethod.DepthTest && (!info.depth || aspect === 'stencil-only')) || + (readMethod === ReadMethod.StencilTest && (!info.stencil || aspect === 'depth-only')) || + (readMethod === ReadMethod.ColorBlending && !info.color) || + // [1]: Test with depth/stencil sampling + (readMethod === ReadMethod.Sample && (!!info.depth || !!info.stencil)) || + (aspect === 'depth-only' && !info.depth) || + (aspect === 'stencil-only' && !info.stencil) || + (aspect === 'all' && !!info.depth && !!info.stencil) || + // Cannot copy from a packed depth format. + // [2]: Test copying out of the stencil aspect. + ((readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) && + (format === 'depth24plus' || format === 'depth24plus-stencil8')) + ); + }) + .combine('mipLevelCount', kMipLevelCounts) + // 1D texture can only have a single mip level + .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1) + .combine('sampleCount', kSampleCounts) + .unless( + ({ readMethod, sampleCount }) => + // We can only read from multisampled textures by sampling. + sampleCount > 1 && + (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) + ) + // Multisampled textures may only have one mip + .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1) + .combine('uninitializeMethod', kUninitializeMethods) + .unless(({ dimension, readMethod, uninitializeMethod, format, sampleCount }) => { + const formatInfo = kTextureFormatInfo[format]; + return ( + dimension !== '2d' && + (sampleCount > 1 || + !!formatInfo.depth || + !!formatInfo.stencil || + readMethod === ReadMethod.DepthTest || + readMethod === ReadMethod.StencilTest || + readMethod === ReadMethod.ColorBlending || + uninitializeMethod === UninitializeMethod.StoreOpClear) + ); + }) + .expandWithParams(function* ({ dimension }) { + switch (dimension) { + case '2d': + yield { layerCount: 1 as LayerCounts }; + yield { layerCount: 7 as LayerCounts }; + break; + case '1d': + case '3d': + yield { layerCount: 1 as LayerCounts }; + break; + } + }) + // Multisampled 3D / 2D array textures not supported. + .unless(({ sampleCount, layerCount }) => sampleCount > 1 && layerCount > 1) + .unless(({ format, sampleCount, uninitializeMethod, readMethod }) => { + const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod); + const info = kTextureFormatInfo[format]; + + return ( + ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 && !info.renderable) || + ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.color?.storage) || + (sampleCount > 1 && !info.multisample) + ); + }) + .combine('nonPowerOfTwo', [false, true]) + .combine('canaryOnCreation', [false, true]) + .filter(({ canaryOnCreation, format }) => { + // We can only initialize the texture if it's encodable or renderable. + const canInitialize = format in kTextureFormatInfo || kTextureFormatInfo[format].renderable; + + // Filter out cases where we want canary values but can't initialize. + return !canaryOnCreation || canInitialize; + }); + +type TextureZeroParams = ParamTypeOf; + +export type CheckContents = ( + t: TextureZeroInitTest, + params: TextureZeroParams, + texture: GPUTexture, + state: InitializedState, + subresourceRange: SubresourceRange +) => void; diff --git a/src/webgpu/api/operation/resource_init/texture_zero.spec.ts b/src/webgpu/api/operation/resource_init/texture_zero.spec.ts index 3f0baeccbd3c..ee4c141aef6b 100644 --- a/src/webgpu/api/operation/resource_init/texture_zero.spec.ts +++ b/src/webgpu/api/operation/resource_init/texture_zero.spec.ts @@ -7,550 +7,9 @@ TODO: - test compressed texture formats [3] `; -// MAINTENANCE_TODO: This is a test file, it probably shouldn't export anything. -// Everything that's exported should be moved to another file. - -import { TestCaseRecorder, TestParams } from '../../../../common/framework/fixture.js'; -import { - kUnitCaseParamsBuilder, - ParamTypeOf, -} from '../../../../common/framework/params_builder.js'; import { makeTestGroup } from '../../../../common/framework/test_group.js'; -import { assert, unreachable } from '../../../../common/util/util.js'; -import { kTextureAspects, kTextureDimensions } from '../../../capability_info.js'; -import { GPUConst } from '../../../constants.js'; -import { - kTextureFormatInfo, - kUncompressedTextureFormats, - textureDimensionAndFormatCompatible, - UncompressedTextureFormat, - EncodableTextureFormat, -} from '../../../format_info.js'; -import { GPUTest, GPUTestSubcaseBatchState } from '../../../gpu_test.js'; -import { virtualMipSize } from '../../../util/texture/base.js'; -import { createTextureUploadBuffer } from '../../../util/texture/layout.js'; -import { BeginEndRange, SubresourceRange } from '../../../util/texture/subresource.js'; -import { PerTexelComponent, kTexelRepresentationInfo } from '../../../util/texture/texel_data.js'; - -export enum UninitializeMethod { - Creation = 'Creation', // The texture was just created. It is uninitialized. - StoreOpClear = 'StoreOpClear', // The texture was rendered to with GPUStoreOp "clear" -} -const kUninitializeMethods = Object.keys(UninitializeMethod) as UninitializeMethod[]; - -export const enum ReadMethod { - Sample = 'Sample', // The texture is sampled from - CopyToBuffer = 'CopyToBuffer', // The texture is copied to a buffer - CopyToTexture = 'CopyToTexture', // The texture is copied to another texture - DepthTest = 'DepthTest', // The texture is read as a depth buffer - StencilTest = 'StencilTest', // The texture is read as a stencil buffer - ColorBlending = 'ColorBlending', // Read the texture by blending as a color attachment - Storage = 'Storage', // Read the texture as a storage texture -} - -// Test with these mip level counts -type MipLevels = 1 | 5; -const kMipLevelCounts: MipLevels[] = [1, 5]; - -// For each mip level count, define the mip ranges to leave uninitialized. -const kUninitializedMipRangesToTest: { [k in MipLevels]: BeginEndRange[] } = { - 1: [{ begin: 0, end: 1 }], // Test the only mip - 5: [ - { begin: 0, end: 2 }, - { begin: 3, end: 4 }, - ], // Test a range and a single mip -}; - -// Test with these sample counts. -const kSampleCounts: number[] = [1, 4]; - -// Test with these layer counts. -type LayerCounts = 1 | 7; - -// For each layer count, define the layers to leave uninitialized. -const kUninitializedLayerRangesToTest: { [k in LayerCounts]: BeginEndRange[] } = { - 1: [{ begin: 0, end: 1 }], // Test the only layer - 7: [ - { begin: 2, end: 4 }, - { begin: 6, end: 7 }, - ], // Test a range and a single layer -}; - -// Enums to abstract over color / depth / stencil values in textures. Depending on the texture format, -// the data for each value may have a different representation. These enums are converted to a -// representation such that their values can be compared. ex.) An integer is needed to upload to an -// unsigned normalized format, but its value is read as a float in the shader. -export const enum InitializedState { - Canary, // Set on initialized subresources. It should stay the same. On discarded resources, we should observe zero. - Zero, // We check that uninitialized subresources are in this state when read back. -} - -const initializedStateAsFloat = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 1, -}; - -const initializedStateAsUint = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 1, -}; - -const initializedStateAsSint = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: -1, -}; - -function initializedStateAsColor( - state: InitializedState, - format: GPUTextureFormat -): [number, number, number, number] { - let value; - if (format.indexOf('uint') !== -1) { - value = initializedStateAsUint[state]; - } else if (format.indexOf('sint') !== -1) { - value = initializedStateAsSint[state]; - } else { - value = initializedStateAsFloat[state]; - } - return [value, value, value, value]; -} - -const initializedStateAsDepth = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 0.8, -}; - -const initializedStateAsStencil = { - [InitializedState.Zero]: 0, - [InitializedState.Canary]: 42, -}; - -function getRequiredTextureUsage( - format: UncompressedTextureFormat, - sampleCount: number, - uninitializeMethod: UninitializeMethod, - readMethod: ReadMethod -): GPUTextureUsageFlags { - let usage: GPUTextureUsageFlags = GPUConst.TextureUsage.COPY_DST; - - switch (uninitializeMethod) { - case UninitializeMethod.Creation: - break; - case UninitializeMethod.StoreOpClear: - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - break; - default: - unreachable(); - } - - switch (readMethod) { - case ReadMethod.CopyToBuffer: - case ReadMethod.CopyToTexture: - usage |= GPUConst.TextureUsage.COPY_SRC; - break; - case ReadMethod.Sample: - usage |= GPUConst.TextureUsage.TEXTURE_BINDING; - break; - case ReadMethod.Storage: - usage |= GPUConst.TextureUsage.STORAGE_BINDING; - break; - case ReadMethod.DepthTest: - case ReadMethod.StencilTest: - case ReadMethod.ColorBlending: - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - break; - default: - unreachable(); - } - - if (sampleCount > 1) { - // Copies to multisampled textures are not allowed. We need OutputAttachment to initialize - // canary data in multisampled textures. - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - } - - if (!kTextureFormatInfo[format].copyDst) { - // Copies are not possible. We need OutputAttachment to initialize - // canary data. - assert(kTextureFormatInfo[format].renderable); - usage |= GPUConst.TextureUsage.RENDER_ATTACHMENT; - } - - return usage; -} - -export class TextureZeroInitTest extends GPUTest { - readonly stateToTexelComponents: { [k in InitializedState]: PerTexelComponent }; - - private p: TextureZeroParams; - constructor(sharedState: GPUTestSubcaseBatchState, rec: TestCaseRecorder, params: TestParams) { - super(sharedState, rec, params); - this.p = params as TextureZeroParams; - - const stateToTexelComponents = (state: InitializedState) => { - const [R, G, B, A] = initializedStateAsColor(state, this.p.format); - return { - R, - G, - B, - A, - Depth: initializedStateAsDepth[state], - Stencil: initializedStateAsStencil[state], - }; - }; - - this.stateToTexelComponents = { - [InitializedState.Zero]: stateToTexelComponents(InitializedState.Zero), - [InitializedState.Canary]: stateToTexelComponents(InitializedState.Canary), - }; - } - - get textureWidth(): number { - let width = 1 << this.p.mipLevelCount; - if (this.p.nonPowerOfTwo) { - width = 2 * width - 1; - } - return width; - } - - get textureHeight(): number { - if (this.p.dimension === '1d') { - return 1; - } - - let height = 1 << this.p.mipLevelCount; - if (this.p.nonPowerOfTwo) { - height = 2 * height - 1; - } - return height; - } - - get textureDepth(): number { - return this.p.dimension === '3d' ? 11 : 1; - } - - get textureDepthOrArrayLayers(): number { - return this.p.dimension === '2d' ? this.p.layerCount : this.textureDepth; - } - - // Used to iterate subresources and check that their uninitialized contents are zero when accessed - *iterateUninitializedSubresources(): Generator { - for (const mipRange of kUninitializedMipRangesToTest[this.p.mipLevelCount]) { - for (const layerRange of kUninitializedLayerRangesToTest[this.p.layerCount]) { - yield new SubresourceRange({ mipRange, layerRange }); - } - } - } - - // Used to iterate and initialize other subresources not checked for zero-initialization. - // Zero-initialization of uninitialized subresources should not have side effects on already - // initialized subresources. - *iterateInitializedSubresources(): Generator { - const uninitialized: boolean[][] = new Array(this.p.mipLevelCount); - for (let level = 0; level < uninitialized.length; ++level) { - uninitialized[level] = new Array(this.p.layerCount); - } - for (const subresources of this.iterateUninitializedSubresources()) { - for (const { level, layer } of subresources.each()) { - uninitialized[level][layer] = true; - } - } - for (let level = 0; level < uninitialized.length; ++level) { - for (let layer = 0; layer < uninitialized[level].length; ++layer) { - if (!uninitialized[level][layer]) { - yield new SubresourceRange({ - mipRange: { begin: level, count: 1 }, - layerRange: { begin: layer, count: 1 }, - }); - } - } - } - } - - *generateTextureViewDescriptorsForRendering( - aspect: GPUTextureAspect, - subresourceRange?: SubresourceRange - ): Generator { - const viewDescriptor: GPUTextureViewDescriptor = { - dimension: '2d', - aspect, - }; - - if (subresourceRange === undefined) { - return viewDescriptor; - } - - for (const { level, layer } of subresourceRange.each()) { - yield { - ...viewDescriptor, - baseMipLevel: level, - mipLevelCount: 1, - baseArrayLayer: layer, - arrayLayerCount: 1, - }; - } - } - - private initializeWithStoreOp( - state: InitializedState, - texture: GPUTexture, - subresourceRange?: SubresourceRange - ): void { - const commandEncoder = this.device.createCommandEncoder(); - commandEncoder.pushDebugGroup('initializeWithStoreOp'); - - for (const viewDescriptor of this.generateTextureViewDescriptorsForRendering( - 'all', - subresourceRange - )) { - if (kTextureFormatInfo[this.p.format].color) { - commandEncoder - .beginRenderPass({ - colorAttachments: [ - { - view: texture.createView(viewDescriptor), - storeOp: 'store', - clearValue: initializedStateAsColor(state, this.p.format), - loadOp: 'clear', - }, - ], - }) - .end(); - } else { - const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - view: texture.createView(viewDescriptor), - }; - if (kTextureFormatInfo[this.p.format].depth) { - depthStencilAttachment.depthClearValue = initializedStateAsDepth[state]; - depthStencilAttachment.depthLoadOp = 'clear'; - depthStencilAttachment.depthStoreOp = 'store'; - } - if (kTextureFormatInfo[this.p.format].stencil) { - depthStencilAttachment.stencilClearValue = initializedStateAsStencil[state]; - depthStencilAttachment.stencilLoadOp = 'clear'; - depthStencilAttachment.stencilStoreOp = 'store'; - } - commandEncoder - .beginRenderPass({ - colorAttachments: [], - depthStencilAttachment, - }) - .end(); - } - } - - commandEncoder.popDebugGroup(); - this.queue.submit([commandEncoder.finish()]); - } - - private initializeWithCopy( - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange - ): void { - assert(this.p.format in kTextureFormatInfo); - const format = this.p.format as EncodableTextureFormat; - - const firstSubresource = subresourceRange.each().next().value; - assert(typeof firstSubresource !== 'undefined'); - - const [largestWidth, largestHeight, largestDepth] = virtualMipSize( - this.p.dimension, - [this.textureWidth, this.textureHeight, this.textureDepth], - firstSubresource.level - ); - - const rep = kTexelRepresentationInfo[format]; - const texelData = new Uint8Array(rep.pack(rep.encode(this.stateToTexelComponents[state]))); - const { buffer, bytesPerRow, rowsPerImage } = createTextureUploadBuffer( - texelData, - this.device, - format, - this.p.dimension, - [largestWidth, largestHeight, largestDepth] - ); - - const commandEncoder = this.device.createCommandEncoder(); - - for (const { level, layer } of subresourceRange.each()) { - const [width, height, depth] = virtualMipSize( - this.p.dimension, - [this.textureWidth, this.textureHeight, this.textureDepth], - level - ); - - commandEncoder.copyBufferToTexture( - { - buffer, - bytesPerRow, - rowsPerImage, - }, - { texture, mipLevel: level, origin: { x: 0, y: 0, z: layer } }, - { width, height, depthOrArrayLayers: depth } - ); - } - this.queue.submit([commandEncoder.finish()]); - buffer.destroy(); - } - - initializeTexture( - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange - ): void { - if (this.p.sampleCount > 1 || !kTextureFormatInfo[this.p.format].copyDst) { - // Copies to multisampled textures not yet specified. - // Use a storeOp for now. - assert(kTextureFormatInfo[this.p.format].renderable); - this.initializeWithStoreOp(state, texture, subresourceRange); - } else { - this.initializeWithCopy(texture, state, subresourceRange); - } - } - - discardTexture(texture: GPUTexture, subresourceRange: SubresourceRange): void { - const commandEncoder = this.device.createCommandEncoder(); - commandEncoder.pushDebugGroup('discardTexture'); - - for (const desc of this.generateTextureViewDescriptorsForRendering('all', subresourceRange)) { - if (kTextureFormatInfo[this.p.format].color) { - commandEncoder - .beginRenderPass({ - colorAttachments: [ - { - view: texture.createView(desc), - storeOp: 'discard', - loadOp: 'load', - }, - ], - }) - .end(); - } else { - const depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - view: texture.createView(desc), - }; - if (kTextureFormatInfo[this.p.format].depth) { - depthStencilAttachment.depthLoadOp = 'load'; - depthStencilAttachment.depthStoreOp = 'discard'; - } - if (kTextureFormatInfo[this.p.format].stencil) { - depthStencilAttachment.stencilLoadOp = 'load'; - depthStencilAttachment.stencilStoreOp = 'discard'; - } - commandEncoder - .beginRenderPass({ - colorAttachments: [], - depthStencilAttachment, - }) - .end(); - } - } - - commandEncoder.popDebugGroup(); - this.queue.submit([commandEncoder.finish()]); - } -} - -const kTestParams = kUnitCaseParamsBuilder - .combine('dimension', kTextureDimensions) - .combine('readMethod', [ - ReadMethod.CopyToBuffer, - ReadMethod.CopyToTexture, - ReadMethod.Sample, - ReadMethod.DepthTest, - ReadMethod.StencilTest, - ]) - // [3] compressed formats - .combine('format', kUncompressedTextureFormats) - .filter(({ dimension, format }) => textureDimensionAndFormatCompatible(dimension, format)) - .beginSubcases() - .combine('aspect', kTextureAspects) - .unless(({ readMethod, format, aspect }) => { - const info = kTextureFormatInfo[format]; - return ( - (readMethod === ReadMethod.DepthTest && (!info.depth || aspect === 'stencil-only')) || - (readMethod === ReadMethod.StencilTest && (!info.stencil || aspect === 'depth-only')) || - (readMethod === ReadMethod.ColorBlending && !info.color) || - // [1]: Test with depth/stencil sampling - (readMethod === ReadMethod.Sample && (!!info.depth || !!info.stencil)) || - (aspect === 'depth-only' && !info.depth) || - (aspect === 'stencil-only' && !info.stencil) || - (aspect === 'all' && !!info.depth && !!info.stencil) || - // Cannot copy from a packed depth format. - // [2]: Test copying out of the stencil aspect. - ((readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) && - (format === 'depth24plus' || format === 'depth24plus-stencil8')) - ); - }) - .combine('mipLevelCount', kMipLevelCounts) - // 1D texture can only have a single mip level - .unless(p => p.dimension === '1d' && p.mipLevelCount !== 1) - .combine('sampleCount', kSampleCounts) - .unless( - ({ readMethod, sampleCount }) => - // We can only read from multisampled textures by sampling. - sampleCount > 1 && - (readMethod === ReadMethod.CopyToBuffer || readMethod === ReadMethod.CopyToTexture) - ) - // Multisampled textures may only have one mip - .unless(({ sampleCount, mipLevelCount }) => sampleCount > 1 && mipLevelCount > 1) - .combine('uninitializeMethod', kUninitializeMethods) - .unless(({ dimension, readMethod, uninitializeMethod, format, sampleCount }) => { - const formatInfo = kTextureFormatInfo[format]; - return ( - dimension !== '2d' && - (sampleCount > 1 || - !!formatInfo.depth || - !!formatInfo.stencil || - readMethod === ReadMethod.DepthTest || - readMethod === ReadMethod.StencilTest || - readMethod === ReadMethod.ColorBlending || - uninitializeMethod === UninitializeMethod.StoreOpClear) - ); - }) - .expandWithParams(function* ({ dimension }) { - switch (dimension) { - case '2d': - yield { layerCount: 1 as LayerCounts }; - yield { layerCount: 7 as LayerCounts }; - break; - case '1d': - case '3d': - yield { layerCount: 1 as LayerCounts }; - break; - } - }) - // Multisampled 3D / 2D array textures not supported. - .unless(({ sampleCount, layerCount }) => sampleCount > 1 && layerCount > 1) - .unless(({ format, sampleCount, uninitializeMethod, readMethod }) => { - const usage = getRequiredTextureUsage(format, sampleCount, uninitializeMethod, readMethod); - const info = kTextureFormatInfo[format]; - - return ( - ((usage & GPUConst.TextureUsage.RENDER_ATTACHMENT) !== 0 && !info.renderable) || - ((usage & GPUConst.TextureUsage.STORAGE_BINDING) !== 0 && !info.color?.storage) || - (sampleCount > 1 && !info.multisample) - ); - }) - .combine('nonPowerOfTwo', [false, true]) - .combine('canaryOnCreation', [false, true]) - .filter(({ canaryOnCreation, format }) => { - // We can only initialize the texture if it's encodable or renderable. - const canInitialize = format in kTextureFormatInfo || kTextureFormatInfo[format].renderable; - - // Filter out cases where we want canary values but can't initialize. - return !canaryOnCreation || canInitialize; - }); - -type TextureZeroParams = ParamTypeOf; - -export type CheckContents = ( - t: TextureZeroInitTest, - params: TextureZeroParams, - texture: GPUTexture, - state: InitializedState, - subresourceRange: SubresourceRange -) => void; +import { unreachable } from '../../../../common/util/util.js'; +import { kTextureFormatInfo } from '../../../format_info.js'; import { checkContentsByBufferCopy, checkContentsByTextureCopy } from './check_texture/by_copy.js'; import { @@ -558,6 +17,15 @@ import { checkContentsByStencilTest, } from './check_texture/by_ds_test.js'; import { checkContentsBySampling } from './check_texture/by_sampling.js'; +import { + getRequiredTextureUsage, + ReadMethod, + CheckContents, + TextureZeroInitTest, + kTestParams, + UninitializeMethod, + InitializedState, +} from './check_texture/texture_zero_init_test.js'; const checkContentsImpl: { [k in ReadMethod]: CheckContents } = { Sample: checkContentsBySampling,