summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts456
1 files changed, 456 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts
new file mode 100644
index 0000000000..6971439598
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroupLayout.spec.ts
@@ -0,0 +1,456 @@
+export const description = `
+createBindGroupLayout validation tests.
+
+TODO: make sure tests are complete.
+`;
+
+import { kUnitCaseParamsBuilder } from '../../../common/framework/params_builder.js';
+import { makeTestGroup } from '../../../common/framework/test_group.js';
+import {
+ kAllTextureFormats,
+ kShaderStages,
+ kShaderStageCombinations,
+ kStorageTextureAccessValues,
+ kTextureFormatInfo,
+ kTextureSampleTypes,
+ kTextureViewDimensions,
+ allBindingEntries,
+ bindingTypeInfo,
+ bufferBindingTypeInfo,
+ kBufferBindingTypes,
+ BGLEntry,
+} from '../../capability_info.js';
+
+import { ValidationTest } from './validation_test.js';
+
+function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
+ return JSON.parse(JSON.stringify(descriptor));
+}
+
+export const g = makeTestGroup(ValidationTest);
+
+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(async 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);
+ });
+
+// MAINTENANCE_TODO: Move this into kLimits with the proper name after the spec PR lands.
+// https://github.com/gpuweb/gpuweb/pull/3318
+const kMaxBindingsPerBindGroup = 640;
+
+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('binding', [1, 4, 8, 256, kMaxBindingsPerBindGroup - 1, kMaxBindingsPerBindGroup])
+ )
+ .fn(async t => {
+ const { binding } = t.params;
+ const entries: Array<GPUBindGroupLayoutEntry> = [];
+
+ entries.push({
+ binding,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: 'storage' as const },
+ });
+
+ const success = binding < kMaxBindingsPerBindGroup;
+
+ 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(async t => {
+ const { visibility, entry } = t.params;
+ const info = bindingTypeInfo(entry);
+
+ const success = (visibility & ~info.validStages) === 0;
+
+ 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(async t => {
+ const { shaderStage, type } = t.params;
+
+ const success = !(type === 'storage' && shaderStage & GPUShaderStage.VERTEX);
+
+ 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(async t => {
+ const { shaderStage, access } = t.params;
+
+ const success = !(
+ (access ?? 'write-only') === 'write-only' && shaderStage & GPUShaderStage.VERTEX
+ );
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: shaderStage,
+ storageTexture: { access, format: 'rgba8unorm' },
+ },
+ ],
+ });
+ }, !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(async 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(async t => {
+ const { type, extraDynamicBuffers, staticBuffers } = t.params;
+ const info = bufferBindingTypeInfo({ type });
+
+ const dynamicBufferCount = info.perPipelineLimitClass.maxDynamic + extraDynamicBuffers;
+
+ 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);
+ });
+
+/**
+ * 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 kMaxBindingsPerBindGroup 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(async t => {
+ const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
+ const maxedTypeInfo = bindingTypeInfo(maxedEntry);
+ const maxedCount = maxedTypeInfo.perStageLimitClass.max;
+ const extraTypeInfo = bindingTypeInfo(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(async t => {
+ const { maxedEntry, extraEntry, maxedVisibility, extraVisibility } = t.params;
+ const maxedTypeInfo = bindingTypeInfo(maxedEntry);
+ const maxedCount = maxedTypeInfo.perStageLimitClass.max;
+ const extraTypeInfo = bindingTypeInfo(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(async 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.
+
+ TODO: Test "bgra8unorm" with the "bgra8unorm-storage" feature.
+ `
+ )
+ .params(u => u.combine('format', kAllTextureFormats))
+ .beforeAllSubcases(t => {
+ t.selectDeviceForTextureFormatOrSkipTestCase(t.params.format);
+ })
+ .fn(async t => {
+ const { format } = t.params;
+ const info = kTextureFormatInfo[format];
+
+ t.expectValidationError(() => {
+ t.device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: { format },
+ },
+ ],
+ });
+ }, !info.storage);
+ });