1export const description = `
2createBindGroupLayout validation tests.
3`;
4
5import { pbool, poptions, params } from '../../../common/framework/params_builder.js';
6import { makeTestGroup } from '../../../common/framework/test_group.js';
7import {
8  kBindingTypeInfo,
9  kBindingTypes,
10  kBufferBindingTypeInfo,
11  kMaxBindingsPerBindGroup,
12  kShaderStages,
13  kShaderStageCombinations,
14  kTextureBindingTypeInfo,
15  kTextureComponentTypes,
16  kTextureViewDimensions,
17  kTextureViewDimensionInfo,
18  kAllTextureFormats,
19  kAllTextureFormatInfo,
20} from '../../capability_info.js';
21
22import { ValidationTest } from './validation_test.js';
23
24function clone<T extends GPUBindGroupLayoutDescriptor>(descriptor: T): T {
25  return JSON.parse(JSON.stringify(descriptor));
26}
27
28export const g = makeTestGroup(ValidationTest);
29
30g.test('some_binding_index_was_specified_more_than_once').fn(async t => {
31  const goodDescriptor = {
32    entries: [
33      { binding: 0, visibility: GPUShaderStage.COMPUTE, type: 'storage-buffer' as const },
34      { binding: 1, visibility: GPUShaderStage.COMPUTE, type: 'storage-buffer' as const },
35    ],
36  };
37
38  // Control case
39  t.device.createBindGroupLayout(goodDescriptor);
40
41  const badDescriptor = clone(goodDescriptor);
42  badDescriptor.entries[1].binding = 0;
43
44  // Binding index 0 can't be specified twice.
45  t.expectValidationError(() => {
46    t.device.createBindGroupLayout(badDescriptor);
47  });
48});
49
50g.test('visibility')
51  .params(
52    params()
53      .combine(poptions('type', kBindingTypes))
54      .combine(poptions('visibility', kShaderStageCombinations))
55  )
56  .fn(async t => {
57    const { type, visibility } = t.params;
58
59    const success = (visibility & ~kBindingTypeInfo[type].validStages) === 0;
60
61    t.expectValidationError(() => {
62      t.device.createBindGroupLayout({
63        entries: [{ binding: 0, visibility, type }],
64      });
65    }, !success);
66  });
67
68g.test('bindingTypeSpecific_optional_members')
69  .params(
70    params()
71      .combine(poptions('type', kBindingTypes))
72      .combine([
73        // Case with every member set to `undefined`.
74        {
75          // Workaround for TS inferring the type of [ {}, ...x ] overly conservatively, as {}[].
76          _: 0,
77        },
78        // Cases with one member set.
79        ...pbool('hasDynamicOffset'),
80        ...poptions('minBufferBindingSize', [0, 4]),
81        ...poptions('textureComponentType', kTextureComponentTypes),
82        ...pbool('multisampled'),
83        ...poptions('viewDimension', kTextureViewDimensions),
84        ...poptions('storageTextureFormat', kAllTextureFormats),
85      ])
86  )
87  .fn(t => {
88    const {
89      type,
90      hasDynamicOffset,
91      minBufferBindingSize,
92      textureComponentType,
93      multisampled,
94      viewDimension,
95      storageTextureFormat,
96    } = t.params;
97
98    let success = true;
99    if (!(type in kBufferBindingTypeInfo)) {
100      success &&= hasDynamicOffset === undefined;
101      success &&= minBufferBindingSize === undefined;
102    }
103    if (!(type in kTextureBindingTypeInfo)) {
104      success &&= viewDimension === undefined;
105    }
106    if (kBindingTypeInfo[type].resource !== 'sampledTex') {
107      success &&= textureComponentType === undefined;
108      success &&= multisampled === undefined;
109    }
110    if (kBindingTypeInfo[type].resource !== 'storageTex') {
111      success &&= storageTextureFormat === undefined;
112    } else {
113      success &&= viewDimension === undefined || kTextureViewDimensionInfo[viewDimension].storage;
114      success &&=
115        storageTextureFormat === undefined || kAllTextureFormatInfo[storageTextureFormat].storage;
116    }
117
118    t.expectValidationError(() => {
119      t.device.createBindGroupLayout({
120        entries: [
121          {
122            binding: 0,
123            visibility: GPUShaderStage.COMPUTE,
124            type,
125            hasDynamicOffset,
126            minBufferBindingSize,
127            textureComponentType,
128            multisampled,
129            viewDimension,
130            storageTextureFormat,
131          },
132        ],
133      });
134    }, !success);
135  });
136
137g.test('multisample_requires_2d_view_dimension')
138  .params(
139    params()
140      .combine(poptions('multisampled', [undefined, false, true]))
141      .combine(poptions('viewDimension', [undefined, ...kTextureViewDimensions]))
142  )
143  .fn(async t => {
144    const { multisampled, viewDimension } = t.params;
145
146    const success = multisampled !== true || viewDimension === '2d' || viewDimension === undefined;
147
148    t.expectValidationError(() => {
149      t.device.createBindGroupLayout({
150        entries: [
151          {
152            binding: 0,
153            visibility: GPUShaderStage.COMPUTE,
154            type: 'sampled-texture',
155            multisampled,
156            viewDimension,
157          },
158        ],
159      });
160    }, !success);
161  });
162
163g.test('number_of_dynamic_buffers_exceeds_the_maximum_value')
164  .params([
165    { type: 'storage-buffer' as const, maxDynamicBufferCount: 4 },
166    { type: 'uniform-buffer' as const, maxDynamicBufferCount: 8 },
167  ])
168  .fn(async t => {
169    const { type, maxDynamicBufferCount } = t.params;
170
171    const maxDynamicBufferBindings: GPUBindGroupLayoutEntry[] = [];
172    for (let i = 0; i < maxDynamicBufferCount; i++) {
173      maxDynamicBufferBindings.push({
174        binding: i,
175        visibility: GPUShaderStage.COMPUTE,
176        type,
177        hasDynamicOffset: true,
178      });
179    }
180
181    const goodDescriptor = {
182      entries: [
183        ...maxDynamicBufferBindings,
184        {
185          binding: maxDynamicBufferBindings.length,
186          visibility: GPUShaderStage.COMPUTE,
187          type,
188          hasDynamicOffset: false,
189        },
190      ],
191    };
192
193    // Control case
194    t.device.createBindGroupLayout(goodDescriptor);
195
196    // Dynamic buffers exceed maximum in a bind group layout.
197    const badDescriptor = clone(goodDescriptor);
198    badDescriptor.entries[maxDynamicBufferCount].hasDynamicOffset = true;
199
200    t.expectValidationError(() => {
201      t.device.createBindGroupLayout(badDescriptor);
202    });
203  });
204
205// One bind group layout will be filled with kPerStageBindingLimit[...] of the type |type|.
206// For each item in the array returned here, a case will be generated which tests a pipeline
207// layout with one extra bind group layout with one extra binding. That extra binding will have:
208//
209//   - If extraTypeSame, any of the binding types which counts toward the same limit as |type|.
210//     (i.e. 'storage-buffer' <-> 'readonly-storage-buffer').
211//   - Otherwise, an arbitrary other type.
212function* pickExtraBindingTypes(
213  bindingType: GPUBindingType,
214  extraTypeSame: boolean
215): IterableIterator<GPUBindingType> {
216  const info = kBindingTypeInfo[bindingType];
217  if (extraTypeSame) {
218    for (const extraBindingType of kBindingTypes) {
219      if (
220        info.perStageLimitClass.class ===
221        kBindingTypeInfo[extraBindingType].perStageLimitClass.class
222      ) {
223        yield extraBindingType;
224      }
225    }
226  } else {
227    yield info.perStageLimitClass.class === 'sampler' ? 'sampled-texture' : 'sampler';
228  }
229}
230
231const kCasesForMaxResourcesPerStageTests = params()
232  .combine(poptions('maxedType', kBindingTypes))
233  .combine(poptions('maxedVisibility', kShaderStages))
234  .filter(p => (kBindingTypeInfo[p.maxedType].validStages & p.maxedVisibility) !== 0)
235  .expand(function* (p) {
236    for (const extraTypeSame of [true, false]) {
237      yield* poptions('extraType', pickExtraBindingTypes(p.maxedType, extraTypeSame));
238    }
239  })
240  .combine(poptions('extraVisibility', kShaderStages))
241  .filter(p => (kBindingTypeInfo[p.extraType].validStages & p.extraVisibility) !== 0);
242
243// Should never fail unless kMaxBindingsPerBindGroup is exceeded, because the validation for
244// resources-of-type-per-stage is in pipeline layout creation.
245g.test('max_resources_per_stage,in_bind_group_layout')
246  .params(kCasesForMaxResourcesPerStageTests)
247  .fn(async t => {
248    const { maxedType, extraType, maxedVisibility, extraVisibility } = t.params;
249    const maxedTypeInfo = kBindingTypeInfo[maxedType];
250    const maxedCount = maxedTypeInfo.perStageLimitClass.max;
251    const extraTypeInfo = kBindingTypeInfo[extraType];
252
253    const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
254    for (let i = 0; i < maxedCount; i++) {
255      maxResourceBindings.push({
256        binding: i,
257        visibility: maxedVisibility,
258        type: maxedType,
259        storageTextureFormat: maxedTypeInfo.resource === 'storageTex' ? 'rgba8unorm' : undefined,
260      });
261    }
262
263    const goodDescriptor = { entries: maxResourceBindings };
264
265    // Control
266    t.device.createBindGroupLayout(goodDescriptor);
267
268    const newDescriptor = clone(goodDescriptor);
269    newDescriptor.entries.push({
270      binding: maxedCount,
271      visibility: extraVisibility,
272      type: extraType,
273      storageTextureFormat: extraTypeInfo.resource === 'storageTex' ? 'rgba8unorm' : undefined,
274    });
275
276    const shouldError = maxedCount >= kMaxBindingsPerBindGroup;
277
278    t.expectValidationError(() => {
279      t.device.createBindGroupLayout(newDescriptor);
280    }, shouldError);
281  });
282
283// One pipeline layout can have a maximum number of each type of binding *per stage* (which is
284// different for each type). Test that the max works, then add one more binding of same-or-different
285// type and same-or-different visibility.
286g.test('max_resources_per_stage,in_pipeline_layout')
287  .params(kCasesForMaxResourcesPerStageTests)
288  .fn(async t => {
289    const { maxedType, extraType, maxedVisibility, extraVisibility } = t.params;
290    const maxedTypeInfo = kBindingTypeInfo[maxedType];
291    const maxedCount = maxedTypeInfo.perStageLimitClass.max;
292    const extraTypeInfo = kBindingTypeInfo[extraType];
293
294    const maxResourceBindings: GPUBindGroupLayoutEntry[] = [];
295    for (let i = 0; i < maxedCount; i++) {
296      maxResourceBindings.push({
297        binding: i,
298        visibility: maxedVisibility,
299        type: maxedType,
300        storageTextureFormat: maxedTypeInfo.resource === 'storageTex' ? 'rgba8unorm' : undefined,
301      });
302    }
303
304    const goodLayout = t.device.createBindGroupLayout({ entries: maxResourceBindings });
305
306    // Control
307    t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout] });
308
309    const extraLayout = t.device.createBindGroupLayout({
310      entries: [
311        {
312          binding: 0,
313          visibility: extraVisibility,
314          type: extraType,
315          storageTextureFormat: extraTypeInfo.resource === 'storageTex' ? 'rgba8unorm' : undefined,
316        },
317      ],
318    });
319
320    // Some binding types use the same limit, e.g. 'storage-buffer' and 'readonly-storage-buffer'.
321    const newBindingCountsTowardSamePerStageLimit =
322      (maxedVisibility & extraVisibility) !== 0 &&
323      kBindingTypeInfo[maxedType].perStageLimitClass.class ===
324        kBindingTypeInfo[extraType].perStageLimitClass.class;
325    const layoutExceedsPerStageLimit = newBindingCountsTowardSamePerStageLimit;
326
327    t.expectValidationError(() => {
328      t.device.createPipelineLayout({ bindGroupLayouts: [goodLayout, extraLayout] });
329    }, layoutExceedsPerStageLimit);
330  });
331