Skip to content

Commit 3dd1fec

Browse files
authored
Test maxStorage(Buffers/Textures)In(Fragment/Vertex)Stage (gpuweb#4133)
1 parent 1ffe504 commit 3dd1fec

File tree

5 files changed

+943
-13
lines changed

5 files changed

+943
-13
lines changed

src/webgpu/api/validation/capability_checks/limits/limit_utils.ts

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,19 @@ export function addMaximumLimitUpToDependentLimit(
386386
limits[limit] = value;
387387
}
388388

389+
type LimitCheckParams = {
390+
limit: GPUSupportedLimit;
391+
actualLimit: number;
392+
defaultLimit: number;
393+
};
394+
395+
type LimitCheckFn = (t: LimitTestsImpl, device: GPUDevice, params: LimitCheckParams) => boolean;
396+
389397
export class LimitTestsImpl extends GPUTestBase {
390398
_adapter: GPUAdapter | null = null;
391399
_device: GPUDevice | undefined = undefined;
392400
limit: GPUSupportedLimit = '' as GPUSupportedLimit;
401+
limitTestParams: LimitTestParams = {};
393402
defaultLimit = 0;
394403
adapterLimit = 0;
395404

@@ -398,6 +407,11 @@ export class LimitTestsImpl extends GPUTestBase {
398407
const gpu = getGPU(this.rec);
399408
this._adapter = await gpu.requestAdapter();
400409
const limit = this.limit;
410+
// MAINTENANCE_TODO: consider removing this skip if the spec has no optional limits.
411+
this.skipIf(
412+
this._adapter?.limits[limit] === undefined && !!this.limitTestParams.limitOptional,
413+
`${limit} is missing but optional for now`
414+
);
401415
this.defaultLimit = getDefaultLimitForAdapter(this.adapter, limit);
402416
this.adapterLimit = this.adapter.limits[limit] as number;
403417
assert(!Number.isNaN(this.defaultLimit));
@@ -504,16 +518,21 @@ export class LimitTestsImpl extends GPUTestBase {
504518
);
505519
}
506520
} else {
507-
if (requestedLimit <= defaultLimit) {
508-
this.expect(
509-
actualLimit === defaultLimit,
510-
`expected actual actualLimit: ${actualLimit} to equal defaultLimit: ${defaultLimit}`
511-
);
512-
} else {
513-
this.expect(
514-
actualLimit === requestedLimit,
515-
`expected actual actualLimit: ${actualLimit} to equal requestedLimit: ${requestedLimit}`
516-
);
521+
const checked = this.limitTestParams.limitCheckFn
522+
? this.limitTestParams.limitCheckFn(this, device!, { limit, actualLimit, defaultLimit })
523+
: false;
524+
if (!checked) {
525+
if (requestedLimit <= defaultLimit) {
526+
this.expect(
527+
actualLimit === defaultLimit,
528+
`expected actual actualLimit: ${actualLimit} to equal defaultLimit: ${defaultLimit}`
529+
);
530+
} else {
531+
this.expect(
532+
actualLimit === requestedLimit,
533+
`expected actual actualLimit: ${actualLimit} to equal requestedLimit: ${requestedLimit}`
534+
);
535+
}
517536
}
518537
}
519538
}
@@ -534,6 +553,10 @@ export class LimitTestsImpl extends GPUTestBase {
534553
const { defaultLimit, adapterLimit: maximumLimit } = this;
535554

536555
const requestedLimit = getLimitValue(defaultLimit, maximumLimit, limitValueTest);
556+
this.skipIf(
557+
requestedLimit < 0 && limitValueTest === 'underDefault',
558+
`requestedLimit(${requestedLimit}) for ${this.limit} is < 0`
559+
);
537560
return this._getDeviceWithSpecificLimit(requestedLimit, extraLimits, features);
538561
}
539562

@@ -1209,12 +1232,21 @@ export class LimitTestsImpl extends GPUTestBase {
12091232
}
12101233
}
12111234

