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