Skip to content

Commit

Permalink
Compat: refactor maxStorage(Buffers/Textures)PerShaderStage test (#4122)
Browse files Browse the repository at this point in the history
Refactor to handle maxStorage(Buffers/Textures)In(Vertex/Fragment)Stage
being less than maxStorage(Buffers/Textures)PerShaderStage.

Also maxDynamicStorageBuffersPerPipelineLayout.

I noticed a test was missing there too so added it.
  • Loading branch information
greggman authored Dec 31, 2024
1 parent 473fede commit d65f8cb
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 55 deletions.
43 changes: 43 additions & 0 deletions src/webgpu/api/validation/capability_checks/limits/limit_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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}`
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,108 @@
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);

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({
entries: range(testValue, i => ({
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
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getPipelineTypeForBindingCombination,
getPerStageWGSLForBindingCombination,
LimitsRequest,
getStageVisibilityForBinidngCombination,
} from './limit_utils.js';

const kExtraLimits: LimitsRequest = {
Expand All @@ -35,7 +36,7 @@ function createBindGroupLayout(
order: ReorderOrder,
numBindings: number
) {
return device.createBindGroupLayout({
const bindGroupLayoutDescription = {
entries: reorder(
order,
range(numBindings, i => ({
Expand All @@ -44,7 +45,8 @@ function createBindGroupLayout(
buffer: { type },
}))
),
});
};
return device.createBindGroupLayout(bindGroupLayoutDescription);
}

g.test('createBindGroupLayout,at_over')
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
)
Expand All @@ -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,
Expand Down
Loading

0 comments on commit d65f8cb

Please sign in to comment.