| export const description = `Test that variables in the shader are zero initialized`; |
| |
| import { makeTestGroup } from '../../../common/framework/test_group.js'; |
| import { iterRange, unreachable } from '../../../common/util/util.js'; |
| import { AllFeaturesMaxLimitsGPUTest } from '../../gpu_test.js'; |
| import { |
| ScalarType, |
| kVectorContainerTypes, |
| kVectorContainerTypeInfo, |
| kMatrixContainerTypes, |
| kMatrixContainerTypeInfo, |
| supportedScalarTypes, |
| supportsAtomics, |
| } from '../types.js'; |
| |
| type ShaderTypeInfo = |
| | { type: 'container'; containerType: 'array'; elementType: ShaderTypeInfo; length: number } |
| | { type: 'container'; containerType: 'struct'; members: readonly ShaderTypeInfo[] } |
| | { |
| type: 'container'; |
| containerType: keyof typeof kVectorContainerTypeInfo | keyof typeof kMatrixContainerTypeInfo; |
| scalarType: ScalarType; |
| } |
| | { type: 'scalar'; scalarType: ScalarType; isAtomic: boolean }; |
| |
| function prettyPrint(t: ShaderTypeInfo): string { |
| switch (t.type) { |
| case 'container': |
| switch (t.containerType) { |
| case 'array': |
| return `array<${prettyPrint(t.elementType)}, ${t.length}>`; |
| case 'struct': |
| return `struct { ${t.members.map(m => prettyPrint(m)).join(', ')} }`; |
| default: |
| return `${t.containerType}<${prettyPrint({ |
| type: 'scalar', |
| scalarType: t.scalarType, |
| isAtomic: false, |
| })}>`; |
| } |
| break; |
| case 'scalar': |
| if (t.isAtomic) { |
| return `atomic<${t.scalarType}>`; |
| } |
| return t.scalarType; |
| } |
| } |
| |
| export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); |
| g.test('compute,zero_init') |
| .desc( |
| `Test that uninitialized variables in workgroup, private, and function storage classes are initialized to zero.` |
| ) |
| .params(u => |
| u |
| // Only workgroup, function, and private variables can be declared without data bound to them. |
| // The implementation's shader translator should ensure these values are initialized. |
| .combine('addressSpace', ['workgroup', 'private', 'function'] as const) |
| .expand('workgroupSize', ({ addressSpace }) => { |
| switch (addressSpace) { |
| case 'workgroup': |
| return [ |
| [1, 1, 1], |
| [1, 32, 1], |
| [64, 1, 1], |
| [1, 1, 48], |
| [1, 47, 1], |
| [33, 1, 1], |
| [1, 1, 63], |
| [8, 8, 2], |
| [7, 7, 3], |
| ]; |
| case 'function': |
| case 'private': |
| return [[1, 1, 1]]; |
| } |
| }) |
| .beginSubcases() |
| // Fewer subcases: Only 0 and 2. If double-nested containers work, single-nested should too. |
| .combine('_containerDepth', [0, 2]) |
| .expandWithParams(function* (p) { |
| const kElementCounts = [ |
| [], // Not used. Depth 0 is always scalars. |
| [1, 3, 67], // Test something above the workgroup size. |
| [1, 3], |
| ] as const; |
| const kMemberCounts = [1, 3] as const; |
| |
| const memoizedTypes: ShaderTypeInfo[][] = []; |
| |
| function generateTypesMemo(depth: number): ShaderTypeInfo[] { |
| if (memoizedTypes[depth] === undefined) { |
| memoizedTypes[depth] = Array.from(generateTypes(depth)); |
| } |
| return memoizedTypes[depth]; |
| } |
| |
| function* generateTypes(depth: number): Generator<ShaderTypeInfo> { |
| if (depth === 0) { |
| for (const isAtomic of supportsAtomics({ |
| ...p, |
| access: 'read_write', |
| storageMode: undefined, |
| containerType: 'scalar', |
| }) |
| ? [true, false] |
| : [false]) { |
| for (const scalarType of supportedScalarTypes({ isAtomic, ...p })) { |
| // Fewer subcases: supportedScalarTypes was expanded to include f16 |
| // but that may take too much time. It would require more complex code. |
| if (scalarType === 'f16') continue; |
| |
| // Fewer subcases: For nested types, skip atomic u32 and non-atomic i32. |
| if (p._containerDepth > 0) { |
| if (scalarType === 'u32' && isAtomic) continue; |
| if (scalarType === 'i32' && !isAtomic) continue; |
| } |
| |
| yield { |
| type: 'scalar', |
| scalarType, |
| isAtomic, |
| }; |
| if (!isAtomic) { |
| // Vector types |
| for (const vectorType of kVectorContainerTypes) { |
| // Fewer subcases: For nested types, only include |
| // vec2<u32>, vec3<i32>, and vec4<f32> |
| if (p._containerDepth > 0) { |
| if ( |
| !( |
| (vectorType === 'vec2' && scalarType === 'u32') || |
| (vectorType === 'vec3' && scalarType === 'i32') || |
| (vectorType === 'vec4' && scalarType === 'f32') |
| ) |
| ) { |
| continue; |
| } |
| } |
| yield { |
| type: 'container', |
| containerType: vectorType, |
| scalarType, |
| }; |
| } |
| // Matrices can only be f32. |
| if (scalarType === 'f32') { |
| for (const matrixType of kMatrixContainerTypes) { |
| yield { |
| type: 'container', |
| containerType: matrixType, |
| scalarType, |
| }; |
| } |
| } |
| } |
| } |
| } |
| return; |
| } |
| |
| for (const containerType of ['array', 'struct']) { |
| const innerTypes = generateTypesMemo(depth - 1); |
| switch (containerType) { |
| case 'array': |
| for (const elementCount of kElementCounts[depth]) { |
| for (const innerType of innerTypes) { |
| yield { |
| type: 'container', |
| containerType, |
| elementType: innerType, |
| length: elementCount, |
| }; |
| } |
| } |
| break; |
| case 'struct': |
| for (const memberCount of kMemberCounts) { |
| const memberIndices = new Array(memberCount); |
| for (let m = 0; m < memberCount; ++m) { |
| memberIndices[m] = m; |
| } |
| |
| // Don't generate all possible combinations of inner struct members, |
| // because that's in the millions. Instead, just round-robin through |
| // to pick member types. Loop through the types, concatenated forward |
| // and backward, three times to produce a bounded but variable set of |
| // types. |
| const memberTypes = [...innerTypes, ...[...innerTypes].reverse()]; |
| const seenTypes = new Set(); |
| let typeIndex = 0; |
| while (typeIndex < memberTypes.length * 3) { |
| const prevTypeIndex = typeIndex; |
| const members: ShaderTypeInfo[] = []; |
| for (const m of memberIndices) { |
| members[m] = memberTypes[typeIndex % memberTypes.length]; |
| typeIndex += 1; |
| } |
| |
| const t: ShaderTypeInfo = { |
| type: 'container', |
| containerType, |
| members, |
| }; |
| const serializedT = prettyPrint(t); |
| if (seenTypes.has(serializedT)) { |
| // We produced an identical type. shuffle the member indices, |
| // "revert" typeIndex back to where it was before this loop, and |
| // shift it by one. This helps ensure we don't loop forever, and |
| // that we produce a different type on the next iteration. |
| memberIndices.push(memberIndices.shift()); |
| typeIndex = prevTypeIndex + 1; |
| continue; |
| } |
| seenTypes.add(serializedT); |
| yield t; |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| for (const t of generateTypesMemo(p._containerDepth)) { |
| yield { |
| shaderTypeParam: prettyPrint(t), |
| _type: t, |
| }; |
| } |
| }) |
| ) |
| .batch(15) |
| .fn(async t => { |
| const { workgroupSize } = t.params; |
| const { maxComputeInvocationsPerWorkgroup } = t.device.limits; |
| const numWorkgroupInvocations = workgroupSize.reduce((a, b) => a * b); |
| t.skipIf( |
| numWorkgroupInvocations > maxComputeInvocationsPerWorkgroup, |
| `workgroupSize: ${workgroupSize} > maxComputeInvocationsPerWorkgroup: ${maxComputeInvocationsPerWorkgroup}` |
| ); |
| |
| let moduleScope = ` |
| struct Output { |
| failed : atomic<u32> |
| } |
| @group(0) @binding(0) var<storage, read_write> output : Output; |
| |
| // This uniform value that's a zero is used to prevent the shader compilers from trying to |
| // unroll the massive loops generated by these tests. |
| @group(0) @binding(1) var<uniform> zero : u32; |
| `; |
| let functionScope = ''; |
| |
| const declaredStructTypes = new Map<ShaderTypeInfo, string>(); |
| const typeDecl = (function ensureType( |
| typeName: string, |
| type: ShaderTypeInfo, |
| depth: number = 0 |
| ): string { |
| switch (type.type) { |
| case 'container': |
| switch (type.containerType) { |
| case 'array': |
| return `array<${ensureType( |
| `${typeName}_ArrayElement`, |
| type.elementType, |
| depth + 1 |
| )}, ${type.length}>`; |
| case 'struct': { |
| if (declaredStructTypes.has(type)) { |
| return declaredStructTypes.get(type)!; |
| } |
| |
| const members = type.members |
| .map((member, i) => { |
| return `\n member${i} : ${ensureType( |
| `${typeName}_Member${i}`, |
| member, |
| depth + 1 |
| )},`; |
| }) |
| .join(''); |
| declaredStructTypes.set(type, typeName); |
| moduleScope += `\nstruct ${typeName} {`; |
| moduleScope += members; |
| moduleScope += '\n};'; |
| |
| return typeName; |
| } |
| default: |
| return `${type.containerType}<${ensureType( |
| typeName, |
| { |
| type: 'scalar', |
| scalarType: type.scalarType, |
| isAtomic: false, |
| }, |
| depth + 1 |
| )}>`; |
| } |
| break; |
| case 'scalar': |
| return type.isAtomic ? `atomic<${type.scalarType}>` : type.scalarType; |
| } |
| })('TestType', t.params._type); |
| |
| switch (t.params.addressSpace) { |
| case 'workgroup': |
| case 'private': |
| moduleScope += `\nvar<${t.params.addressSpace}> testVar: ${typeDecl};`; |
| break; |
| case 'function': |
| functionScope += `\nvar testVar: ${typeDecl};`; |
| break; |
| } |
| |
| const checkZeroCode = (function checkZero( |
| value: string, |
| type: ShaderTypeInfo, |
| depth: number = 0 |
| ): string { |
| switch (type.type) { |
| case 'container': |
| switch (type.containerType) { |
| case 'array': |
| return `\nfor (var i${depth} = 0u; i${depth} < ${ |
| type.length |
| }u + zero; i${depth} = i${depth} + 1u) { |
| ${checkZero(`${value}[i${depth}]`, type.elementType, depth + 1)} |
| }`; |
| case 'struct': |
| return type.members |
| .map((member, i) => { |
| return checkZero(`${value}.member${i}`, member, depth + 1); |
| }) |
| .join('\n'); |
| default: |
| if (type.containerType.indexOf('vec') !== -1) { |
| const length = type.containerType[3]; |
| return `\nfor (var i${depth} = 0u; i${depth} < ${length}u + zero; i${depth} = i${depth} + 1u) { |
| ${checkZero( |
| `${value}[i${depth}]`, |
| { |
| type: 'scalar', |
| scalarType: type.scalarType, |
| isAtomic: false, |
| }, |
| depth + 1 |
| )} |
| }`; |
| } else if (type.containerType.indexOf('mat') !== -1) { |
| const cols = type.containerType[3]; |
| const rows = type.containerType[5]; |
| return `\nfor (var c${depth} = 0u; c${depth} < ${cols}u + zero; c${depth} = c${depth} + 1u) { |
| for (var r${depth} = 0u; r${depth} < ${rows}u; r${depth} = r${depth} + 1u) { |
| ${checkZero( |
| `${value}[c${depth}][r${depth}]`, |
| { |
| type: 'scalar', |
| scalarType: type.scalarType, |
| isAtomic: false, |
| }, |
| depth + 1 |
| )} |
| } |
| }`; |
| } else { |
| unreachable(); |
| } |
| } |
| break; |
| case 'scalar': { |
| let expected; |
| switch (type.scalarType) { |
| case 'bool': |
| expected = 'false'; |
| break; |
| case 'f32': |
| expected = '0.0'; |
| break; |
| case 'i32': |
| expected = '0'; |
| break; |
| case 'u32': |
| expected = '0u'; |
| break; |
| } |
| if (type.isAtomic) { |
| value = `atomicLoad(&${value})`; |
| } |
| |
| // Note: this could have an early return, but we omit it because it makes |
| // the tests fail cause with DXGI_ERROR_DEVICE_HUNG on Windows. |
| return `\nif (${value} != ${expected}) { atomicStore(&output.failed, 1u); }`; |
| } |
| } |
| })('testVar', t.params._type); |
| |
| const wgsl = ` |
| ${moduleScope} |
| @compute @workgroup_size(${t.params.workgroupSize}) |
| fn main() { |
| ${functionScope} |
| ${checkZeroCode} |
| _ = zero; |
| } |
| `; |
| |
| if (t.params.addressSpace === 'workgroup') { |
| // Populate the maximum amount of workgroup memory with known values to |
| // ensure initialization overrides in another shader. |
| const wg_memory_limits = t.device.limits.maxComputeWorkgroupStorageSize; |
| const wg_x_dim = t.device.limits.maxComputeWorkgroupSizeX; |
| |
| const wgsl = ` |
| @group(0) @binding(0) var<storage, read> inputs : array<u32>; |
| @group(0) @binding(1) var<storage, read_write> outputs : array<u32>; |
| var<workgroup> wg_mem : array<u32, ${wg_memory_limits} / 4>; |
| |
| @compute @workgroup_size(${wg_x_dim}) |
| fn fill(@builtin(local_invocation_index) lid : u32) { |
| const num_u32_per_invocation = ${wg_memory_limits} / (4 * ${wg_x_dim}); |
| |
| for (var i = 0u; i < num_u32_per_invocation; i++) { |
| let idx = num_u32_per_invocation * lid + i; |
| wg_mem[idx] = inputs[idx]; |
| } |
| workgroupBarrier(); |
| // Copy out to avoid wg_mem being elided. |
| for (var i = 0u; i < num_u32_per_invocation; i++) { |
| let idx = num_u32_per_invocation * lid + i; |
| outputs[idx] = wg_mem[idx]; |
| } |
| } |
| `; |
| |
| const fillLayout = t.device.createBindGroupLayout({ |
| entries: [ |
| { |
| binding: 0, |
| visibility: GPUShaderStage.COMPUTE, |
| buffer: { type: 'read-only-storage' }, |
| }, |
| { |
| binding: 1, |
| visibility: GPUShaderStage.COMPUTE, |
| buffer: { type: 'storage' }, |
| }, |
| ], |
| }); |
| |
| const fillPipeline = await t.device.createComputePipelineAsync({ |
| layout: t.device.createPipelineLayout({ bindGroupLayouts: [fillLayout] }), |
| label: 'Workgroup Fill Pipeline', |
| compute: { |
| module: t.device.createShaderModule({ |
| code: wgsl, |
| }), |
| entryPoint: 'fill', |
| }, |
| }); |
| |
| const inputBuffer = t.makeBufferWithContents( |
| new Uint32Array([...iterRange(wg_memory_limits / 4, _i => 0xdeadbeef)]), |
| GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST |
| ); |
| const outputBuffer = t.createBufferTracked({ |
| size: wg_memory_limits, |
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| }); |
| |
| const bg = t.device.createBindGroup({ |
| layout: fillPipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource: { |
| buffer: inputBuffer, |
| }, |
| }, |
| { |
| binding: 1, |
| resource: { |
| buffer: outputBuffer, |
| }, |
| }, |
| ], |
| }); |
| |
| const e = t.device.createCommandEncoder(); |
| const p = e.beginComputePass(); |
| p.setPipeline(fillPipeline); |
| p.setBindGroup(0, bg); |
| p.dispatchWorkgroups(1); |
| p.end(); |
| t.queue.submit([e.finish()]); |
| } |
| |
| const pipeline = await t.device.createComputePipelineAsync({ |
| layout: 'auto', |
| compute: { |
| module: t.device.createShaderModule({ |
| code: wgsl, |
| }), |
| entryPoint: 'main', |
| }, |
| }); |
| |
| const resultBuffer = t.createBufferTracked({ |
| size: 4, |
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, |
| }); |
| |
| const zeroBuffer = t.createBufferTracked({ |
| size: 4, |
| usage: GPUBufferUsage.UNIFORM, |
| }); |
| |
| const bindGroup = t.device.createBindGroup({ |
| layout: pipeline.getBindGroupLayout(0), |
| entries: [ |
| { |
| binding: 0, |
| resource: { |
| buffer: resultBuffer, |
| }, |
| }, |
| { |
| binding: 1, |
| resource: { |
| buffer: zeroBuffer, |
| }, |
| }, |
| ], |
| }); |
| |
| const encoder = t.device.createCommandEncoder(); |
| const pass = encoder.beginComputePass(); |
| pass.setPipeline(pipeline); |
| pass.setBindGroup(0, bindGroup); |
| pass.dispatchWorkgroups(1); |
| pass.end(); |
| t.queue.submit([encoder.finish()]); |
| t.expectGPUBufferValuesEqual(resultBuffer, new Uint32Array([0])); |
| }); |