summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/createBindGroup.spec.ts
diff options
context:
space:
mode:
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.ts1131
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);
+ });