Skip to content

Commit

Permalink
Compat: Add tests for maxStorage(Buffer/Texture)sIn(Vertex/Fragment)S…
Browse files Browse the repository at this point in the history
…tage (#4118)

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
  • Loading branch information
greggman authored Jan 6, 2025
1 parent d3fec1f commit e99550b
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
91 changes: 91 additions & 0 deletions src/webgpu/compat/api/validation/createPipelineLayout.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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<storage, read> v${i}: f32;`
: (i: number) => `var v${i}: texture_storage_2d<r32float, read>;`;

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);
});
Loading

0 comments on commit e99550b

Please sign in to comment.