1235+
type LimitTestParams = {
1236+
limitCheckFn?: LimitCheckFn;
1237+
limitOptional?: boolean;
1238+
};
1239+
12121240
/**
12131241
* Makes a new LimitTest class so that the tests have access to `limit`
12141242
*/
1215-
function makeLimitTestFixture(limit: GPUSupportedLimit): typeof LimitTestsImpl {
1243+
function makeLimitTestFixture(
1244+
limit: GPUSupportedLimit,
1245+
params?: LimitTestParams
1246+
): typeof LimitTestsImpl {
12161247
class LimitTests extends LimitTestsImpl {
12171248
override limit = limit;
1249+
override limitTestParams = params ?? {};
12181250
}
12191251

12201252
return LimitTests;
@@ -1225,8 +1257,79 @@ function makeLimitTestFixture(limit: GPUSupportedLimit): typeof LimitTestsImpl {
12251257
* writing these tests where I'd copy a test, need to rename a limit in 3-4 places,
12261258
* forget one place, and then spend 20-30 minutes wondering why the test was failing.
12271259
*/
1228-
export function makeLimitTestGroup(limit: GPUSupportedLimit) {
1260+
export function makeLimitTestGroup(limit: GPUSupportedLimit, params?: LimitTestParams) {
12291261
const description = `API Validation Tests for ${limit}.`;
1230-
const g = makeTestGroup(makeLimitTestFixture(limit));
1262+
const g = makeTestGroup(makeLimitTestFixture(limit, params));
12311263
return { g, description, limit };
12321264
}
1265+
1266+
/**
1267+
* Test that limit must be less than dependentLimitName when requesting a device.
1268+
*/
1269+
export function testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(
1270+
g: ReturnType<typeof makeLimitTestGroup>['g'],
1271+
limit:
1272+
| 'maxStorageBuffersInFragmentStage'
1273+
| 'maxStorageBuffersInVertexStage'
1274+
| 'maxStorageTexturesInFragmentStage'
1275+
| 'maxStorageTexturesInVertexStage',
1276+
dependentLimitName: 'maxStorageBuffersPerShaderStage' | 'maxStorageTexturesPerShaderStage'
1277+
) {
1278+
g.test(`validate,${dependentLimitName}`)
1279+
.desc(
1280+
`Test that adapter.limit.${limit} and requiredLimits.${limit} must be <= ${dependentLimitName}`
1281+
)
1282+
.params(u => u.combine('useMax', [true, false] as const)) // true case should not reject.
1283+
.fn(async t => {
1284+
const { useMax } = t.params;
1285+
const { adapterLimit: maximumLimit, adapter } = t;
1286+
1287+
const dependentLimit = adapter.limits[dependentLimitName]!;
1288+
t.expect(
1289+
maximumLimit <= dependentLimit,
1290+
`maximumLimit(${maximumLimit}) is <= adapter.limits.${dependentLimitName}(${dependentLimit})`
1291+
);
1292+
1293+
const dependentEffectiveLimits = useMax
1294+
? dependentLimit
1295+
: t.getDefaultLimit(dependentLimitName);
1296+
const shouldReject = maximumLimit > dependentEffectiveLimits;
1297+
t.debug(
1298+
`${limit}(${maximumLimit}) > ${dependentLimitName}(${dependentEffectiveLimits}) shouldReject: ${shouldReject}`
1299+
);
1300+
const device = await t.requestDeviceWithLimits(
1301+
adapter,
1302+
{
1303+
[limit]: maximumLimit,
1304+
...(useMax && {
1305+
[dependentLimitName]: dependentLimit,
1306+
}),
1307+
},
1308+
shouldReject
1309+
);
1310+
device?.destroy();
1311+
});
1312+
1313+
g.test(`auto_upgrade,${dependentLimitName}`)
1314+
.desc(
1315+
`Test that adapter.limit.${limit} is automatically upgraded to ${dependentLimitName} except in compat.`
1316+
)
1317+
.fn(async t => {
1318+
const { adapter, defaultLimit } = t;
1319+
const dependentAdapterLimit = adapter.limits[dependentLimitName];
1320+
const shouldReject = false;
1321+
const device = await t.requestDeviceWithLimits(
1322+
adapter,
1323+
{
1324+
[dependentLimitName]: dependentAdapterLimit,
1325+
},
1326+
shouldReject
1327+
);
1328+
1329+
const expectedLimit = t.isCompatibility ? defaultLimit : dependentAdapterLimit;
1330+
t.expect(
1331+
device!.limits[limit] === expectedLimit,
1332+
`${limit}(${device!.limits[limit]}) === ${expectedLimit}`
1333+
);
1334+
});
1335+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import {
2+
range,
3+
reorder,
4+
kReorderOrderKeys,
5+
ReorderOrder,
6+
assert,
7+
} from '../../../../../common/util/util.js';
8+
9+
import {
10+
kMaximumLimitBaseParams,
11+
makeLimitTestGroup,
12+
kBindGroupTests,
13+
getPipelineTypeForBindingCombination,
14+
getPerStageWGSLForBindingCombination,
15+
LimitsRequest,
16+
getStageVisibilityForBinidngCombination,
17+
testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit,
18+
} from './limit_utils.js';
19+
20+
const limit = 'maxStorageBuffersInFragmentStage';
21+
const dependentLimitName = 'maxStorageBuffersPerShaderStage';
22+
23+
const kExtraLimits: LimitsRequest = {
24+
maxBindingsPerBindGroup: 'adapterLimit',
25+
maxBindGroups: 'adapterLimit',
26+
[dependentLimitName]: 'adapterLimit',
27+
};
28+
29+
export const { g, description } = makeLimitTestGroup(limit, {
30+
// MAINTAINANCE_TODO: remove once this limit is required.
31+
limitOptional: true,
32+
limitCheckFn(t, device, { actualLimit }) {
33+
if (!t.isCompatibility) {
34+
const expectedLimit = device.limits[dependentLimitName];
35+
t.expect(
36+
actualLimit === expectedLimit,
37+
`expected actual actualLimit: ${actualLimit} to equal ${dependentLimitName}: ${expectedLimit}`
38+
);
39+
return true;
40+
}
41+
return false;
42+
},
43+
});
44+
45+
function createBindGroupLayout(
46+
device: GPUDevice,
47+
visibility: number,
48+
type: GPUBufferBindingType,
49+
order: ReorderOrder,
50+
numBindings: number
51+
) {
52+
const bindGroupLayoutDescription: GPUBindGroupLayoutDescriptor = {
53+
entries: reorder(
54+
order,
55+
range(numBindings, i => ({
56+
binding: i,
57+
visibility,
58+
buffer: { type },
59+
}))
60+
),
61+
};
62+
return device.createBindGroupLayout(bindGroupLayoutDescription);
63+
}
64+
65+
g.test('createBindGroupLayout,at_over')
66+
.desc(
67+
`
68+
Test using at and over ${limit} limit in createBindGroupLayout
69+
70+
Note: We also test order to make sure the implementation isn't just looking
71+
at just the last entry.
72+
`
73+
)
74+
.params(
75+
kMaximumLimitBaseParams
76+
.combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[])
77+
.combine('order', kReorderOrderKeys)
78+
)
79+
.fn(async t => {
80+
const { limitTest, testValueName, order, type } = t.params;
81+
82+
await t.testDeviceWithRequestedMaximumLimits(
83+
limitTest,
84+
testValueName,
85+
async ({ device, testValue, shouldError }) => {
86+
t.skipIf(
87+
t.adapter.limits.maxBindingsPerBindGroup < testValue,
88+
`maxBindingsPerBindGroup = ${t.adapter.limits.maxBindingsPerBindGroup} which is less than ${testValue}`
89+
);
90+
91+
const visibility = GPUShaderStage.FRAGMENT;
92+
await t.expectValidationError(() => {
93+
createBindGroupLayout(device, visibility, type, order, testValue);
94+
}, shouldError);
95+
},
96+
kExtraLimits
97+
);
98+
});
99+
100+
g.test('createPipelineLayout,at_over')
101+
.desc(
102+
`
103+
Test using at and over ${limit} limit in createPipelineLayout
104+
105+
Note: We also test order to make sure the implementation isn't just looking
106+
at just the last entry.
107+
`
108+
)
109+
.params(
110+
kMaximumLimitBaseParams
111+
.combine('type', ['storage', 'read-only-storage'] as GPUBufferBindingType[])
112+
.combine('order', kReorderOrderKeys)
113+
)
114+
.fn(async t => {
115+
const { limitTest, testValueName, order, type } = t.params;
116+
117+
await t.testDeviceWithRequestedMaximumLimits(
118+
limitTest,
119+
testValueName,
120+
async ({ device, testValue, shouldError, actualLimit }) => {
121+
const visibility = GPUShaderStage.FRAGMENT;
122+
123+
t.skipIf(
124+
actualLimit === 0,
125+
`can not make a bindGroupLayout to test createPipelineLaoyout if the actaul limit is 0`
126+
);
127+
128+
const maxBindingsPerBindGroup = Math.min(
129+
t.device.limits.maxBindingsPerBindGroup,
130+
actualLimit
131+
);
132+
133+
const kNumGroups = Math.ceil(testValue / maxBindingsPerBindGroup);
134+
135+
// Not sure what to do in this case but best we get notified if it happens.
136+
assert(kNumGroups <= t.device.limits.maxBindGroups);
137+
138+
const bindGroupLayouts = range(kNumGroups, i => {
139+
const numInGroup = Math.min(
140+
testValue - i * maxBindingsPerBindGroup,
141+
maxBindingsPerBindGroup
142+
);
143+
return createBindGroupLayout(device, visibility, type, order, numInGroup);
144+
});
145+
146+
await t.expectValidationError(
147+
() => device.createPipelineLayout({ bindGroupLayouts }),
148+
shouldError
149+
);
150+
},
151+
kExtraLimits
152+
);
153+
});
154+
155+
g.test('createPipeline,at_over')
156+
.desc(
157+
`
158+
Test using createRenderPipeline(Async) and createComputePipeline(Async) at and over ${limit} limit
159+
160+
Note: We also test order to make sure the implementation isn't just looking
161+
at just the last entry.
162+
`
163+
)
164+
.params(
165+
kMaximumLimitBaseParams
166+
.combine('async', [false, true] as const)
167+
.beginSubcases()
168+
.combine('order', kReorderOrderKeys)
169+
.combine('bindGroupTest', kBindGroupTests)
170+
)
171+
.fn(async t => {
172+
const { limitTest, testValueName, async, order, bindGroupTest } = t.params;
173+
const bindingCombination = 'fragment';
174+
const pipelineType = getPipelineTypeForBindingCombination(bindingCombination);
175+
176+
await t.testDeviceWithRequestedMaximumLimits(
177+
limitTest,
178+
testValueName,
179+
async ({ device, testValue, actualLimit, shouldError }) => {
180+
t.skipIf(
181+
bindGroupTest === 'sameGroup' && testValue > device.limits.maxBindingsPerBindGroup,
182+
`can not test ${testValue} bindings in same group because maxBindingsPerBindGroup = ${device.limits.maxBindingsPerBindGroup}`
183+
);
184+
185+
const visibility = getStageVisibilityForBinidngCombination(bindingCombination);
186+
t.skipIfNotEnoughStorageBuffersInStage(visibility, testValue);
187+
188+
const code = getPerStageWGSLForBindingCombination(
189+
bindingCombination,
190+
order,
191+
bindGroupTest,
192+
(i, j) => `var<storage> u${j}_${i}: f32`,
193+
(i, j) => `_ = u${j}_${i};`,
194+
device.limits.maxBindGroups,
195+
testValue
196+
);
197+
const module = device.createShaderModule({ code });
198+
199+
await t.testCreatePipeline(
200+
pipelineType,
201+
async,
202+
module,
203+
shouldError,
204+
`actualLimit: ${actualLimit}, testValue: ${testValue}\n:${code}`
205+
);
206+
},
207+
kExtraLimits
208+
);
209+
});
210+
211+
testMaxStorageXXXInYYYStageDeviceCreationWithDependentLimit(g, limit, dependentLimitName);

0 commit comments

Comments
 (0)