export const description = ` createBindGroupLayout validation tests. TODO: make sure tests are complete. `; import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js'; import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js'; import { makeTestGroup } from '../../../common/framework/test_group.js'; import { kShaderStages, kShaderStageCombinations, kStorageTextureAccessValues, kTextureSampleTypes, kTextureViewDimensions, allBindingEntries, bindingTypeInfo, bufferBindingTypeInfo, kBufferBindingTypes, BGLEntry, getBindingLimitForBindingType, } from '../../capability_info.js'; import { isTextureFormatUsableWithStorageAccessMode, kAllTextureFormats, } from '../../format_info.js'; function clone(descriptor: T): T { return JSON.parse(JSON.stringify(descriptor)); } function isValidBufferTypeForStages( device: GPUDevice, visibility: number, type: GPUBufferBindingType | undefined ) { if (type === 'read-only-storage' || type === 'storage') { if (visibility & GPUShaderStage.VERTEX) { if (!(device.limits.maxStorageBuffersInVertexStage! > 0)) { return false; } } if (visibility & GPUShaderStage.FRAGMENT) { if (!(device.limits.maxStorageBuffersInFragmentStage! > 0)) { return false; } } } return true; } function isValidStorageTextureForStages(device: GPUDevice, visibility: number) { if (visibility & GPUShaderStage.VERTEX) { if (!(device.limits.maxStorageTexturesInVertexStage! > 0)) { return false; } } if (visibility & GPUShaderStage.FRAGMENT) { if (!(device.limits.maxStorageTexturesInFragmentStage! > 0)) { return false; } } return true; } function isValidBGLEntryForStages(device: GPUDevice, visibility: number, entry: BGLEntry) { return entry.storageTexture ? isValidStorageTextureForStages(device, visibility) : entry.buffer ? isValidBufferTypeForStages(device, visibility, entry.buffer?.type) : true; } export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest); g.test('duplicate_bindings') .desc('Test that uniqueness of binding numbers across entries is enforced.') .paramsSubcasesOnly([ { bindings: [0, 1], _valid: true }, { bindings: [0, 0], _valid: false }, ]) .fn(t => { const { bindings, _valid } = t.params; const entries: Array = []; for (const binding of bindings) { entries.push({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' as const }, }); } t.expectValidationError(() => { t.device.createBindGroupLayout({ entries, }); }, !_valid); }); g.test('maximum_binding_limit') .desc( ` Test that a validation error is generated if the binding number exceeds the maximum binding limit. TODO: Need to also test with higher limits enabled on the device, once we have a way to do that. ` ) .paramsSubcasesOnly(u => u.combine('bindingVariant', [1, 4, 8, 256, 'default', 'default-minus-one'] as const) ) .fn(t => { const { bindingVariant } = t.params; const entries: Array = []; const binding = bindingVariant === 'default' ? t.device.limits.maxBindingsPerBindGroup : bindingVariant === 'default-minus-one' ? t.device.limits.maxBindingsPerBindGroup - 1 : bindingVariant; entries.push({ binding, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' as const }, }); const success = binding < t.device.limits.maxBindingsPerBindGroup; t.expectValidationError(() => { t.device.createBindGroupLayout({ entries, }); }, !success); }); g.test('visibility') .desc( ` Test that only the appropriate combinations of visibilities are allowed for each resource type. - Test each possible combination of shader stage visibilities. - Test each type of bind group resource.` ) .params(u => u .combine('visibility', kShaderStageCombinations) .beginSubcases() .combine('entry', allBindingEntries(false)) ) .fn(t => { const { visibility, entry } = t.params; const info = bindingTypeInfo(entry); const success = (visibility & ~info.validStages) === 0 && isValidBGLEntryForStages(t.device, visibility, entry); t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [{ binding: 0, visibility, ...entry }], }); }, !success); }); g.test('visibility,VERTEX_shader_stage_buffer_type') .desc( ` Test that a validation error is generated if the buffer type is 'storage' when the visibility of the entry includes VERTEX. ` ) .params(u => u // .combine('shaderStage', kShaderStageCombinations) .beginSubcases() .combine('type', kBufferBindingTypes) ) .fn(t => { const { shaderStage, type } = t.params; const success = !(type === 'storage' && shaderStage & GPUShaderStage.VERTEX) && isValidBufferTypeForStages(t.device, shaderStage, type); t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: shaderStage, buffer: { type }, }, ], }); }, !success); }); g.test('visibility,VERTEX_shader_stage_storage_texture_access') .desc( ` Test that a validation error is generated if the access value is 'write-only' when the visibility of the entry includes VERTEX. ` ) .params(u => u // .combine('shaderStage', kShaderStageCombinations) .beginSubcases() .combine('access', [undefined, ...kStorageTextureAccessValues]) ) .fn(t => { const { shaderStage, access } = t.params; const appliedAccess = access ?? 'write-only'; const success = !( // If visibility includes VERETX, storageTexture.access must be "read-only" (shaderStage & GPUShaderStage.VERTEX && appliedAccess !== 'read-only') ) && isValidStorageTextureForStages(t.device, shaderStage); t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: shaderStage, storageTexture: { access, format: 'r32uint' }, }, ], }); }, !success); }); g.test('multisampled_validation') .desc( ` Test that multisampling is only allowed if view dimensions is "2d" and the sampleType is not "float". ` ) .params(u => u // .combine('viewDimension', [undefined, ...kTextureViewDimensions]) .beginSubcases() .combine('sampleType', [undefined, ...kTextureSampleTypes]) ) .fn(t => { const { viewDimension, sampleType } = t.params; const success = (viewDimension === '2d' || viewDimension === undefined) && (sampleType ?? 'float') !== 'float'; t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, texture: { multisampled: true, viewDimension, sampleType }, }, ], }); }, !success); }); g.test('max_dynamic_buffers') .desc( ` Test that limits on the maximum number of dynamic buffers are enforced. - Test creation of a bind group layout using the maximum number of dynamic buffers works. - Test creation of a bind group layout using the maximum number of dynamic buffers + 1 fails. - TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.` ) .params(u => u .combine('type', kBufferBindingTypes) .beginSubcases() .combine('extraDynamicBuffers', [0, 1]) .combine('staticBuffers', [0, 1]) ) .fn(t => { const { type, extraDynamicBuffers, staticBuffers } = t.params; const info = bufferBindingTypeInfo({ type }); const limitName = info.perPipelineLimitClass.maxDynamicLimit; const bufferCount = limitName ? t.device.limits[limitName]! : 0; const dynamicBufferCount = bufferCount + extraDynamicBuffers; const perStageLimit = t.device.limits[info.perStageLimitClass.maxLimits.COMPUTE]!; const entries = []; for (let i = 0; i < dynamicBufferCount; i++) { entries.push({ binding: i, visibility: GPUShaderStage.COMPUTE, buffer: { type, hasDynamicOffset: true }, }); } for (let i = dynamicBufferCount; i < dynamicBufferCount + staticBuffers; i++) { entries.push({ binding: i, visibility: GPUShaderStage.COMPUTE, buffer: { type, hasDynamicOffset: false }, }); } const descriptor = { entries, }; t.expectValidationError( () => { t.device.createBindGroupLayout(descriptor); }, extraDynamicBuffers > 0 || entries.length > perStageLimit ); }); /** * One bind group layout will be filled with kPerStageBindingLimit[...] of the type |type|. * For each item in the array returned here, a case will be generated which tests a pipeline * layout with one extra bind group layout with one extra binding. That extra binding will have: * * - If extraTypeSame, any of the binding types which counts toward the same limit as |type|. * (i.e. 'storage-buffer' <-> 'readonly-storage-buffer'). * - Otherwise, an arbitrary other type. */ function* pickExtraBindingTypesForPerStage(entry: BGLEntry, extraTypeSame: boolean) { if (extraTypeSame) { const info = bindingTypeInfo(entry); for (const extra of allBindingEntries(false)) { const extraInfo = bindingTypeInfo(extra); if (info.perStageLimitClass.class === extraInfo.perStageLimitClass.class) { yield extra; } } } else { yield entry.sampler ? { texture: {} } : { sampler: {} }; } } const kMaxResourcesCases = kUnitCaseParamsBuilder .combine('maxedEntry', allBindingEntries(false)) .beginSubcases() .combine('maxedVisibility', kShaderStages) .filter(p => (bindingTypeInfo(p.maxedEntry).validStages & p.maxedVisibility) !== 0) .expand('extraEntry', p => [ ...pickExtraBindingTypesForPerStage(p.maxedEntry, true), ...pickExtraBindingTypesForPerStage(p.maxedEntry, false), ]) .combine('extraVisibility', kShaderStages) .filter(p => (bindingTypeInfo(p.extraEntry).validStages & p.extraVisibility) !== 0); // Should never fail unless limitInfo.maxBindingsPerBindGroup.default is exceeded, because the validation for // resources-of-type-per-stage is in pipeline layout creation. g.test('max_resources_per_stage,in_bind_group_layout') .desc( ` Test that the maximum number of bindings of a given type per-stage cannot be exceeded in a single bind group layout. - Test each binding type. - Test that creation of a bind group layout using the maximum number of bindings works. - Test that creation of a bind group layout using the maximum number of bindings + 1 fails. - TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.` ) .params(kMaxResourcesCases) .fn(t => { const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params; const maxedTypeInfo = bindingTypeInfo(maxedEntry); const maxedCount = getBindingLimitForBindingType(t.device, maxedVisibility, maxedEntry); const extraTypeInfo = bindingTypeInfo(extraEntry); t.skipIf(!isValidBGLEntryForStages(t.device, extraVisibility, extraEntry)); const maxResourceBindings: GPUBindGroupLayoutEntry[] = []; for (let i = 0; i < maxedCount; i++) { maxResourceBindings.push({ binding: i, visibility: maxedVisibility, ...maxedEntry, }); } const goodDescriptor = { entries: maxResourceBindings }; // Control t.device.createBindGroupLayout(goodDescriptor); // Add an entry counting towards the same limit. It should produce a validation error. const newDescriptor = clone(goodDescriptor); newDescriptor.entries.push({ binding: maxedCount, visibility: extraVisibility, ...extraEntry, }); const newBindingCountsTowardSamePerStageLimit = (maxedVisibility & extraVisibility) !== 0 && maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class; t.expectValidationError(() => { t.device.createBindGroupLayout(newDescriptor); }, newBindingCountsTowardSamePerStageLimit); }); // One pipeline layout can have a maximum number of each type of binding *per stage* (which is // different for each type). Test that the max works, then add one more binding of same-or-different // type and same-or-different visibility. g.test('max_resources_per_stage,in_pipeline_layout') .desc( ` Test that the maximum number of bindings of a given type per-stage cannot be exceeded across multiple bind group layouts when creating a pipeline layout. - Test each binding type. - Test that creation of a pipeline using the maximum number of bindings works. - Test that creation of a pipeline using the maximum number of bindings + 1 fails. ` ) .params(kMaxResourcesCases) .fn(t => { const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params; const maxedTypeInfo = bindingTypeInfo(maxedEntry); const maxedCount = getBindingLimitForBindingType(t.device, maxedVisibility, maxedEntry); const extraTypeInfo = bindingTypeInfo(extraEntry); t.skipIf(!isValidBGLEntryForStages(t.device, extraVisibility, extraEntry)); const maxResourceBindings: GPUBindGroupLayoutEntry[] = []; for (let i = 0; i < maxedCount; i++) { maxResourceBindings.push({ binding: i, visibility: maxedVisibility, ...maxedEntry, }); } const goodLayout = t.device.createBindGroupLayout({ entries: maxResourceBindings }); // Control t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout] }); const extraLayout = t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: extraVisibility, ...extraEntry, }, ], }); // Some binding types use the same limit, e.g. 'storage-buffer' and 'readonly-storage-buffer'. const newBindingCountsTowardSamePerStageLimit = (maxedVisibility & extraVisibility) !== 0 && maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class; t.expectValidationError(() => { t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout, extraLayout] }); }, newBindingCountsTowardSamePerStageLimit); }); g.test('storage_texture,layout_dimension') .desc( ` Test that viewDimension is not cube or cube-array if storageTextureLayout is not undefined. ` ) .params(u => u // .combine('viewDimension', [undefined, ...kTextureViewDimensions]) ) .fn(t => { const { viewDimension } = t.params; const success = viewDimension !== 'cube' && viewDimension !== `cube-array`; t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, storageTexture: { format: 'rgba8unorm', viewDimension }, }, ], }); }, !success); }); g.test('storage_texture,formats') .desc( ` Test that a validation error is generated if the format doesn't support the storage usage. A validation error is also generated if the format doesn't support the 'read-write' storage access when the storage access is 'read-write'. ` ) .params(u => u // .combine('format', kAllTextureFormats) // .combine('access', kStorageTextureAccessValues) ) .fn(t => { const { format, access } = t.params; t.skipIfTextureFormatNotSupported(format); const success = isTextureFormatUsableWithStorageAccessMode(t.device.features, format, access); t.expectValidationError(() => { t.device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, storageTexture: { format, access }, }, ], }); }, !success); });