blob: efe4ee5652a3f28b88706c3dd0d1205b122d9e8e [file] [log] [blame]
export const description = `
createBindGroupLayout validation tests.
TODO: make sure tests are complete.
`;
import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import {
kShaderStages,
kShaderStageCombinations,
kStorageTextureAccessValues,
kTextureSampleTypes,
kTextureViewDimensions,
allBindingEntries,
bindingTypeInfo,
bufferBindingTypeInfo,
kBufferBindingTypes,
BGLEntry,
getBindingLimitForBindingType,
} from '../../capability_info.js';
import {
isTextureFormatUsableWithStorageAccessMode,
kAllTextureFormats,
} from '../../format_info.js';
function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
return JSON.parse(JSON.stringify(descriptor));
}
function isValidBufferTypeForStages(
device: GPUDevice,
visibility: number,
type: GPUBufferBindingType | undefined
) {
if (type === 'read-only-storage' || type === 'storage') {
if (visibility & GPUShaderStage.VERTEX) {
if (!(device.limits.maxStorageBuffersInVertexStage! > 0)) {
return false;
}
}
if (visibility & GPUShaderStage.FRAGMENT) {
if (!(device.limits.maxStorageBuffersInFragmentStage! > 0)) {
return false;
}
}
}
return true;
}
function isValidStorageTextureForStages(device: GPUDevice, visibility: number) {
if (visibility & GPUShaderStage.VERTEX) {
if (!(device.limits.maxStorageTexturesInVertexStage! > 0)) {
return false;
}
}
if (visibility & GPUShaderStage.FRAGMENT) {
if (!(device.limits.maxStorageTexturesInFragmentStage! > 0)) {
return false;
}
}
return true;
}
function isValidBGLEntryForStages(device: GPUDevice, visibility: number, entry: BGLEntry) {
return entry.storageTexture
? isValidStorageTextureForStages(device, visibility)
: entry.buffer
? isValidBufferTypeForStages(device, visibility, entry.buffer?.type)
: true;
}
export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);
g.test('duplicate_bindings')
.desc('Test that uniqueness of binding numbers across entries is enforced.')
.paramsSubcasesOnly([
{ bindings: [0, 1], _valid: true },
{ bindings: [0, 0], _valid: false },
])
.fn(t => {
const { bindings, _valid } = t.params;
const entries: Array<GPUBindGroupLayoutEntry> = [];
for (const binding of bindings) {
entries.push({
binding,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' as const },
});
}
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries,
});
}, !_valid);
});
g.test('maximum_binding_limit')
.desc(
`
Test that a validation error is generated if the binding number exceeds the maximum binding limit.
TODO: Need to also test with higher limits enabled on the device, once we have a way to do that.
`
)
.paramsSubcasesOnly(u =>
u.combine('bindingVariant', [1, 4, 8, 256, 'default', 'default-minus-one'] as const)
)
.fn(t => {
const { bindingVariant } = t.params;
const entries: Array<GPUBindGroupLayoutEntry> = [];
const binding =
bindingVariant === 'default'
? t.device.limits.maxBindingsPerBindGroup
: bindingVariant === 'default-minus-one'
? t.device.limits.maxBindingsPerBindGroup - 1
: bindingVariant;
entries.push({
binding,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' as const },
});
const success = binding < t.device.limits.maxBindingsPerBindGroup;
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries,
});
}, !success);
});
g.test('visibility')
.desc(
`
Test that only the appropriate combinations of visibilities are allowed for each resource type.
- Test each possible combination of shader stage visibilities.
- Test each type of bind group resource.`
)
.params(u =>
u
.combine('visibility', kShaderStageCombinations)
.beginSubcases()
.combine('entry', allBindingEntries(false))
)
.fn(t => {
const { visibility, entry } = t.params;
const info = bindingTypeInfo(entry);
const success =
(visibility & ~info.validStages) === 0 &&
isValidBGLEntryForStages(t.device, visibility, entry);
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [{ binding: 0, visibility, ...entry }],
});
}, !success);
});
g.test('visibility,VERTEX_shader_stage_buffer_type')
.desc(
`
Test that a validation error is generated if the buffer type is 'storage' when the
visibility of the entry includes VERTEX.
`
)
.params(u =>
u //
.combine('shaderStage', kShaderStageCombinations)
.beginSubcases()
.combine('type', kBufferBindingTypes)
)
.fn(t => {
const { shaderStage, type } = t.params;
const success =
!(type === 'storage' && shaderStage & GPUShaderStage.VERTEX) &&
isValidBufferTypeForStages(t.device, shaderStage, type);
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: shaderStage,
buffer: { type },
},
],
});
}, !success);
});
g.test('visibility,VERTEX_shader_stage_storage_texture_access')
.desc(
`
Test that a validation error is generated if the access value is 'write-only' when the
visibility of the entry includes VERTEX.
`
)
.params(u =>
u //
.combine('shaderStage', kShaderStageCombinations)
.beginSubcases()
.combine('access', [undefined, ...kStorageTextureAccessValues])
)
.fn(t => {
const { shaderStage, access } = t.params;
const appliedAccess = access ?? 'write-only';
const success =
!(
// If visibility includes VERETX, storageTexture.access must be "read-only"
(shaderStage & GPUShaderStage.VERTEX && appliedAccess !== 'read-only')
) && isValidStorageTextureForStages(t.device, shaderStage);
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: shaderStage,
storageTexture: { access, format: 'r32uint' },
},
],
});
}, !success);
});
g.test('multisampled_validation')
.desc(
`
Test that multisampling is only allowed if view dimensions is "2d" and the sampleType is not
"float".
`
)
.params(u =>
u //
.combine('viewDimension', [undefined, ...kTextureViewDimensions])
.beginSubcases()
.combine('sampleType', [undefined, ...kTextureSampleTypes])
)
.fn(t => {
const { viewDimension, sampleType } = t.params;
const success =
(viewDimension === '2d' || viewDimension === undefined) &&
(sampleType ?? 'float') !== 'float';
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
texture: { multisampled: true, viewDimension, sampleType },
},
],
});
}, !success);
});
g.test('max_dynamic_buffers')
.desc(
`
Test that limits on the maximum number of dynamic buffers are enforced.
- Test creation of a bind group layout using the maximum number of dynamic buffers works.
- Test creation of a bind group layout using the maximum number of dynamic buffers + 1 fails.
- TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.`
)
.params(u =>
u
.combine('type', kBufferBindingTypes)
.beginSubcases()
.combine('extraDynamicBuffers', [0, 1])
.combine('staticBuffers', [0, 1])
)
.fn(t => {
const { type, extraDynamicBuffers, staticBuffers } = t.params;
const info = bufferBindingTypeInfo({ type });
const limitName = info.perPipelineLimitClass.maxDynamicLimit;
const bufferCount = limitName ? t.device.limits[limitName]! : 0;
const dynamicBufferCount = bufferCount + extraDynamicBuffers;
const perStageLimit = t.device.limits[info.perStageLimitClass.maxLimits.COMPUTE]!;
const entries = [];
for (let i = 0; i < dynamicBufferCount; i++) {
entries.push({
binding: i,
visibility: GPUShaderStage.COMPUTE,
buffer: { type, hasDynamicOffset: true },
});
}
for (let i = dynamicBufferCount; i < dynamicBufferCount + staticBuffers; i++) {
entries.push({
binding: i,
visibility: GPUShaderStage.COMPUTE,
buffer: { type, hasDynamicOffset: false },
});
}
const descriptor = {
entries,
};
t.expectValidationError(
() => {
t.device.createBindGroupLayout(descriptor);
},
extraDynamicBuffers > 0 || entries.length > perStageLimit
);
});
/**
* One bind group layout will be filled with kPerStageBindingLimit[...] of the type |type|.
* For each item in the array returned here, a case will be generated which tests a pipeline
* layout with one extra bind group layout with one extra binding. That extra binding will have:
*
* - If extraTypeSame, any of the binding types which counts toward the same limit as |type|.
* (i.e. 'storage-buffer' <-> 'readonly-storage-buffer').
* - Otherwise, an arbitrary other type.
*/
function* pickExtraBindingTypesForPerStage(entry: BGLEntry, extraTypeSame: boolean) {
if (extraTypeSame) {
const info = bindingTypeInfo(entry);
for (const extra of allBindingEntries(false)) {
const extraInfo = bindingTypeInfo(extra);
if (info.perStageLimitClass.class === extraInfo.perStageLimitClass.class) {
yield extra;
}
}
} else {
yield entry.sampler ? { texture: {} } : { sampler: {} };
}
}
const kMaxResourcesCases = kUnitCaseParamsBuilder
.combine('maxedEntry', allBindingEntries(false))
.beginSubcases()
.combine('maxedVisibility', kShaderStages)
.filter(p => (bindingTypeInfo(p.maxedEntry).validStages & p.maxedVisibility) !== 0)
.expand('extraEntry', p => [
...pickExtraBindingTypesForPerStage(p.maxedEntry, true),
...pickExtraBindingTypesForPerStage(p.maxedEntry, false),
])
.combine('extraVisibility', kShaderStages)
.filter(p => (bindingTypeInfo(p.extraEntry).validStages & p.extraVisibility) !== 0);
// Should never fail unless limitInfo.maxBindingsPerBindGroup.default is exceeded, because the validation for
// resources-of-type-per-stage is in pipeline layout creation.
g.test('max_resources_per_stage,in_bind_group_layout')
.desc(
`
Test that the maximum number of bindings of a given type per-stage cannot be exceeded in a
single bind group layout.
- Test each binding type.
- Test that creation of a bind group layout using the maximum number of bindings works.
- Test that creation of a bind group layout using the maximum number of bindings + 1 fails.
- TODO(#230): Update to enforce per-stage and per-pipeline-layout limits on BGLs as well.`
)
.params(kMaxResourcesCases)
.fn(t => {
const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
const maxedTypeInfo = bindingTypeInfo(maxedEntry);
const maxedCount = getBindingLimitForBindingType(t.device, maxedVisibility, maxedEntry);
const extraTypeInfo = bindingTypeInfo(extraEntry);
t.skipIf(!isValidBGLEntryForStages(t.device, extraVisibility, extraEntry));
const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
for (let i = 0; i < maxedCount; i++) {
maxResourceBindings.push({
binding: i,
visibility: maxedVisibility,
...maxedEntry,
});
}
const goodDescriptor = { entries: maxResourceBindings };
// Control
t.device.createBindGroupLayout(goodDescriptor);
// Add an entry counting towards the same limit. It should produce a validation error.
const newDescriptor = clone(goodDescriptor);
newDescriptor.entries.push({
binding: maxedCount,
visibility: extraVisibility,
...extraEntry,
});
const newBindingCountsTowardSamePerStageLimit =
(maxedVisibility & extraVisibility) !== 0 &&
maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class;
t.expectValidationError(() => {
t.device.createBindGroupLayout(newDescriptor);
}, newBindingCountsTowardSamePerStageLimit);
});
// One pipeline layout can have a maximum number of each type of binding *per stage* (which is
// different for each type). Test that the max works, then add one more binding of same-or-different
// type and same-or-different visibility.
g.test('max_resources_per_stage,in_pipeline_layout')
.desc(
`
Test that the maximum number of bindings of a given type per-stage cannot be exceeded across
multiple bind group layouts when creating a pipeline layout.
- Test each binding type.
- Test that creation of a pipeline using the maximum number of bindings works.
- Test that creation of a pipeline using the maximum number of bindings + 1 fails.
`
)
.params(kMaxResourcesCases)
.fn(t => {
const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
const maxedTypeInfo = bindingTypeInfo(maxedEntry);
const maxedCount = getBindingLimitForBindingType(t.device, maxedVisibility, maxedEntry);
const extraTypeInfo = bindingTypeInfo(extraEntry);
t.skipIf(!isValidBGLEntryForStages(t.device, extraVisibility, extraEntry));
const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
for (let i = 0; i < maxedCount; i++) {
maxResourceBindings.push({
binding: i,
visibility: maxedVisibility,
...maxedEntry,
});
}
const goodLayout = t.device.createBindGroupLayout({ entries: maxResourceBindings });
// Control
t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout] });
const extraLayout = t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: extraVisibility,
...extraEntry,
},
],
});
// Some binding types use the same limit, e.g. 'storage-buffer' and 'readonly-storage-buffer'.
const newBindingCountsTowardSamePerStageLimit =
(maxedVisibility & extraVisibility) !== 0 &&
maxedTypeInfo.perStageLimitClass.class === extraTypeInfo.perStageLimitClass.class;
t.expectValidationError(() => {
t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout, extraLayout] });
}, newBindingCountsTowardSamePerStageLimit);
});
g.test('storage_texture,layout_dimension')
.desc(
`
Test that viewDimension is not cube or cube-array if storageTextureLayout is not undefined.
`
)
.params(u =>
u //
.combine('viewDimension', [undefined, ...kTextureViewDimensions])
)
.fn(t => {
const { viewDimension } = t.params;
const success = viewDimension !== 'cube' && viewDimension !== `cube-array`;
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
storageTexture: { format: 'rgba8unorm', viewDimension },
},
],
});
}, !success);
});
g.test('storage_texture,formats')
.desc(
`
Test that a validation error is generated if the format doesn't support the storage usage. A
validation error is also generated if the format doesn't support the 'read-write' storage access
when the storage access is 'read-write'.
`
)
.params(u =>
u //
.combine('format', kAllTextureFormats) //
.combine('access', kStorageTextureAccessValues)
)
.fn(t => {
const { format, access } = t.params;
t.skipIfTextureFormatNotSupported(format);
const success = isTextureFormatUsableWithStorageAccessMode(t.device.features, format, access);
t.expectValidationError(() => {
t.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
storageTexture: { format, access },
},
],
});
}, !success);
});