| 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<T extends GPUBindGroupLayoutDescriptor>(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<GPUBindGroupLayoutEntry> = []; |
| |
| 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<GPUBindGroupLayoutEntry> = []; |
| |
| 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); |
| }); |