From e3d58ff3774d5ff6f61db89b0a00f08c304b219d Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Thu, 26 Dec 2024 16:11:04 -0800 Subject: [PATCH 1/2] Compat: Add tests for maxStorage(Buffer/Texture)sIn(Vertex/Fragment)Stage Note: I couldn't really think of a test on the core side. Originally I was going to try to request `maxXXXInYYYStage` less than `maxXXXPerShaderStage` which is what the tests do in compat but in core I believe it will be a no-op and that InStage limits will always match their PerStage counterparts. I added a `RequiredLimitsTestMixin` to try to make it easier to write a test that needs specific limits. The issue is the key. I thought about making the key just be the source of `getRequiredLimits` but I can imagine someone writing some more generic requester that takes a list of of limits in which case the code would be generic and so using the source of the funciton would not be a valid key. I also though about adding a MaxOfSpecificLimitsMixin where you could pass in a list of maxes but I think most code that needs maxes can use MaxLimitsTestMixin --- .../createBindGroupLayout_limits.spec.ts | 87 +++++++++++++ .../validation/createPipelineLayout.spec.ts | 91 +++++++++++++ .../render_pipeline/in_stage_limits.spec.ts | 123 ++++++++++++++++++ src/webgpu/gpu_test.ts | 91 ++++++++++--- src/webgpu/listing_meta.json | 5 + 5 files changed, 382 insertions(+), 15 deletions(-) create mode 100644 src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts create mode 100644 src/webgpu/compat/api/validation/createPipelineLayout.spec.ts create mode 100644 src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts diff --git a/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts b/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts new file mode 100644 index 000000000000..7f8fabc5ec65 --- /dev/null +++ b/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts @@ -0,0 +1,87 @@ +export const description = ` +Tests that, in compat mode, you can not create a bind group layout with with +more than the max in stage limit even if the per stage limit is higher. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { range } from '../../../../common/util/util.js'; +import { RequiredLimitsTestMixin } from '../../../gpu_test.js'; +import { CompatibilityTest } from '../../compatibility_test.js'; + +export const g = makeTestGroup( + RequiredLimitsTestMixin(CompatibilityTest, { + getRequiredLimits(adapter: GPUAdapter) { + return { + maxStorageBuffersInFragmentStage: adapter.limits.maxStorageBuffersInFragmentStage! / 2, + maxStorageBuffersInVertexStage: adapter.limits.maxStorageBuffersInVertexStage! / 2, + maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage, + maxStorageTexturesInFragmentStage: adapter.limits.maxStorageTexturesInFragmentStage! / 2, + maxStorageTexturesInVertexStage: adapter.limits.maxStorageTexturesInVertexStage! / 2, + maxStorageTexturesPerShaderStage: adapter.limits.maxStorageTexturesPerShaderStage, + }; + }, + key() { + return ` + maxStorageBuffersInFragmentStage/2, + maxStorageBuffersInVertexStage/2, + maxStorageTexturesInFragmentStage/2, + maxStorageTexturesInVertexStage/2, + maxStorageBuffersPerShaderStage + maxStorageTexturesPerShaderStage + `; + }, + }) +); + +g.test('maxStorageBuffersTexturesInVertexFragmentStage') + .desc( + ` + Tests that you can't use more than maxStorage(Buffers/Textures)In(Fragment/Vertex)Stage when + the limit is less than maxStorage(Buffers/Textures)PerShaderStage + ` + ) + .params(u => + u + .combine('limit', [ + 'maxStorageBuffersInFragmentStage', + 'maxStorageBuffersInVertexStage', + 'maxStorageTexturesInFragmentStage', + 'maxStorageTexturesInVertexStage', + ] as const) + .beginSubcases() + .combine('extra', [0, 1] as const) + ) + .fn(t => { + const { limit, extra } = t.params; + const { device } = t; + + const isBuffer = limit.includes('Buffers'); + const inStageLimit = device.limits[limit]!; + const perStageLimitName = isBuffer + ? 'maxStorageBuffersPerShaderStage' + : 'maxStorageTexturesPerShaderStage'; + const perStageLimit = device.limits[perStageLimitName]; + + t.debug(`{${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit}})`); + + t.skipIf(inStageLimit === 0, `${limit} is 0`); + t.skipIf( + !(inStageLimit < perStageLimit), + `{${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit}})` + ); + + const visibility = limit.includes('Fragment') ? GPUShaderStage.FRAGMENT : GPUShaderStage.VERTEX; + + const expectFailure = extra > 0; + t.expectValidationError(() => { + device.createBindGroupLayout({ + entries: range(inStageLimit + extra, i => ({ + binding: i, + visibility, + ...(isBuffer + ? { buffer: { type: 'read-only-storage' } } + : { storageTexture: { format: 'r32float', access: 'read-only' } }), + })), + }); + }, expectFailure); + }); diff --git a/src/webgpu/compat/api/validation/createPipelineLayout.spec.ts b/src/webgpu/compat/api/validation/createPipelineLayout.spec.ts new file mode 100644 index 000000000000..a7c6c1429c98 --- /dev/null +++ b/src/webgpu/compat/api/validation/createPipelineLayout.spec.ts @@ -0,0 +1,91 @@ +export const description = ` +Tests that, in compat mode, you can not create a pipeline layout with with +more than the max in stage limit even if the per stage limit is higher. +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { range } from '../../../../common/util/util.js'; +import { RequiredLimitsTestMixin } from '../../../gpu_test.js'; +import { CompatibilityTest } from '../../compatibility_test.js'; + +export const g = makeTestGroup( + RequiredLimitsTestMixin(CompatibilityTest, { + getRequiredLimits(adapter: GPUAdapter) { + return { + maxStorageBuffersInFragmentStage: adapter.limits.maxStorageBuffersInFragmentStage! / 2, + maxStorageBuffersInVertexStage: adapter.limits.maxStorageBuffersInVertexStage! / 2, + maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage, + maxStorageTexturesInFragmentStage: adapter.limits.maxStorageTexturesInFragmentStage! / 2, + maxStorageTexturesInVertexStage: adapter.limits.maxStorageTexturesInVertexStage! / 2, + maxStorageTexturesPerShaderStage: adapter.limits.maxStorageTexturesPerShaderStage, + }; + }, + key() { + return ` + maxStorageBuffersInFragmentStage/2, + maxStorageBuffersInVertexStage/2, + maxStorageTexturesInFragmentStage/2, + maxStorageTexturesInVertexStage/2, + maxStorageBuffersPerShaderStage + maxStorageTexturesPerShaderStage + `; + }, + }) +); + +g.test('maxStorageBuffersTexturesInVertexFragmentStage') + .desc( + ` + Tests that you can't use more than maxStorage(Buffers/Textures)In(Fragment/Vertex)Stage when + the limit is less than maxStorage(Buffers/Textures)PerShaderStage + ` + ) + .params(u => + u + .combine('limit', [ + 'maxStorageBuffersInFragmentStage', + 'maxStorageBuffersInVertexStage', + 'maxStorageTexturesInFragmentStage', + 'maxStorageTexturesInVertexStage', + ] as const) + .beginSubcases() + .combine('extra', [0, 1] as const) + ) + .fn(t => { + const { limit, extra } = t.params; + const { device } = t; + + const isBuffer = limit.includes('Buffers'); + const inStageLimit = device.limits[limit]!; + const perStageLimitName = isBuffer + ? 'maxStorageBuffersPerShaderStage' + : 'maxStorageTexturesPerShaderStage'; + const perStageLimit = device.limits[perStageLimitName]; + + t.debug(`{${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit}})`); + + t.skipIf(inStageLimit === 0, `${limit} is 0`); + t.skipIf( + !(inStageLimit < perStageLimit), + `{${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit}})` + ); + + const visibility = limit.includes('Fragment') ? GPUShaderStage.FRAGMENT : GPUShaderStage.VERTEX; + + const bindGroupLayouts = [inStageLimit, extra].map(count => + device.createBindGroupLayout({ + entries: range(count, i => ({ + binding: i, + visibility, + ...(isBuffer + ? { buffer: { type: 'read-only-storage' } } + : { storageTexture: { format: 'r32float', access: 'read-only' } }), + })), + }) + ); + + const expectFailure = extra > 0; + t.expectValidationError(() => { + device.createPipelineLayout({ bindGroupLayouts }); + }, expectFailure); + }); diff --git a/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts b/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts new file mode 100644 index 000000000000..d7fc44088495 --- /dev/null +++ b/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts @@ -0,0 +1,123 @@ +export const description = ` +Tests that, in compat mode, you can not create a pipeline layout with with +more than the max in stage limit even if the per stage limit is higher. +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { range } from '../../../../../common/util/util.js'; +import { RequiredLimitsTestMixin } from '../../../../gpu_test.js'; +import { CompatibilityTest } from '../../../compatibility_test.js'; + +export const g = makeTestGroup( + RequiredLimitsTestMixin(CompatibilityTest, { + getRequiredLimits(adapter: GPUAdapter) { + return { + maxStorageBuffersInFragmentStage: adapter.limits.maxStorageBuffersInFragmentStage! / 2, + maxStorageBuffersInVertexStage: adapter.limits.maxStorageBuffersInVertexStage! / 2, + maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage, + maxStorageTexturesInFragmentStage: adapter.limits.maxStorageTexturesInFragmentStage! / 2, + maxStorageTexturesInVertexStage: adapter.limits.maxStorageTexturesInVertexStage! / 2, + maxStorageTexturesPerShaderStage: adapter.limits.maxStorageTexturesPerShaderStage, + }; + }, + key() { + return ` + maxStorageBuffersInFragmentStage/2, + maxStorageBuffersInVertexStage/2, + maxStorageTexturesInFragmentStage/2, + maxStorageTexturesInVertexStage/2, + maxStorageBuffersPerShaderStage + maxStorageTexturesPerShaderStage + `; + }, + }) +); + +g.test('maxStorageBuffersTexturesInVertexFragmentStage') + .desc( + ` + Tests that you can't use more than maxStorage(Buffers/Textures)In(Fragment/Vertex)Stage when + the limit is less than maxStorage(Buffers/Textures)PerShaderStage + ` + ) + .params(u => + u + .combine('limit', [ + 'maxStorageBuffersInFragmentStage', + 'maxStorageBuffersInVertexStage', + 'maxStorageTexturesInFragmentStage', + 'maxStorageTexturesInVertexStage', + ] as const) + .beginSubcases() + .combine('async', [false, true] as const) + .combine('extra', [0, 1] as const) + ) + .fn(t => { + const { limit, extra, async } = t.params; + const { device } = t; + + const isBuffer = limit.includes('Buffers'); + const inStageLimit = device.limits[limit]!; + const perStageLimitName = isBuffer + ? 'maxStorageBuffersPerShaderStage' + : 'maxStorageTexturesPerShaderStage'; + const perStageLimit = device.limits[perStageLimitName]; + + t.debug(`{${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit}})`); + + t.skipIf(inStageLimit === 0, `${limit} is 0`); + t.skipIf( + !(inStageLimit < perStageLimit), + `{${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit}})` + ); + + const typeWGSLFn = isBuffer + ? (i: number) => `var v${i}: f32;` + : (i: number) => `var v${i}: texture_storage_2d;`; + + const count = inStageLimit + extra; + const code = ` + ${range(count, i => `@group(0) @binding(${i}) ${typeWGSLFn(i)}`).join('\n')} + + fn useResources() { + ${range(count, i => `_ = v${i};`).join('\n')} + } + + @vertex fn vsNoUse() -> @builtin(position) vec4f { + return vec4f(0); + } + + @vertex fn vsUse() -> @builtin(position) vec4f { + useResources(); + return vec4f(0); + } + + @fragment fn fsNoUse() -> @location(0) vec4f { + return vec4f(0); + } + + @fragment fn fsUse() -> @location(0) vec4f { + useResources(); + return vec4f(0); + } + `; + + const module = device.createShaderModule({ code }); + + const isFragment = limit.includes('Fragment'); + const pipelineDescriptor: GPURenderPipelineDescriptor = { + layout: 'auto', + vertex: { + module, + entryPoint: isFragment ? 'vsNoUse' : 'vsUse', + }, + fragment: { + module, + entryPoint: isFragment ? 'fsUse' : 'fsNoUse', + targets: [{ format: 'rgba8unorm' }], + }, + }; + + const success = extra === 0; + t.doCreateRenderPipelineTest(async, success, pipelineDescriptor); + }); diff --git a/src/webgpu/gpu_test.ts b/src/webgpu/gpu_test.ts index 53b286d8103c..a7d8cef19c8d 100644 --- a/src/webgpu/gpu_test.ts +++ b/src/webgpu/gpu_test.ts @@ -1298,63 +1298,124 @@ function getAdapterLimitsAsDeviceRequiredLimits(adapter: GPUAdapter) { return requiredLimits; } -function setAllLimitsToAdapterLimits( +function applyLimitsToDescriptor( adapter: GPUAdapter, - desc: CanonicalDeviceDescriptor | undefined + desc: CanonicalDeviceDescriptor | undefined, + getRequiredLimits: (adapter: GPUAdapter) => Record ) { const descWithMaxLimits: CanonicalDeviceDescriptor = { requiredFeatures: [], defaultQueue: {}, ...desc, - requiredLimits: getAdapterLimitsAsDeviceRequiredLimits(adapter), + requiredLimits: getRequiredLimits(adapter), }; return descWithMaxLimits; } /** - * Used by MaxLimitsTest to request a device with all the max limits of the adapter. + * Used by RequiredLimitsTestMixin to allow you to request specific limits + * + * Supply a `getRequiredLimits` function that given a GPUAdapter, turns the limits + * you want. + * + * Also supply a key function that returns a device key. You should generally return + * the name of each limit you request and any math you did on the limit. For example + * + * ```js + * { + * getRequiredLimits(adapter) { + * return { + * maxBindGroups: adapter.limits.maxBindGroups / 2, + * maxTextureDimensions2D: Math.max(adapter.limits.maxTextureDimensions2D, 8192), + * }, + * }, + * key() { + * return ` + * maxBindGroups / 2, + * max(maxTextureDimension2D, 8192), + * `; + * }, + * } + * ``` + * + * Its important to note, the key is used BEFORE knowing the adapter limits to get a device + * that was already created with the same key. */ -export class MaxLimitsGPUTestSubcaseBatchState extends GPUTestSubcaseBatchState { +interface RequiredLimitsHelper { + getRequiredLimits: (adapter: GPUAdapter) => Record; + key(): string; +} + +/** + * Used by RequiredLimitsTest to request a device with all requested limits of the adapter. + */ +export class RequiredLimitsGPUTestSubcaseBatchState extends GPUTestSubcaseBatchState { + private requiredLimitsHelper: RequiredLimitsHelper; + constructor( + protected override readonly recorder: TestCaseRecorder, + public override readonly params: TestParams, + requiredLimitsHelper: RequiredLimitsHelper + ) { + super(recorder, params); + this.requiredLimitsHelper = requiredLimitsHelper; + } override selectDeviceOrSkipTestCase( descriptor: DeviceSelectionDescriptor, descriptorModifier?: DescriptorModifier ): void { + const requiredLimitsHelper = this.requiredLimitsHelper; const mod: DescriptorModifier = { descriptorModifier(adapter: GPUAdapter, desc: CanonicalDeviceDescriptor | undefined) { desc = descriptorModifier?.descriptorModifier ? descriptorModifier.descriptorModifier(adapter, desc) : desc; - return setAllLimitsToAdapterLimits(adapter, desc); + return applyLimitsToDescriptor(adapter, desc, requiredLimitsHelper.getRequiredLimits); }, keyModifier(baseKey: string) { - return `${baseKey}:MaxLimits`; + return `${baseKey}:${requiredLimitsHelper.key()}`; }, }; super.selectDeviceOrSkipTestCase(initUncanonicalizedDeviceDescriptor(descriptor), mod); } } -export type MaxLimitsTestMixinType = { +export type RequiredLimitsTestMixinType = { // placeholder. Change to an interface if we need MaxLimits specific methods. }; -export function MaxLimitsTestMixin>( - Base: F -): FixtureClassWithMixin { - class MaxLimitsImpl +/** + * A text mixin to make it relatively easy to request specific limits. + */ +export function RequiredLimitsTestMixin>( + Base: F, + requiredLimitsHelper: RequiredLimitsHelper +): FixtureClassWithMixin { + class RequiredLimitsImpl extends (Base as FixtureClassInterface) - implements MaxLimitsTestMixinType + implements RequiredLimitsTestMixinType { // public static override MakeSharedState( recorder: TestCaseRecorder, params: TestParams ): GPUTestSubcaseBatchState { - return new MaxLimitsGPUTestSubcaseBatchState(recorder, params); + return new RequiredLimitsGPUTestSubcaseBatchState(recorder, params, requiredLimitsHelper); } } - return MaxLimitsImpl as unknown as FixtureClassWithMixin; + return RequiredLimitsImpl as unknown as FixtureClassWithMixin; +} + +/** + * Requests all the max limits from the adapter. + */ +export function MaxLimitsTestMixin>(Base: F) { + return RequiredLimitsTestMixin(Base, { + getRequiredLimits: getAdapterLimitsAsDeviceRequiredLimits, + key() { + return 'AllLimits'; + }, + }); } /** diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index 618753748601..6b8ced65d043 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -867,6 +867,8 @@ "webgpu:api,validation,texture,rg11b10ufloat_renderable:create_texture:*": { "subcaseMS": 12.700 }, "webgpu:compat,api,validation,createBindGroup:viewDimension_matches_textureBindingViewDimension:*": { "subcaseMS": 6.523 }, "webgpu:compat,api,validation,createBindGroupLayout:unsupportedStorageTextureFormats:*": { "subcaseMS": 0.601 }, + "webgpu:compat,api,validation,createBindGroupLayout_limits:maxStorageBuffersTexturesInVertexFragmentStage:*": { "subcaseMS": 21.765 }, + "webgpu:compat,api,validation,createPipelineLayout:maxStorageBuffersTexturesInVertexFragmentStage:*": { "subcaseMS": 7.776 }, "webgpu:compat,api,validation,encoding,cmds,copyTextureToBuffer:compressed:*": { "subcaseMS": 202.929 }, "webgpu:compat,api,validation,encoding,cmds,copyTextureToTexture:compressed:*": { "subcaseMS": 0.600 }, "webgpu:compat,api,validation,encoding,cmds,copyTextureToTexture:multisample:*": { "subcaseMS": 0.600 }, @@ -874,8 +876,11 @@ "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,compute_pass,used:*": { "subcaseMS": 49.405 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,render_pass,unused:*": { "subcaseMS": 16.002 }, "webgpu:compat,api,validation,encoding,programmable,pipeline_bind_group_compat:twoDifferentTextureViews,render_pass,used:*": { "subcaseMS": 0.000 }, + "webgpu:compat,api,validation,pipeline_creation:depth_textures:*": { "subcaseMS": 335.073 }, + "webgpu:compat,api,validation,pipeline_creation:texture_sampler_combos:*": { "subcaseMS": 2072.005 }, "webgpu:compat,api,validation,render_pipeline,depth_stencil_state:depthBiasClamp:*": { "subcaseMS": 1.604 }, "webgpu:compat,api,validation,render_pipeline,fragment_state:colorState:*": { "subcaseMS": 32.604 }, + "webgpu:compat,api,validation,render_pipeline,in_stage_limits:maxStorageBuffersTexturesInVertexFragmentStage:*": { "subcaseMS": 275.162 }, "webgpu:compat,api,validation,render_pipeline,unsupported_wgsl:interpolate:*": { "subcaseMS": 3.488 }, "webgpu:compat,api,validation,render_pipeline,unsupported_wgsl:sample_index:*": { "subcaseMS": 0.487 }, "webgpu:compat,api,validation,render_pipeline,unsupported_wgsl:sample_mask:*": { "subcaseMS": 0.408 }, From 6cea535208801bec8f5b457835f47c9b9fc8a34f Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Mon, 6 Jan 2025 10:22:13 -0800 Subject: [PATCH 2/2] address comments --- .../createBindGroupLayout_limits.spec.ts | 4 ++-- .../render_pipeline/in_stage_limits.spec.ts | 4 ++-- src/webgpu/gpu_test.ts | 23 ++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts b/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts index 7f8fabc5ec65..d5bf7a063aaf 100644 --- a/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts +++ b/src/webgpu/compat/api/validation/createBindGroupLayout_limits.spec.ts @@ -62,12 +62,12 @@ g.test('maxStorageBuffersTexturesInVertexFragmentStage') : 'maxStorageTexturesPerShaderStage'; const perStageLimit = device.limits[perStageLimitName]; - t.debug(`{${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit}})`); + t.debug(`${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit})`); t.skipIf(inStageLimit === 0, `${limit} is 0`); t.skipIf( !(inStageLimit < perStageLimit), - `{${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit}})` + `${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit})` ); const visibility = limit.includes('Fragment') ? GPUShaderStage.FRAGMENT : GPUShaderStage.VERTEX; diff --git a/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts b/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts index d7fc44088495..5e07c274958f 100644 --- a/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts +++ b/src/webgpu/compat/api/validation/render_pipeline/in_stage_limits.spec.ts @@ -63,12 +63,12 @@ g.test('maxStorageBuffersTexturesInVertexFragmentStage') : 'maxStorageTexturesPerShaderStage'; const perStageLimit = device.limits[perStageLimitName]; - t.debug(`{${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit}})`); + t.debug(`${limit}(${inStageLimit}), ${perStageLimitName}(${perStageLimit})`); t.skipIf(inStageLimit === 0, `${limit} is 0`); t.skipIf( !(inStageLimit < perStageLimit), - `{${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit}})` + `${limit}(${inStageLimit}) is not less than ${perStageLimitName}(${perStageLimit})` ); const typeWGSLFn = isBuffer diff --git a/src/webgpu/gpu_test.ts b/src/webgpu/gpu_test.ts index a7d8cef19c8d..1e7fac4a984e 100644 --- a/src/webgpu/gpu_test.ts +++ b/src/webgpu/gpu_test.ts @@ -1298,6 +1298,27 @@ function getAdapterLimitsAsDeviceRequiredLimits(adapter: GPUAdapter) { return requiredLimits; } +/** + * Removes limits that don't exist on the adapter. + * A test might request a new limit that not all implementions support. The test itself + * should check the requested limit using code that expects undefined. + * + * ```ts + * t.skipIf(limit < 2); // BAD! Doesn't skip if unsupported beause undefined is never less than 2. + * t.skipIf(!(limit >= 2)); // Good. Skips if limits is not >= 2. undefined is not >= 2. + * ``` + */ +function removeNonExistantLimits(adapter: GPUAdapter, limits: Record) { + const filteredLimits: Record = {}; + const adapterLimits = adapter.limits as unknown as Record; + for (const [limit, value] of Object.entries(limits)) { + if (adapterLimits[limit] !== undefined) { + filteredLimits[limit] = value; + } + } + return filteredLimits; +} + function applyLimitsToDescriptor( adapter: GPUAdapter, desc: CanonicalDeviceDescriptor | undefined, @@ -1307,7 +1328,7 @@ function applyLimitsToDescriptor( requiredFeatures: [], defaultQueue: {}, ...desc, - requiredLimits: getRequiredLimits(adapter), + requiredLimits: removeNonExistantLimits(adapter, getRequiredLimits(adapter)), }; return descWithMaxLimits; }