diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts | 1131 |
1 files changed, 1131 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts new file mode 100644 index 0000000000..2815cddcc6 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts @@ -0,0 +1,1131 @@ +export const description = ` + createBindGroup validation tests. + + TODO: Ensure sure tests cover all createBindGroup validation rules. +`; + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { assert, unreachable } from '../../../common/util/util.js'; +import { + allBindingEntries, + bindingTypeInfo, + bufferBindingEntries, + bufferBindingTypeInfo, + kAllTextureFormats, + kBindableResources, + kBufferBindingTypes, + kBufferUsages, + kCompareFunctions, + kLimitInfo, + kSamplerBindingTypes, + kTextureFormatInfo, + kTextureUsages, + kTextureViewDimensions, + sampledAndStorageBindingEntries, + texBindingTypeInfo, +} from '../../capability_info.js'; +import { GPUConst } from '../../constants.js'; +import { kResourceStates } from '../../gpu_test.js'; +import { getTextureDimensionFromView } from '../../util/texture/base.js'; + +import { ValidationTest } from './validation_test.js'; + +function clone<T extends GPUTextureDescriptor>(descriptor: T): T { + return JSON.parse(JSON.stringify(descriptor)); +} + +export const g = makeTestGroup(ValidationTest); + +const kStorageTextureFormats = kAllTextureFormats.filter(f => kTextureFormatInfo[f].storage); + +g.test('binding_count_mismatch') + .desc('Test that the number of entries must match the number of entries in the BindGroupLayout.') + .paramsSubcasesOnly(u => + u // + .combine('layoutEntryCount', [1, 2, 3]) + .combine('bindGroupEntryCount', [1, 2, 3]) + ) + .fn(async t => { + const { layoutEntryCount, bindGroupEntryCount } = t.params; + + const layoutEntries: Array<GPUBindGroupLayoutEntry> = []; + for (let i = 0; i < layoutEntryCount; ++i) { + layoutEntries.push({ + binding: i, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'storage' }, + }); + } + const bindGroupLayout = t.device.createBindGroupLayout({ entries: layoutEntries }); + + const entries: Array<GPUBindGroupEntry> = []; + for (let i = 0; i < bindGroupEntryCount; ++i) { + entries.push({ + binding: i, + resource: { buffer: t.getStorageBuffer() }, + }); + } + + const shouldError = layoutEntryCount !== bindGroupEntryCount; + t.expectValidationError(() => { + t.device.createBindGroup({ + entries, + layout: bindGroupLayout, + }); + }, shouldError); + }); + +g.test('binding_must_be_present_in_layout') + .desc( + 'Test that the binding slot for each entry matches a binding slot defined in the BindGroupLayout.' + ) + .paramsSubcasesOnly(u => + u // + .combine('layoutBinding', [0, 1, 2]) + .combine('binding', [0, 1, 2]) + ) + .fn(async t => { + const { layoutBinding, binding } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { binding: layoutBinding, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + ], + }); + + const descriptor = { + entries: [{ binding, resource: { buffer: t.getStorageBuffer() } }], + layout: bindGroupLayout, + }; + + const shouldError = layoutBinding !== binding; + t.expectValidationError(() => { + t.device.createBindGroup(descriptor); + }, shouldError); + }); + +g.test('binding_must_contain_resource_defined_in_layout') + .desc( + 'Test that only compatible resource types specified in the BindGroupLayout are allowed for each entry.' + ) + .paramsSubcasesOnly(u => + u // + .combine('resourceType', kBindableResources) + .combine('entry', allBindingEntries(false)) + ) + .fn(t => { + const { resourceType, entry } = t.params; + const info = bindingTypeInfo(entry); + + const layout = t.device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: GPUShaderStage.COMPUTE, ...entry }], + }); + + const resource = t.getBindingResource(resourceType); + + let resourceBindingIsCompatible; + switch (info.resource) { + // Either type of sampler may be bound to a filtering sampler binding. + case 'filtSamp': + resourceBindingIsCompatible = resourceType === 'filtSamp' || resourceType === 'nonFiltSamp'; + break; + // But only non-filtering samplers can be used with non-filtering sampler bindings. + case 'nonFiltSamp': + resourceBindingIsCompatible = resourceType === 'nonFiltSamp'; + break; + default: + resourceBindingIsCompatible = info.resource === resourceType; + break; + } + t.expectValidationError(() => { + t.device.createBindGroup({ layout, entries: [{ binding: 0, resource }] }); + }, !resourceBindingIsCompatible); + }); + +g.test('texture_binding_must_have_correct_usage') + .desc('Tests that texture bindings must have the correct usage.') + .paramsSubcasesOnly(u => + u // + .combine('entry', sampledAndStorageBindingEntries(false)) + .combine('usage', kTextureUsages) + .unless(({ entry, usage }) => { + const info = texBindingTypeInfo(entry); + // Can't create the texture for this (usage=STORAGE_BINDING and sampleCount=4), so skip. + return usage === GPUConst.TextureUsage.STORAGE_BINDING && info.resource === 'sampledTexMS'; + }) + ) + .fn(async t => { + const { entry, usage } = t.params; + const info = texBindingTypeInfo(entry); + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, ...entry }], + }); + + // The `RENDER_ATTACHMENT` usage must be specified if sampleCount > 1 according to WebGPU SPEC. + const appliedUsage = + info.resource === 'sampledTexMS' ? usage | GPUConst.TextureUsage.RENDER_ATTACHMENT : usage; + + const descriptor = { + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm' as const, + usage: appliedUsage, + sampleCount: info.resource === 'sampledTexMS' ? 4 : 1, + }; + const resource = t.device.createTexture(descriptor).createView(); + + const shouldError = (usage & info.usage) === 0; + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource }], + layout: bindGroupLayout, + }); + }, shouldError); + }); + +g.test('texture_must_have_correct_component_type') + .desc( + ` + Tests that texture bindings must have a format that matches the sample type specified in the BindGroupLayout. + - Tests a compatible format for every sample type + - Tests an incompatible format for every sample type` + ) + .params(u => u.combine('sampleType', ['float', 'sint', 'uint'] as const)) + .fn(async t => { + const { sampleType } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: { sampleType }, + }, + ], + }); + + let format: GPUTextureFormat; + if (sampleType === 'float') { + format = 'r8unorm'; + } else if (sampleType === 'sint') { + format = 'r8sint'; + } else if (sampleType === 'uint') { + format = 'r8uint'; + } else { + unreachable('Unexpected texture component type'); + } + + const goodDescriptor = { + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format, + usage: GPUTextureUsage.TEXTURE_BINDING, + }; + + // Control case + t.device.createBindGroup({ + entries: [ + { + binding: 0, + resource: t.device.createTexture(goodDescriptor).createView(), + }, + ], + layout: bindGroupLayout, + }); + + function* mismatchedTextureFormats(): Iterable<GPUTextureFormat> { + if (sampleType !== 'float') { + yield 'r8unorm'; + } + if (sampleType !== 'sint') { + yield 'r8sint'; + } + if (sampleType !== 'uint') { + yield 'r8uint'; + } + } + + // Mismatched texture binding formats are not valid. + for (const mismatchedTextureFormat of mismatchedTextureFormats()) { + const badDescriptor: GPUTextureDescriptor = clone(goodDescriptor); + badDescriptor.format = mismatchedTextureFormat; + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: t.device.createTexture(badDescriptor).createView() }], + layout: bindGroupLayout, + }); + }); + } + }); + +g.test('texture_must_have_correct_dimension') + .desc( + ` + Test that bound texture views match the dimensions supplied in the BindGroupLayout + - Test for every GPUTextureViewDimension + - Test for both TEXTURE_BINDING and STORAGE_BINDING. + ` + ) + .params(u => + u + .combine('usage', [ + GPUConst.TextureUsage.TEXTURE_BINDING, + GPUConst.TextureUsage.STORAGE_BINDING, + ]) + .combine('viewDimension', kTextureViewDimensions) + .unless( + p => + p.usage === GPUConst.TextureUsage.STORAGE_BINDING && + (p.viewDimension === 'cube' || p.viewDimension === 'cube-array') + ) + .beginSubcases() + .combine('dimension', kTextureViewDimensions) + ) + .fn(async t => { + const { usage, viewDimension, dimension } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + usage === GPUTextureUsage.TEXTURE_BINDING + ? { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: { viewDimension }, + } + : { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + storageTexture: { access: 'write-only', format: 'rgba8unorm', viewDimension }, + }, + ], + }); + + let height = 16; + let depthOrArrayLayers = 6; + if (dimension === '1d') { + height = 1; + depthOrArrayLayers = 1; + } + + const texture = t.device.createTexture({ + size: { width: 16, height, depthOrArrayLayers }, + format: 'rgba8unorm' as const, + usage, + dimension: getTextureDimensionFromView(dimension), + }); + + const shouldError = viewDimension !== dimension; + const textureView = texture.createView({ dimension }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: textureView }], + layout: bindGroupLayout, + }); + }, shouldError); + }); + +g.test('multisampled_validation') + .desc( + ` + Test that the sample count of the texture is greater than 1 if the BindGroup entry's + multisampled is true. Otherwise, the texture's sampleCount should be 1. + ` + ) + .params(u => + u // + .combine('multisampled', [true, false]) + .beginSubcases() + .combine('sampleCount', [1, 4]) + ) + .fn(async t => { + const { multisampled, sampleCount } = t.params; + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: { multisampled }, + }, + ], + }); + + const texture = t.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm' as const, + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount, + }); + + const isValid = (!multisampled && sampleCount === 1) || (multisampled && sampleCount > 1); + + const textureView = texture.createView(); + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: textureView }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('buffer_offset_and_size_for_bind_groups_match') + .desc( + ` + Test that a buffer binding's [offset, offset + size) must be contained in the BindGroup entry's buffer. + - Test for various offsets and sizes` + ) + .paramsSubcasesOnly([ + { offset: 0, size: 512, _success: true }, // offset 0 is valid + { offset: 256, size: 256, _success: true }, // offset 256 (aligned) is valid + + // Touching the end of the buffer + { offset: 0, size: 1024, _success: true }, + { offset: 0, size: undefined, _success: true }, + { offset: 256 * 3, size: 256, _success: true }, + { offset: 256 * 3, size: undefined, _success: true }, + + // Zero-sized bindings + { offset: 0, size: 0, _success: false }, + { offset: 256, size: 0, _success: false }, + { offset: 1024, size: 0, _success: false }, + { offset: 1024, size: undefined, _success: false }, + + // Unaligned buffer offset is invalid + { offset: 1, size: 256, _success: false }, + { offset: 1, size: undefined, _success: false }, + { offset: 128, size: 256, _success: false }, + { offset: 255, size: 256, _success: false }, + + // Out-of-bounds + { offset: 256 * 5, size: 0, _success: false }, // offset is OOB + { offset: 0, size: 256 * 5, _success: false }, // size is OOB + { offset: 1024, size: 1, _success: false }, // offset+size is OOB + ]) + .fn(async t => { + const { offset, size, _success } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }], + }); + + const buffer = t.device.createBuffer({ + size: 1024, + usage: GPUBufferUsage.STORAGE, + }); + + const descriptor = { + entries: [ + { + binding: 0, + resource: { buffer, offset, size }, + }, + ], + layout: bindGroupLayout, + }; + + if (_success) { + // Control case + t.device.createBindGroup(descriptor); + } else { + // Buffer offset and/or size don't match in bind groups. + t.expectValidationError(() => { + t.device.createBindGroup(descriptor); + }); + } + }); + +g.test('minBindingSize') + .desc('Tests that minBindingSize is correctly enforced.') + .paramsSubcasesOnly(u => + u // + .combine('minBindingSize', [undefined, 4, 8, 256]) + .expand('size', ({ minBindingSize }) => + minBindingSize !== undefined + ? [minBindingSize - 4, minBindingSize, minBindingSize + 4] + : [4, 256] + ) + ) + .fn(t => { + const { size, minBindingSize } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + type: 'storage', + minBindingSize, + }, + }, + ], + }); + + const storageBuffer = t.device.createBuffer({ + size, + usage: GPUBufferUsage.STORAGE, + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: storageBuffer, + }, + }, + ], + }); + }, minBindingSize !== undefined && size < minBindingSize); + }); + +g.test('buffer,resource_state') + .desc('Test bind group creation with various buffer resource states') + .paramsSubcasesOnly(u => + u.combine('state', kResourceStates).combine('entry', bufferBindingEntries(true)) + ) + .fn(t => { + const { state, entry } = t.params; + + assert(entry.buffer !== undefined); + const info = bufferBindingTypeInfo(entry.buffer); + + const bgl = t.device.createBindGroupLayout({ + entries: [ + { + ...entry, + binding: 0, + visibility: info.validStages, + }, + ], + }); + + const buffer = t.createBufferWithState(state, { + usage: info.usage, + size: 4, + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bgl, + entries: [ + { + binding: 0, + resource: { + buffer, + }, + }, + ], + }); + }, state === 'invalid'); + }); + +g.test('texture,resource_state') + .desc('Test bind group creation with various texture resource states') + .paramsSubcasesOnly(u => + u + .combine('state', kResourceStates) + .combine('entry', sampledAndStorageBindingEntries(true, 'rgba8unorm')) + ) + .fn(t => { + const { state, entry } = t.params; + const info = texBindingTypeInfo(entry); + + const bgl = t.device.createBindGroupLayout({ + entries: [ + { + ...entry, + binding: 0, + visibility: info.validStages, + }, + ], + }); + + // The `RENDER_ATTACHMENT` usage must be specified if sampleCount > 1 according to WebGPU SPEC. + const usage = entry.texture?.multisampled + ? info.usage | GPUConst.TextureUsage.RENDER_ATTACHMENT + : info.usage; + const texture = t.createTextureWithState(state, { + usage, + size: [1, 1], + format: 'rgba8unorm', + sampleCount: entry.texture?.multisampled ? 4 : 1, + }); + + let textureView: GPUTextureView; + t.expectValidationError(() => { + textureView = texture.createView(); + }, state === 'invalid'); + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bgl, + entries: [ + { + binding: 0, + resource: textureView, + }, + ], + }); + }, state === 'invalid'); + }); + +g.test('bind_group_layout,device_mismatch') + .desc( + 'Tests createBindGroup cannot be called with a bind group layout created from another device' + ) + .paramsSubcasesOnly(u => u.combine('mismatched', [true, false])) + .beforeAllSubcases(t => { + t.selectMismatchedDeviceOrSkipTestCase(undefined); + }) + .fn(async t => { + const mismatched = t.params.mismatched; + + const sourceDevice = mismatched ? t.mismatchedDevice : t.device; + + const bgl = sourceDevice.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUConst.ShaderStage.VERTEX, + buffer: {}, + }, + ], + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bgl, + entries: [ + { + binding: 0, + resource: { buffer: t.getUniformBuffer() }, + }, + ], + }); + }, mismatched); + }); + +g.test('binding_resources,device_mismatch') + .desc( + ` + Tests createBindGroup cannot be called with various resources created from another device + Test with two resources to make sure all resources can be validated: + - resource0 and resource1 from same device + - resource0 and resource1 from different device + + TODO: test GPUExternalTexture as a resource + ` + ) + .params(u => + u + .combine('entry', [ + { buffer: { type: 'storage' } }, + { sampler: { type: 'filtering' } }, + { texture: { multisampled: false } }, + { storageTexture: { access: 'write-only', format: 'rgba8unorm' } }, + ] as const) + .beginSubcases() + .combineWithParams([ + { resource0Mismatched: false, resource1Mismatched: false }, //control case + { resource0Mismatched: true, resource1Mismatched: false }, + { resource0Mismatched: false, resource1Mismatched: true }, + ]) + ) + .beforeAllSubcases(t => { + t.selectMismatchedDeviceOrSkipTestCase(undefined); + }) + .fn(async t => { + const { entry, resource0Mismatched, resource1Mismatched } = t.params; + + const info = bindingTypeInfo(entry); + + const resource0 = resource0Mismatched + ? t.getDeviceMismatchedBindingResource(info.resource) + : t.getBindingResource(info.resource); + const resource1 = resource1Mismatched + ? t.getDeviceMismatchedBindingResource(info.resource) + : t.getBindingResource(info.resource); + + const bgl = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: info.validStages, + ...entry, + }, + { + binding: 1, + visibility: info.validStages, + ...entry, + }, + ], + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bgl, + entries: [ + { + binding: 0, + resource: resource0, + }, + { + binding: 1, + resource: resource1, + }, + ], + }); + }, resource0Mismatched || resource1Mismatched); + }); + +g.test('storage_texture,usage') + .desc( + ` + Test that the texture usage contains STORAGE_BINDING if the BindGroup entry defines + storageTexture. + ` + ) + .params(u => + u // + // If usage0 and usage1 are the same, the usage being test is a single usage. Otherwise, it's + // a combined usage. + .combine('usage0', kTextureUsages) + .combine('usage1', kTextureUsages) + ) + .fn(async t => { + const { usage0, usage1 } = t.params; + + const usage = usage0 | usage1; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + storageTexture: { access: 'write-only', format: 'rgba8unorm' }, + }, + ], + }); + + const texture = t.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm' as const, + usage, + }); + + const isValid = GPUTextureUsage.STORAGE_BINDING & usage; + + const textureView = texture.createView(); + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: textureView }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('storage_texture,mip_level_count') + .desc( + ` + Test that the mip level count of the resource of the BindGroup entry as a descriptor is 1 if the + BindGroup entry defines storageTexture. If the mip level count is not 1, a validation error + should be generated. + ` + ) + .params(u => + u // + .combine('baseMipLevel', [1, 2]) + .combine('mipLevelCount', [1, 2]) + ) + .fn(async t => { + const { baseMipLevel, mipLevelCount } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + storageTexture: { access: 'write-only', format: 'rgba8unorm' }, + }, + ], + }); + + const MIP_LEVEL_COUNT = 4; + const texture = t.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm' as const, + usage: GPUTextureUsage.STORAGE_BINDING, + mipLevelCount: MIP_LEVEL_COUNT, + }); + + const textureView = texture.createView({ baseMipLevel, mipLevelCount }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: textureView }], + layout: bindGroupLayout, + }); + }, mipLevelCount !== 1); + }); + +g.test('storage_texture,format') + .desc( + ` + Test that the format of the storage texture is equal to resource's descriptor format if the + BindGroup entry defines storageTexture. + ` + ) + .params(u => + u // + .combine('storageTextureFormat', kStorageTextureFormats) + .combine('resourceFormat', kStorageTextureFormats) + ) + .fn(async t => { + const { storageTextureFormat, resourceFormat } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + storageTexture: { access: 'write-only', format: storageTextureFormat }, + }, + ], + }); + + const texture = t.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: resourceFormat, + usage: GPUTextureUsage.STORAGE_BINDING, + }); + + const isValid = storageTextureFormat === resourceFormat; + const textureView = texture.createView({ format: resourceFormat }); + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: textureView }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('buffer,usage') + .desc( + ` + Test that the buffer usage contains 'UNIFORM' if the BindGroup entry defines buffer and it's + type is 'uniform', and the buffer usage contains 'STORAGE' if the BindGroup entry's buffer type + is 'storage'|read-only-storage'. + ` + ) + .params(u => + u // + .combine('type', kBufferBindingTypes) + // If usage0 and usage1 are the same, the usage being test is a single usage. Otherwise, it's + // a combined usage. + .beginSubcases() + .combine('usage0', kBufferUsages) + .combine('usage1', kBufferUsages) + .unless( + ({ usage0, usage1 }) => + ((usage0 | usage1) & (GPUConst.BufferUsage.MAP_READ | GPUConst.BufferUsage.MAP_WRITE)) !== + 0 + ) + ) + .fn(async t => { + const { type, usage0, usage1 } = t.params; + + const usage = usage0 | usage1; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type }, + }, + ], + }); + + const buffer = t.device.createBuffer({ + size: 4, + usage, + }); + + let isValid = false; + if (type === 'uniform') { + isValid = GPUBufferUsage.UNIFORM & usage ? true : false; + } else if (type === 'storage' || type === 'read-only-storage') { + isValid = GPUBufferUsage.STORAGE & usage ? true : false; + } + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: { buffer } }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('buffer,resource_offset') + .desc( + ` + Test that the resource.offset of the BindGroup entry is a multiple of limits. + 'minUniformBufferOffsetAlignment|minStorageBufferOffsetAlignment' if the BindGroup entry defines + buffer and the buffer type is 'uniform|storage|read-only-storage'. + ` + ) + .params(u => + u // + .combine('type', kBufferBindingTypes) + .beginSubcases() + .expand('offset', ({ type }) => + type === 'uniform' + ? [ + kLimitInfo.minUniformBufferOffsetAlignment.default, + kLimitInfo.minUniformBufferOffsetAlignment.default * 0.5, + kLimitInfo.minUniformBufferOffsetAlignment.default * 1.5, + kLimitInfo.minUniformBufferOffsetAlignment.default + 2, + ] + : [ + kLimitInfo.minStorageBufferOffsetAlignment.default, + kLimitInfo.minStorageBufferOffsetAlignment.default * 0.5, + kLimitInfo.minStorageBufferOffsetAlignment.default * 1.5, + kLimitInfo.minStorageBufferOffsetAlignment.default + 2, + ] + ) + ) + .fn(async t => { + const { type, offset } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type }, + }, + ], + }); + + let usage, isValid; + if (type === 'uniform') { + usage = GPUBufferUsage.UNIFORM; + isValid = offset % kLimitInfo.minUniformBufferOffsetAlignment.default === 0; + } else { + usage = GPUBufferUsage.STORAGE; + isValid = offset % kLimitInfo.minStorageBufferOffsetAlignment.default === 0; + } + + const buffer = t.device.createBuffer({ + size: 1024, + usage, + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: { buffer, offset } }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('buffer,resource_binding_size') + .desc( + ` + Test that the buffer binding size of the BindGroup entry is equal to or less than limits. + 'maxUniformBufferBindingSize|maxStorageBufferBindingSize' if the BindGroup entry defines + buffer and the buffer type is 'uniform|storage|read-only-storage'. + ` + ) + .params(u => + u + .combine('type', kBufferBindingTypes) + .beginSubcases() + // Test a size of 1 (for uniform buffer) or 4 (for storage and read-only storage buffer) + // then values just within and just above the limit. + .expand('bindingSize', ({ type }) => + type === 'uniform' + ? [ + 1, + kLimitInfo.maxUniformBufferBindingSize.default, + kLimitInfo.maxUniformBufferBindingSize.default + 1, + ] + : [ + 4, + kLimitInfo.maxStorageBufferBindingSize.default, + kLimitInfo.maxStorageBufferBindingSize.default + 4, + ] + ) + ) + .fn(async t => { + const { type, bindingSize } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type }, + }, + ], + }); + + let usage, isValid; + if (type === 'uniform') { + usage = GPUBufferUsage.UNIFORM; + isValid = bindingSize <= kLimitInfo.maxUniformBufferBindingSize.default; + } else { + usage = GPUBufferUsage.STORAGE; + isValid = bindingSize <= kLimitInfo.maxStorageBufferBindingSize.default; + } + + const buffer = t.device.createBuffer({ + size: kLimitInfo.maxStorageBufferBindingSize.default, + usage, + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: { buffer, size: bindingSize } }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('buffer,effective_buffer_binding_size') + .desc( + ` + Test that the effective buffer binding size of the BindGroup entry must be a multiple of 4 if the + buffer type is 'storage|read-only-storage', while there is no such restriction on uniform buffers. +` + ) + .params(u => + u + .combine('type', kBufferBindingTypes) + .beginSubcases() + .expand('offset', ({ type }) => + type === 'uniform' + ? [0, kLimitInfo.minUniformBufferOffsetAlignment.default] + : [0, kLimitInfo.minStorageBufferOffsetAlignment.default] + ) + .expand('bufferSize', ({ type }) => + type === 'uniform' + ? [ + kLimitInfo.minUniformBufferOffsetAlignment.default + 8, + kLimitInfo.minUniformBufferOffsetAlignment.default + 10, + ] + : [ + kLimitInfo.minStorageBufferOffsetAlignment.default + 8, + kLimitInfo.minStorageBufferOffsetAlignment.default + 10, + ] + ) + .combine('bindingSize', [undefined, 2, 4, 6]) + ) + .fn(async t => { + const { type, offset, bufferSize, bindingSize } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type }, + }, + ], + }); + + const effectiveBindingSize = bindingSize ?? bufferSize - offset; + let usage, isValid; + if (type === 'uniform') { + usage = GPUBufferUsage.UNIFORM; + isValid = true; + } else { + usage = GPUBufferUsage.STORAGE; + isValid = effectiveBindingSize % 4 === 0; + } + + const buffer = t.device.createBuffer({ + size: bufferSize, + usage, + }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: { buffer, offset, size: bindingSize } }], + layout: bindGroupLayout, + }); + }, !isValid); + }); + +g.test('sampler,device_mismatch') + .desc(`Tests createBindGroup cannot be called with a sampler created from another device.`) + .paramsSubcasesOnly(u => u.combine('mismatched', [true, false])) + .beforeAllSubcases(t => { + t.selectMismatchedDeviceOrSkipTestCase(undefined); + }) + .fn(async t => { + const { mismatched } = t.params; + + const sourceDevice = mismatched ? t.mismatchedDevice : t.device; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { type: 'filtering' as const }, + }, + ], + }); + + const sampler = sourceDevice.createSampler(); + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: sampler }], + layout: bindGroupLayout, + }); + }, mismatched); + }); + +g.test('sampler,compare_function_with_binding_type') + .desc( + ` + Test that the sampler of the BindGroup has a 'compareFunction' value if the sampler type of the + BindGroupLayout is 'comparison'. Other sampler types should not have 'compare' field in + the descriptor of the sampler. + ` + ) + .params(u => + u // + .combine('bgType', kSamplerBindingTypes) + .beginSubcases() + .combine('compareFunction', [undefined, ...kCompareFunctions]) + ) + .fn(async t => { + const { bgType, compareFunction } = t.params; + + const bindGroupLayout = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + sampler: { type: bgType }, + }, + ], + }); + + const isValid = + bgType === 'comparison' ? compareFunction !== undefined : compareFunction === undefined; + + const sampler = t.device.createSampler({ compare: compareFunction }); + + t.expectValidationError(() => { + t.device.createBindGroup({ + entries: [{ binding: 0, resource: sampler }], + layout: bindGroupLayout, + }); + }, !isValid); + }); |