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 3ef275f51b1..9652f1fb971 100644 --- a/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts +++ b/src/webgpu/api/validation/capability_checks/limits/limit_utils.ts @@ -386,10 +386,19 @@ export function addMaximumLimitUpToDependentLimit( limits[limit] = value; } +type LimitCheckParams = { + limit: GPUSupportedLimit; + actualLimit: number; + defaultLimit: number; +}; + +type LimitCheckFn = (t: LimitTestsImpl, device: GPUDevice, params: LimitCheckParams) => boolean; + export class LimitTestsImpl extends GPUTestBase { _adapter: GPUAdapter | null = null; _device: GPUDevice | undefined = undefined; limit: GPUSupportedLimit = '' as GPUSupportedLimit; + limitTestParams: LimitTestParams = {}; defaultLimit = 0; adapterLimit = 0; @@ -398,6 +407,10 @@ export class LimitTestsImpl extends GPUTestBase { const gpu = getGPU(this.rec); this._adapter = await gpu.requestAdapter(); const limit = this.limit; + this.skipIf( + this._adapter?.limits[limit] === undefined && !!this.limitTestParams.limitOptional, + `${limit} is missing but optional for now` + ); this.defaultLimit = getDefaultLimitForAdapter(this.adapter, limit); this.adapterLimit = this.adapter.limits[limit] as number; assert(!Number.isNaN(this.defaultLimit)); @@ -504,16 +517,21 @@ export class LimitTestsImpl extends GPUTestBase { ); } } else { - if (requestedLimit <= defaultLimit) { - this.expect( - actualLimit === defaultLimit, - `expected actual actualLimit: ${actualLimit} to equal defaultLimit: ${defaultLimit}` - ); - } else { - this.expect( - actualLimit === requestedLimit, - `expected actual actualLimit: ${actualLimit} to equal requestedLimit: ${requestedLimit}` - ); + const checked = this.limitTestParams.limitCheckFn + ? this.limitTestParams.limitCheckFn(this, device!, { limit, actualLimit, defaultLimit }) + : false; + if (!checked) { + if (requestedLimit <= defaultLimit) { + this.expect( + actualLimit === defaultLimit, + `expected actual actualLimit: ${actualLimit} to equal defaultLimit: ${defaultLimit}` + ); + } else { + this.expect( + actualLimit === requestedLimit, + `expected actual actualLimit: ${actualLimit} to equal requestedLimit: ${requestedLimit}` + ); + } } } } @@ -534,6 +552,10 @@ export class LimitTestsImpl extends GPUTestBase { const { defaultLimit, adapterLimit: maximumLimit } = this; const requestedLimit = getLimitValue(defaultLimit, maximumLimit, limitValueTest); + this.skipIf( + requestedLimit < 0 && limitValueTest === 'underDefault', + `requestedLimit(${requestedLimit}) for ${this.limit} is < 0` + ); return this._getDeviceWithSpecificLimit(requestedLimit, extraLimits, features); } @@ -1209,12 +1231,21 @@ export class LimitTestsImpl extends GPUTestBase { } } +type LimitTestParams = { + limitCheckFn?: LimitCheckFn; + limitOptional?: boolean; +}; + /** * Makes a new LimitTest class so that the tests have access to `limit` */ -function makeLimitTestFixture(limit: GPUSupportedLimit): typeof LimitTestsImpl { +function makeLimitTestFixture( + limit: GPUSupportedLimit, + params?: LimitTestParams +): typeof LimitTestsImpl { class LimitTests extends LimitTestsImpl { override limit = limit; + override limitTestParams = params ?? {}; } return LimitTests; @@ -1225,8 +1256,79 @@ function makeLimitTestFixture(limit: GPUSupportedLimit): typeof LimitTestsImpl { * writing these tests where I'd copy a test, need to rename a limit in 3-4 places, * forget one place, and then spend 20-30 minutes wondering why the test was failing. */ -export function makeLimitTestGroup(limit: GPUSupportedLimit) { +export function makeLimitTestGroup(limit: GPUSupportedLimit, params?: LimitTestParams) { const description = `API Validation Tests for ${limit}.`; - const g = makeTestGroup(makeLimitTestFixture(limit)); + const g = makeTestGroup(makeLimitTestFixture(limit, params)); return { g, description, limit }; } + +/** + * Test that limit must be less than dependentLimitName when requesting a device. + */ +export function testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit( + g: ReturnType['g'], + limit: + | 'maxStorageBuffersInFragmentStage' + | 'maxStorageBuffersInVertexStage' + | 'maxStorageTexturesInFragmentStage' + | 'maxStorageTexturesInVertexStage', + dependentLimitName: 'maxStorageBuffersPerShaderStage' | 'maxStorageTexturesPerShaderStage' +) { + g.test(`validate,${dependentLimitName}`) + .desc( + `Test that adapter.limit.${limit} and requiredLimits.${limit} must be <= ${dependentLimitName}` + ) + .params(u => u.combine('useMax', [true, false] as const)) // true case should not reject. + .fn(async t => { + const { useMax } = t.params; + const { adapterLimit: maximumLimit, adapter } = t; + + const dependentLimit = adapter.limits[dependentLimitName]!; + t.expect( + maximumLimit <= dependentLimit, + `maximumLimit(${maximumLimit}) is <= adapter.limits.${dependentLimitName}(${dependentLimit})` + ); + + const dependentEffectiveLimits = useMax + ? dependentLimit + : t.getDefaultLimit(dependentLimitName); + const shouldReject = maximumLimit > dependentEffectiveLimits; + t.debug( + `${limit}(${maximumLimit}) > ${dependentLimitName}(${dependentEffectiveLimits}) shouldReject: ${shouldReject}` + ); + const device = await t.requestDeviceWithLimits( + adapter, + { + [limit]: maximumLimit, + ...(useMax && { + [dependentLimitName]: dependentLimit, + }), + }, + shouldReject + ); + device?.destroy(); + }); + + g.test(`auto_upgrade,${dependentLimitName}`) + .desc( + `Test that adapter.limit.${limit} is automatically upgraded to ${dependentLimitName} except in compat.` + ) + .fn(async t => { + const { adapter, defaultLimit } = t; + const dependentAdapterLimit = adapter.limits[dependentLimitName]; + const shouldReject = false; + const device = await t.requestDeviceWithLimits( + adapter, + { + [dependentLimitName]: dependentAdapterLimit, + }, + shouldReject + ); + + const expectedLimit = t.isCompatibility ? defaultLimit : dependentAdapterLimit; + t.expect( + device!.limits[limit] === expectedLimit, + `${limit}(${device!.limits[limit]}) === ${expectedLimit}` + ); + }); +} diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInFragmentStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInFragmentStage.spec.ts new file mode 100644 index 00000000000..09216807796 --- /dev/null +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInFragmentStage.spec.ts @@ -0,0 +1,211 @@ +import { + range, + reorder, + kReorderOrderKeys, + ReorderOrder, + assert, +} from '../../../../../common/util/util.js'; + +import { + kMaximumLimitBaseParams, + makeLimitTestGroup, + kBindGroupTests, + getPipelineTypeForBindingCombination, + getPerStageWGSLForBindingCombination, + LimitsRequest, + getStageVisibilityForBinidngCombination, + testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit, +} from './limit_utils.js'; + +const limit = 'maxStorageBuffersInFragmentStage'; +const dependentLimitName = 'maxStorageBuffersPerShaderStage'; + +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', + [dependentLimitName]: 'adapterLimit', +}; + +export const { g, description } = makeLimitTestGroup(limit, { + // MAINTAINANCE_TODO: remove once this limit is required. + limitOptional: true, + limitCheckFn(t, device, { actualLimit }) { + if (!t.isCompatibility) { + const expectedLimit = device.limits[dependentLimitName]; + t.expect( + actualLimit === expectedLimit, + `expected actual actualLimit: ${actualLimit} to equal ${dependentLimitName}: ${expectedLimit}` + ); + return true; + } + return false; + }, +}); + +function createBindGroupLayout( + device: GPUDevice, + visibility: number, + type: GPUBufferBindingType, + order: ReorderOrder, + numBindings: number +) { + const bindGroupLayoutDescription: GPUBindGroupLayoutDescriptor = { + entries: reorder( + order, + range(numBindings, i => ({ + binding: i, + visibility, + buffer: { type }, + })) + ), + }; + return device.createBindGroupLayout(bindGroupLayoutDescription); +} + +g.test('createBindGroupLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createBindGroupLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) + .combine('order', kReorderOrderKeys) + ) + .fn(async t => { + const { limitTest, testValueName, order, type } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + + const visibility = GPUShaderStage.FRAGMENT; + await t.expectValidationError(() => { + createBindGroupLayout(device, visibility, type, order, testValue); + }, shouldError); + }, + kExtraLimits + ); + }); + +g.test('createPipelineLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createPipelineLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[]) + .combine('order', kReorderOrderKeys) + ) + .fn(async t => { + const { limitTest, testValueName, order, type } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const visibility = GPUShaderStage.FRAGMENT; + + t.skipIf( + actualLimit === 0, + `can not make a bindGroupLayout to test createPipelineLaoyout if the actaul limit is 0` + ); + + 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 createBindGroupLayout(device, visibility, type, order, numInGroup); + }); + + await t.expectValidationError( + () => device.createPipelineLayout({ bindGroupLayouts }), + shouldError + ); + }, + kExtraLimits + ); + }); + +g.test('createPipeline,at_over') + .desc( + ` + Test using createRenderPipeline(Async) and createComputePipeline(Async) at and over ${limit} limit + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('async', [false, true] as const) + .beginSubcases() + .combine('order', kReorderOrderKeys) + .combine('bindGroupTest', kBindGroupTests) + ) + .fn(async t => { + const { limitTest, testValueName, async, order, bindGroupTest } = t.params; + const bindingCombination = 'fragment'; + const pipelineType = getPipelineTypeForBindingCombination(bindingCombination); + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + + const code = getPerStageWGSLForBindingCombination( + bindingCombination, + order, + bindGroupTest, + (i, j) => `var u${j}_${i}: f32`, + (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, + testValue + ); + const module = device.createShaderModule({ code }); + + await t.testCreatePipeline( + pipelineType, + async, + module, + shouldError, + `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` + ); + }, + kExtraLimits + ); + }); + +testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(g, limit, dependentLimitName); diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInVertexStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInVertexStage.spec.ts new file mode 100644 index 00000000000..e6feb14be41 --- /dev/null +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageBuffersInVertexStage.spec.ts @@ -0,0 +1,202 @@ +import { + range, + reorder, + kReorderOrderKeys, + ReorderOrder, + assert, +} from '../../../../../common/util/util.js'; + +import { + kMaximumLimitBaseParams, + makeLimitTestGroup, + kBindGroupTests, + getPipelineTypeForBindingCombination, + getPerStageWGSLForBindingCombination, + LimitsRequest, + getStageVisibilityForBinidngCombination, + testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit, +} from './limit_utils.js'; + +const limit = 'maxStorageBuffersInVertexStage'; +const dependentLimitName = 'maxStorageBuffersPerShaderStage'; + +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', + [dependentLimitName]: 'adapterLimit', +}; + +export const { g, description } = makeLimitTestGroup(limit, { + // MAINTAINANCE_TODO: remove once this limit is required. + limitOptional: true, + limitCheckFn(t, device, { actualLimit }) { + if (!t.isCompatibility) { + const expectedLimit = device.limits[dependentLimitName]; + t.expect( + actualLimit === expectedLimit, + `expected actual actualLimit: ${actualLimit} to equal ${dependentLimitName}: ${expectedLimit}` + ); + return true; + } + return false; + }, +}); + +function createBindGroupLayout( + device: GPUDevice, + visibility: number, + order: ReorderOrder, + numBindings: number +) { + const bindGroupLayoutDescription: GPUBindGroupLayoutDescriptor = { + entries: reorder( + order, + range(numBindings, i => ({ + binding: i, + visibility, + buffer: { type: 'read-only-storage' }, + })) + ), + }; + return device.createBindGroupLayout(bindGroupLayoutDescription); +} + +g.test('createBindGroupLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createBindGroupLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params(kMaximumLimitBaseParams.combine('order', kReorderOrderKeys)) + .fn(async t => { + const { limitTest, testValueName, order } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + + const visibility = GPUShaderStage.VERTEX; + await t.expectValidationError(() => { + createBindGroupLayout(device, visibility, order, testValue); + }, shouldError); + }, + kExtraLimits + ); + }); + +g.test('createPipelineLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createPipelineLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params(kMaximumLimitBaseParams.combine('order', kReorderOrderKeys)) + .fn(async t => { + const { limitTest, testValueName, order } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const visibility = GPUShaderStage.VERTEX; + + t.skipIf( + actualLimit === 0, + `can not make a bindGroupLayout to test createPipelineLaoyout if the actaul limit is 0` + ); + + 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 createBindGroupLayout(device, visibility, order, numInGroup); + }); + + await t.expectValidationError( + () => device.createPipelineLayout({ bindGroupLayouts }), + shouldError + ); + }, + kExtraLimits + ); + }); + +g.test('createPipeline,at_over') + .desc( + ` + Test using createRenderPipeline(Async) and createComputePipeline(Async) at and over ${limit} limit + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('async', [false, true] as const) + .beginSubcases() + .combine('order', kReorderOrderKeys) + .combine('bindGroupTest', kBindGroupTests) + ) + .fn(async t => { + const { limitTest, testValueName, async, order, bindGroupTest } = t.params; + const bindingCombination = 'vertex'; + const pipelineType = getPipelineTypeForBindingCombination(bindingCombination); + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + + const code = getPerStageWGSLForBindingCombination( + bindingCombination, + order, + bindGroupTest, + (i, j) => `var u${j}_${i}: f32`, + (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, + testValue + ); + const module = device.createShaderModule({ code }); + + await t.testCreatePipeline( + pipelineType, + async, + module, + shouldError, + `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` + ); + }, + kExtraLimits + ); + }); + +testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(g, limit, dependentLimitName); diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInFragmentStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInFragmentStage.spec.ts new file mode 100644 index 00000000000..2fae97f596c --- /dev/null +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInFragmentStage.spec.ts @@ -0,0 +1,212 @@ +import { + range, + reorder, + kReorderOrderKeys, + ReorderOrder, + assert, +} from '../../../../../common/util/util.js'; +import { kStorageTextureAccessValues } from '../../../../capability_info.js'; + +import { + kMaximumLimitBaseParams, + makeLimitTestGroup, + kBindGroupTests, + getPipelineTypeForBindingCombination, + getPerStageWGSLForBindingCombination, + LimitsRequest, + getStageVisibilityForBinidngCombination, + testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit, +} from './limit_utils.js'; + +const limit = 'maxStorageTexturesInFragmentStage'; +const dependentLimitName = 'maxStorageTexturesPerShaderStage'; + +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', + [dependentLimitName]: 'adapterLimit', +}; + +export const { g, description } = makeLimitTestGroup(limit, { + // MAINTAINANCE_TODO: remove once this limit is required. + limitOptional: true, + limitCheckFn(t, device, { actualLimit }) { + if (!t.isCompatibility) { + const expectedLimit = device.limits[dependentLimitName]; + t.expect( + actualLimit === expectedLimit, + `expected actual actualLimit: ${actualLimit} to equal ${dependentLimitName}: ${expectedLimit}` + ); + return true; + } + return false; + }, +}); + +function createBindGroupLayout( + device: GPUDevice, + visibility: number, + access: GPUStorageTextureAccess, + order: ReorderOrder, + numBindings: number +) { + const bindGroupLayoutDescription: GPUBindGroupLayoutDescriptor = { + entries: reorder( + order, + range(numBindings, i => ({ + binding: i, + visibility, + storageTexture: { format: 'r32float', access }, + })) + ), + }; + return device.createBindGroupLayout(bindGroupLayoutDescription); +} + +g.test('createBindGroupLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createBindGroupLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('access', kStorageTextureAccessValues) + .combine('order', kReorderOrderKeys) + ) + .fn(async t => { + const { limitTest, testValueName, order, access } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + + const visibility = GPUShaderStage.FRAGMENT; + await t.expectValidationError(() => { + createBindGroupLayout(device, visibility, access, order, testValue); + }, shouldError); + }, + kExtraLimits + ); + }); + +g.test('createPipelineLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createPipelineLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('access', kStorageTextureAccessValues) + .combine('order', kReorderOrderKeys) + ) + .fn(async t => { + const { limitTest, testValueName, order, access } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const visibility = GPUShaderStage.FRAGMENT; + + t.skipIf( + actualLimit === 0, + `can not make a bindGroupLayout to test createPipelineLaoyout if the actaul limit is 0` + ); + + 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 createBindGroupLayout(device, visibility, access, order, numInGroup); + }); + + await t.expectValidationError( + () => device.createPipelineLayout({ bindGroupLayouts }), + shouldError + ); + }, + kExtraLimits + ); + }); + +g.test('createPipeline,at_over') + .desc( + ` + Test using createRenderPipeline(Async) and createComputePipeline(Async) at and over ${limit} limit + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('async', [false, true] as const) + .beginSubcases() + .combine('order', kReorderOrderKeys) + .combine('bindGroupTest', kBindGroupTests) + ) + .fn(async t => { + const { limitTest, testValueName, async, order, bindGroupTest } = t.params; + const bindingCombination = 'fragment'; + const pipelineType = getPipelineTypeForBindingCombination(bindingCombination); + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + + const code = getPerStageWGSLForBindingCombination( + bindingCombination, + order, + bindGroupTest, + (i, j) => `var u${j}_${i}: texture_storage_2d`, + (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, + testValue + ); + const module = device.createShaderModule({ code }); + + await t.testCreatePipeline( + pipelineType, + async, + module, + shouldError, + `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` + ); + }, + kExtraLimits + ); + }); + +testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(g, limit, dependentLimitName); diff --git a/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInVertexStage.spec.ts b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInVertexStage.spec.ts new file mode 100644 index 00000000000..c51bb9392e6 --- /dev/null +++ b/src/webgpu/api/validation/capability_checks/limits/maxStorageTexturesInVertexStage.spec.ts @@ -0,0 +1,202 @@ +import { + range, + reorder, + kReorderOrderKeys, + ReorderOrder, + assert, +} from '../../../../../common/util/util.js'; + +import { + kMaximumLimitBaseParams, + makeLimitTestGroup, + kBindGroupTests, + getPipelineTypeForBindingCombination, + getPerStageWGSLForBindingCombination, + LimitsRequest, + getStageVisibilityForBinidngCombination, + testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit, +} from './limit_utils.js'; + +const limit = 'maxStorageTexturesInVertexStage'; +const dependentLimitName = 'maxStorageTexturesPerShaderStage'; + +const kExtraLimits: LimitsRequest = { + maxBindingsPerBindGroup: 'adapterLimit', + maxBindGroups: 'adapterLimit', + [dependentLimitName]: 'adapterLimit', +}; + +export const { g, description } = makeLimitTestGroup(limit, { + // MAINTAINANCE_TODO: remove once this limit is required. + limitOptional: true, + limitCheckFn(t, device, { actualLimit }) { + if (!t.isCompatibility) { + const expectedLimit = device.limits[dependentLimitName]; + t.expect( + actualLimit === expectedLimit, + `expected actual actualLimit: ${actualLimit} to equal ${dependentLimitName}: ${expectedLimit}` + ); + return true; + } + return false; + }, +}); + +function createBindGroupLayout( + device: GPUDevice, + visibility: number, + order: ReorderOrder, + numBindings: number +) { + const bindGroupLayoutDescription: GPUBindGroupLayoutDescriptor = { + entries: reorder( + order, + range(numBindings, i => ({ + binding: i, + visibility, + storageTexture: { format: 'r32float', access: 'read-only' }, + })) + ), + }; + return device.createBindGroupLayout(bindGroupLayoutDescription); +} + +g.test('createBindGroupLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createBindGroupLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params(kMaximumLimitBaseParams.combine('order', kReorderOrderKeys)) + .fn(async t => { + const { limitTest, testValueName, order } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError }) => { + t.skipIf( + t.adapter.limits.maxBindingsPerBindGroup < testValue, + `maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}` + ); + + const visibility = GPUShaderStage.VERTEX; + await t.expectValidationError(() => { + createBindGroupLayout(device, visibility, order, testValue); + }, shouldError); + }, + kExtraLimits + ); + }); + +g.test('createPipelineLayout,at_over') + .desc( + ` + Test using at and over ${limit} limit in createPipelineLayout + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params(kMaximumLimitBaseParams.combine('order', kReorderOrderKeys)) + .fn(async t => { + const { limitTest, testValueName, order } = t.params; + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, shouldError, actualLimit }) => { + const visibility = GPUShaderStage.VERTEX; + + t.skipIf( + actualLimit === 0, + `can not make a bindGroupLayout to test createPipelineLaoyout if the actaul limit is 0` + ); + + 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 createBindGroupLayout(device, visibility, order, numInGroup); + }); + + await t.expectValidationError( + () => device.createPipelineLayout({ bindGroupLayouts }), + shouldError + ); + }, + kExtraLimits + ); + }); + +g.test('createPipeline,at_over') + .desc( + ` + Test using createRenderPipeline(Async) and createComputePipeline(Async) at and over ${limit} limit + + Note: We also test order to make sure the implementation isn't just looking + at just the last entry. + ` + ) + .params( + kMaximumLimitBaseParams + .combine('async', [false, true] as const) + .beginSubcases() + .combine('order', kReorderOrderKeys) + .combine('bindGroupTest', kBindGroupTests) + ) + .fn(async t => { + const { limitTest, testValueName, async, order, bindGroupTest } = t.params; + const bindingCombination = 'vertex'; + const pipelineType = getPipelineTypeForBindingCombination(bindingCombination); + + await t.testDeviceWithRequestedMaximumLimits( + limitTest, + testValueName, + async ({ device, testValue, actualLimit, shouldError }) => { + t.skipIf( + bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup, + `can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}` + ); + + const visibility = getStageVisibilityForBinidngCombination(bindingCombination); + t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue); + + const code = getPerStageWGSLForBindingCombination( + bindingCombination, + order, + bindGroupTest, + (i, j) => `var u${j}_${i}: texture_storage_2d`, + (i, j) => `_ = u${j}_${i};`, + device.limits.maxBindGroups, + testValue + ); + const module = device.createShaderModule({ code }); + + await t.testCreatePipeline( + pipelineType, + async, + module, + shouldError, + `actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}` + ); + }, + kExtraLimits + ); + }); + +testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(g, limit, dependentLimitName);