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 { 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, vec3, and vec4 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 } @group(0) @binding(0) var 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 zero : u32; `; let functionScope = ''; const declaredStructTypes = new Map(); 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 inputs : array; @group(0) @binding(1) var outputs : array; var wg_mem : array; @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])); });