diff --git a/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts b/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts index 14f1642cea9f..c2f002509e34 100644 --- a/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts +++ b/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts @@ -45,6 +45,20 @@ export function getPipelineTypeForBindingCombination(bindingCombination: Binding } } +export function getStageVisibilityForBinidngCombination(bindingCombination: BindingCombination) { + switch (bindingCombination) { + case 'vertex': + return GPUShaderStage.VERTEX; + case 'fragment': + return GPUShaderStage.FRAGMENT; + case 'vertexAndFragmentWithPossibleVertexStageOverflow': + case 'vertexAndFragmentWithPossibleFragmentStageOverflow': + return GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX; + case 'compute': + return GPUShaderStage.COMPUTE; + } +} + function getBindGroupIndex(bindGroupTest: BindGroupTest, numBindGroups: number, i: number) { switch (bindGroupTest) { case 'sameGroup': @@ -1100,6 +1114,35 @@ export class LimitTestsImpl extends GPUTestBase { const module = device.createShaderModule({ code }); return { module, code }; } + + skipIfNotEnoughStorageBuffersInStage(visibility: GPUShaderStageFlags, numRequired: number) { + const { device } = this; + this.skipIf( + this.isCompatibility && + // If we're using the fragment stage + (visibility & GPUShaderStage.FRAGMENT) !== 0 && + // If perShaderStage and inFragment stage are equal we want to + // allow the test to run as otherwise we can't test overMaximum and overLimit + device.limits.maxStorageBuffersPerShaderStage > + device.limits.maxStorageBuffersInFragmentStage! && + // They aren't equal so if there aren't enough supported in the fragment then skip + !(device.limits.maxStorageBuffersInFragmentStage! >= numRequired), + `maxStorageBuffersInFragmentShader = ${device.limits.maxStorageBuffersInFragmentStage} which is less than ${numRequired}` + ); + + this.skipIf( + this.isCompatibility && + // If we're using the vertex stage + (visibility & GPUShaderStage.VERTEX) !== 0 && + // If perShaderStage and inVertex stage are equal we want to + // allow the test to run as otherwise we can't test overMaximum and overLimit + device.limits.maxStorageBuffersPerShaderStage > + device.limits.maxStorageBuffersInVertexStage! && + // They aren't equal so if there aren't enough supported in the vertex then skip + !(device.limits.maxStorageBuffersInVertexStage! >= numRequired), + `maxStorageBuffersInVertexShader = ${device.limits.maxStorageBuffersInVertexStage} which is less than ${numRequired}` + ); + } } /** diff --git a/src/webgpu/api/validation/capability_checks/limits/maxDynamicStorageBuffersPerPipelineLayout.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxDynamicStorageBuffersPerPipelineLayout.spec.ts index efd6c871964a..05345da30ab2 100644 --- a/src/webgpu/api/validation/capability_checks/limits/maxDynamicStorageBuffersPerPipelineLayout.spec.ts +++ b/src/webgpu/api/validation/capability_checks/limits/maxDynamicStorageBuffersPerPipelineLayout.spec.ts @@ -1,7 +1,16 @@ -import { range } from '../../../../../common/util/util.js'; +import { assert, range } from '../../../../../common/util/util.js'; +import { kShaderStageCombinationsWithStage } from '../../../../capability_info.js'; import { GPUConst } from '../../../../constants.js'; -import { kMaximumLimitBaseParams, makeLimitTestGroup } from './limit_utils.js'; +import { kMaximumLimitBaseParams, LimitsRequest, makeLimitTestGroup } from './limit_utils.js'; + +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', + maxStorageBuffersPerShaderStage: 'adapterLimit', + maxStorageBuffersInFragmentStage: 'adapterLimit', + maxStorageBuffersInVertexStage: 'adapterLimit', +}; const limit = 'maxDynamicStorageBuffersPerPipelineLayout'; export const { g, description } = makeLimitTestGroup(limit); @@ -9,18 +18,21 @@ export const { g, description } = makeLimitTestGroup(limit); g.test('createBindGroupLayout,at_over') .desc(`Test using createBindGroupLayout at and over ${limit} limit`) .params( - kMaximumLimitBaseParams.combine('visibility', [ - GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.COMPUTE | GPUConst.ShaderStage.FRAGMENT, - ]) + kMaximumLimitBaseParams + .combine('visibility', kShaderStageCombinationsWithStage) + .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) + .filter( + ({ visibility, type }) => + (visibility & GPUConst.ShaderStage.VERTEX) === 0 || type !== 'storage' + ) ) .fn(async t => { - const { limitTest, testValueName, visibility } = t.params; + const { limitTest, testValueName, visibility, type } = t.params; await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, async ({ device, testValue, shouldError }) => { + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); shouldError ||= testValue > t.device.limits.maxStorageBuffersPerShaderStage; await t.expectValidationError(() => { device.createBindGroupLayout({ @@ -28,12 +40,69 @@ g.test('createBindGroupLayout,at_over') binding: i, visibility, buffer: { - type: 'storage', + type, hasDynamicOffset: true, }, })), }); }, shouldError); - } + }, + kExtraLimits + ); + }); + +g.test('createPipelineLayout,at_over') + .desc(`Test using at and over ${limit} limit in createPipelineLayout`) + .params( + kMaximumLimitBaseParams + .combine('visibility', kShaderStageCombinationsWithStage) + .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) + .filter( + ({ visibility, type }) => + (visibility & GPUConst.ShaderStage.VERTEX) === 0 || type !== 'storage' + ) + ) + .fn(async t => { + const { limitTest, testValueName, visibility, type } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + + const maxBindingsPerBindGroup = Math.min( + t.device.limits.maxBindingsPerBindGroup, + actualLimit + ); + + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); + + // Not sure what to do in this case but best we get notified if it happens. + assert(kNumGroups <= t.device.limits.maxBindGroups); + + const bindGroupLayouts = range(kNumGroups, i => { + const numInGroup = Math.min( + testValue - i * maxBindingsPerBindGroup, + maxBindingsPerBindGroup + ); + return device.createBindGroupLayout({ + entries: range(numInGroup, i => ({ + binding: i, + visibility, + buffer: { + type, + hasDynamicOffset: true, + }, + })), + }); + }); + + await t.expectValidationError( + () => device.createPipelineLayout({ bindGroupLayouts }), + shouldError + ); + }, + kExtraLimits ); }); diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts index 60be6d30c1d2..3891d323d1f6 100644 --- a/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersPerShaderStage.spec.ts @@ -16,6 +16,7 @@ import { getPipelineTypeForBindingCombination, getPerStageWGSLForBindingCombination, LimitsRequest, + getStageVisibilityForBinidngCombination, } from './limit_utils.js'; const kExtraLimits: LimitsRequest = { @@ -35,7 +36,7 @@ function createBindGroupLayout( order: ReorderOrder, numBindings: number ) { - return device.createBindGroupLayout({ + const bindGroupLayoutDescription = { entries: reorder( order, range(numBindings, i => ({ @@ -44,7 +45,8 @@ function createBindGroupLayout( buffer: { type }, })) ), - }); + }; + return device.createBindGroupLayout(bindGroupLayoutDescription); } g.test('createBindGroupLayout,at_over') @@ -78,6 +80,8 @@ g.test('createBindGroupLayout,at_over') `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` ); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + await t.expectValidationError(() => { createBindGroupLayout(device, visibility, type, order, testValue); }, shouldError); @@ -112,10 +116,13 @@ g.test('createPipelineLayout,at_over') limitTest, testValueName, async ({ device, testValue, shouldError, actualLimit }) => { + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + const maxBindingsPerBindGroup = Math.min( t.device.limits.maxBindingsPerBindGroup, actualLimit ); + const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup); // Not sure what to do in this case but best we get notified if it happens. @@ -151,6 +158,7 @@ g.test('createPipeline,at_over') kMaximumLimitBaseParams .combine('async', [false, true] as const) .combine('bindingCombination', kBindingCombinations) + .beginSubcases() .combine('order', kReorderOrderKeys) .combine('bindGroupTest', kBindGroupTests) ) @@ -167,23 +175,8 @@ g.test('createPipeline,at_over') `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` ); - if (t.isCompatibility) { - t.skipIf( - (bindingCombination === 'fragment' || - bindingCombination === 'vertexAndFragmentWithPossibleVertexStageOverflow' || - bindingCombination === 'vertexAndFragmentWithPossibleFragmentStageOverflow') && - testValue > device.limits.maxStorageBuffersInFragmentStage!, - `can not test ${testValue} bindings as it is more than maxStorageBuffersInFragmentStage(${device.limits.maxStorageBuffersInFragmentStage})` - ); - - t.skipIf( - (bindingCombination === 'vertex' || - bindingCombination === 'vertexAndFragmentWithPossibleVertexStageOverflow' || - bindingCombination === 'vertexAndFragmentWithPossibleFragmentStageOverflow') && - testValue > device.limits.maxStorageBuffersInVertexStage!, - `can not test ${testValue} bindings as it is more than maxStorageBuffersInVertexStage(${device.limits.maxStorageBuffersInVertexStage})` - ); - } + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); const code = getPerStageWGSLForBindingCombination( bindingCombination, diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts index 8af61f51fc7a..e32afb6e8c3e 100644 --- a/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesPerShaderStage.spec.ts @@ -5,6 +5,11 @@ import { kReorderOrderKeys, assert, } from '../../../../../common/util/util.js'; +import { + kShaderStageCombinationsWithStage, + kStorageTextureAccessValues, + storageTextureBindingTypeInfo, +} from '../../../../capability_info.js'; import { GPUConst } from '../../../../constants.js'; import { @@ -13,13 +18,16 @@ import { kBindGroupTests, getPerStageWGSLForBindingCombinationStorageTextures, getPipelineTypeForBindingCombination, - BindingCombination, LimitsRequest, + LimitTestsImpl, + kBindingCombinations, + getStageVisibilityForBinidngCombination, } from './limit_utils.js'; const kExtraLimits: LimitsRequest = { maxBindingsPerBindGroup: 'adapterLimit', maxBindGroups: 'adapterLimit', + maxStorageTexturesInFragmentStage: 'adapterLimit', }; const limit = 'maxStorageTexturesPerShaderStage'; @@ -28,6 +36,7 @@ export const { g, description } = makeLimitTestGroup(limit); function createBindGroupLayout( device: GPUDevice, visibility: number, + access: GPUStorageTextureAccess, order: ReorderOrder, numBindings: number ) { @@ -37,12 +46,52 @@ function createBindGroupLayout( range(numBindings, i => ({ binding: i, visibility, - storageTexture: { format: 'rgba8unorm' }, + storageTexture: { format: 'r32float', access }, })) ), }); } +function skipIfNotEnoughStorageTexturesInStage( + t: LimitTestsImpl, + visibility: GPUShaderStageFlags, + testValue: number +) { + t.skipIf( + t.isCompatibility && + // If we're using the fragment stage + (visibility & GPUConst.ShaderStage.FRAGMENT) !== 0 && + // If perShaderStage and inFragment stage are equal we want to + // allow the test to run as otherwise we can't test overMaximum and overLimit + t.device.limits.maxStorageTexturesPerShaderStage > + t.device.limits.maxStorageTexturesInFragmentStage! && + // They aren't equal so if there aren't enough supported in the fragment then skip + !(t.device.limits.maxStorageTexturesInFragmentStage! >= testValue), + `maxStorageTexturesInFragmentShader = ${t.device.limits.maxStorageTexturesInFragmentStage} which is less than ${testValue}` + ); + + t.skipIf( + t.isCompatibility && + // If we're using the vertex stage + (visibility & GPUConst.ShaderStage.VERTEX) !== 0 && + // If perShaderStage and inVertex stage are equal we want to + // allow the test to run as otherwise we can't test overMaximum and overLimit + t.device.limits.maxStorageTexturesPerShaderStage > + t.device.limits.maxStorageTexturesInVertexStage! && + // They aren't equal so if there aren't enough supported in the vertex then skip + !(t.device.limits.maxStorageTexturesInVertexStage! >= testValue), + `maxStorageTexturesInVertexShader = ${t.device.limits.maxStorageTexturesInVertexStage} which is less than ${testValue}` + ); +} + +function skipIfAccessNotSupported(t: LimitTestsImpl, access: GPUStorageTextureAccess) { + t.skipIf( + (access === 'read-only' || access === 'read-write') && + !navigator.gpu.wgslLanguageFeatures.has('readonly_and_readwrite_storage_textures'), + `access = ${access} but navigator.gpu.wsglLanguageFeatures does not contain 'readonly_and_readwrite_storage_textures'` + ); +} + g.test('createBindGroupLayout,at_over') .desc( ` @@ -54,15 +103,15 @@ g.test('createBindGroupLayout,at_over') ) .params( kMaximumLimitBaseParams - .combine('visibility', [ - GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - ]) + .combine('visibility', kShaderStageCombinationsWithStage) + .combine('access', kStorageTextureAccessValues) .combine('order', kReorderOrderKeys) ) .fn(async t => { - const { limitTest, testValueName, visibility, order } = t.params; + const { limitTest, testValueName, visibility, access, order } = t.params; + + skipIfAccessNotSupported(t, access); + await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, @@ -71,9 +120,9 @@ g.test('createBindGroupLayout,at_over') t.adapter.limits.maxBindingsPerBindGroup < testValue, `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` ); - + skipIfNotEnoughStorageTexturesInStage(t, visibility, testValue); await t.expectValidationError( - () => createBindGroupLayout(device, visibility, order, testValue), + () => createBindGroupLayout(device, visibility, access, order, testValue), shouldError ); }, @@ -92,19 +141,21 @@ g.test('createPipelineLayout,at_over') ) .params( kMaximumLimitBaseParams - .combine('visibility', [ - GPUConst.ShaderStage.FRAGMENT, - GPUConst.ShaderStage.COMPUTE, - GPUConst.ShaderStage.FRAGMENT | GPUConst.ShaderStage.COMPUTE, - ]) + .combine('visibility', kShaderStageCombinationsWithStage) + .combine('access', kStorageTextureAccessValues) .combine('order', kReorderOrderKeys) ) .fn(async t => { - const { limitTest, testValueName, visibility, order } = t.params; + const { limitTest, testValueName, visibility, access, order } = t.params; + + skipIfAccessNotSupported(t, access); + await t.testDeviceWithRequestedMaximumLimits( limitTest, testValueName, async ({ device, testValue, shouldError, actualLimit }) => { + skipIfNotEnoughStorageTexturesInStage(t, visibility, testValue); + const maxBindingsPerBindGroup = Math.min( t.device.limits.maxBindingsPerBindGroup, actualLimit @@ -119,7 +170,7 @@ g.test('createPipelineLayout,at_over') testValue - i * maxBindingsPerBindGroup, maxBindingsPerBindGroup ); - return createBindGroupLayout(device, visibility, order, numInGroup); + return createBindGroupLayout(device, visibility, access, order, numInGroup); }); await t.expectValidationError( @@ -143,12 +194,18 @@ g.test('createPipeline,at_over') .params( kMaximumLimitBaseParams .combine('async', [false, true] as const) - .combine('bindingCombination', ['fragment', 'compute'] as BindingCombination[]) + .combine('bindingCombination', kBindingCombinations) + .combine('access', kStorageTextureAccessValues) + .beginSubcases() .combine('order', kReorderOrderKeys) .combine('bindGroupTest', kBindGroupTests) ) .fn(async t => { - const { limitTest, testValueName, async, bindingCombination, order, bindGroupTest } = t.params; + const { limitTest, testValueName, async, bindingCombination, access, order, bindGroupTest } = + t.params; + + skipIfAccessNotSupported(t, access); + const pipelineType = getPipelineTypeForBindingCombination(bindingCombination); await t.testDeviceWithRequestedMaximumLimits( @@ -160,16 +217,20 @@ g.test('createPipeline,at_over') `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` ); - if (bindingCombination === 'fragment') { - return; - } + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + skipIfNotEnoughStorageTexturesInStage(t, visibility, testValue); + + const { wgslAccess } = storageTextureBindingTypeInfo({ access }); const code = getPerStageWGSLForBindingCombinationStorageTextures( bindingCombination, order, bindGroupTest, - (i, j) => `var u${j}_${i}: texture_storage_2d`, - (i, j) => `textureStore(u${j}_${i}, vec2u(0), vec4f(1));`, + (i, j) => `var u${j}_${i}: texture_storage_2d`, + (i, j) => + access === 'write-only' + ? `textureStore(u${j}_${i}, vec2u(0), vec4f(0));` + : `_ = textureLoad(u${j}_${i}, vec2u(0));`, device.limits.maxBindGroups, testValue ); diff --git a/src/webgpu/capability_info.ts b/src/webgpu/capability_info.ts index 7fcab69d1b97..a87ccc1fd7f6 100644 --- a/src/webgpu/capability_info.ts +++ b/src/webgpu/capability_info.ts @@ -503,24 +503,27 @@ export const kTextureSampleTypes = [ ] as const; assertTypeTrue>(); -/** Binding type info (including class limits) for the specified GPUStorageTextureBindingLayout. */ -export function storageTextureBindingTypeInfo(d: GPUStorageTextureBindingLayout) { +/** Binding type info (including class limits) for the specified GPUStorageTextureAccess. */ +export function storageTextureBindingTypeInfo(d: { access?: GPUStorageTextureAccess | undefined }) { switch (d.access) { case undefined: case 'write-only': return { + wgslAccess: 'write', usage: GPUConst.TextureUsage.STORAGE_BINDING, ...kBindingKind.writeonlyStorageTex, ...kValidStagesStorageWrite, }; case 'read-only': return { + wgslAccess: 'read', usage: GPUConst.TextureUsage.STORAGE_BINDING, ...kBindingKind.readonlyStorageTex, ...kValidStagesAll, }; case 'read-write': return { + wgslAccess: 'read_write', usage: GPUConst.TextureUsage.STORAGE_BINDING, ...kBindingKind.readwriteStorageTex, ...kValidStagesStorageWrite,