Skip to content

Commit

Permalink
Test maxStorage(Buffers/Textures)In(Fragment/Vertex)Stage
Browse files Browse the repository at this point in the history
  • Loading branch information
greggman committed Jan 9, 2025
1 parent 40867c3 commit ef118bb
Show file tree
Hide file tree
Showing 5 changed files with 942 additions and 13 deletions.
128 changes: 115 additions & 13 deletions src/webgpu/api/validation/capability_checks/limits/limit_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,19 @@ export const kMinimumLimitBaseParams = kUnitCaseParamsBuilder
.combine('limitTest', kMinimumLimitValueTests)
.combine('testValueName', kMinimumTestValues);

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;

Expand All @@ -350,6 +359,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));
Expand Down Expand Up @@ -452,16 +465,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}`
);
}
}
}
}
Expand All @@ -482,6 +500,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);
}

Expand Down Expand Up @@ -1157,12 +1179,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;
Expand All @@ -1173,8 +1204,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<typeof makeLimitTestGroup>['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}`
);
});
}
Original file line number Diff line number Diff line change
@@ -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<storage> 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);
Loading

0 comments on commit ef118bb

Please sign in to comment.