export const description = ` createComputePipeline and createComputePipelineAsync validation tests. Note: entry point matching tests are in shader_module/entry_point.spec.ts `; import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js'; import { makeTestGroup } from '../../../common/framework/test_group.js'; import { keysOf } from '../../../common/util/data_tables.js'; import { isTextureFormatUsableWithStorageAccessMode, kPossibleStorageTextureFormats, } from '../../format_info.js'; import { kValue } from '../../util/constants.js'; import { TShaderStage, getShaderWithEntryPoint } from '../../util/shader.js'; import { kAPIResources, getWGSLShaderForResource, getAPIBindGroupLayoutForResource, doResourcesMatch, } from './utils.js'; import * as vtu from './validation_test_utils.js'; class F extends AllFeaturesMaxLimitsGPUTest { getShaderModule( shaderStage: TShaderStage = 'compute', entryPoint: string = 'main' ): GPUShaderModule { return this.device.createShaderModule({ code: getShaderWithEntryPoint(shaderStage, entryPoint), }); } } export const g = makeTestGroup(F); g.test('basic') .desc( ` Control case for createComputePipeline and createComputePipelineAsync. Call the API with valid compute shader and matching valid entryPoint, making sure that the test function working well. ` ) .params(u => u.combine('isAsync', [true, false])) .fn(t => { const { isAsync } = t.params; vtu.doCreateComputePipelineTest(t, isAsync, true, { layout: 'auto', compute: { module: t.getShaderModule('compute', 'main'), entryPoint: 'main' }, }); }); g.test('shader_module,invalid') .desc( ` Tests calling createComputePipeline(Async) with a invalid compute shader, and check that the APIs catch this error. ` ) .params(u => u.combine('isAsync', [true, false])) .fn(t => { const { isAsync } = t.params; vtu.doCreateComputePipelineTest(t, isAsync, false, { layout: 'auto', compute: { module: vtu.createInvalidShaderModule(t), entryPoint: 'main', }, }); }); g.test('shader_module,compute') .desc( ` Tests calling createComputePipeline(Async) with valid but different stage shader and matching entryPoint, and check that the APIs only accept compute shader. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('shaderModuleStage', ['compute', 'vertex', 'fragment'] as TShaderStage[]) ) .fn(t => { const { isAsync, shaderModuleStage } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.getShaderModule(shaderModuleStage, 'main'), entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest(t, isAsync, shaderModuleStage === 'compute', descriptor); }); g.test('shader_module,device_mismatch') .desc( 'Tests createComputePipeline(Async) cannot be called with a shader module created from another device' ) .paramsSubcasesOnly(u => u.combine('isAsync', [true, false]).combine('mismatched', [true, false])) .beforeAllSubcases(t => t.usesMismatchedDevice()) .fn(t => { const { isAsync, mismatched } = t.params; const sourceDevice = mismatched ? t.mismatchedDevice : t.device; const module = sourceDevice.createShaderModule({ code: '@compute @workgroup_size(1) fn main() {}', }); const descriptor = { layout: 'auto' as const, compute: { module, entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest(t, isAsync, !mismatched, descriptor); }); g.test('pipeline_layout,device_mismatch') .desc( 'Tests createComputePipeline(Async) cannot be called with a pipeline layout created from another device' ) .paramsSubcasesOnly(u => u.combine('isAsync', [true, false]).combine('mismatched', [true, false])) .beforeAllSubcases(t => t.usesMismatchedDevice()) .fn(t => { const { isAsync, mismatched } = t.params; const sourceDevice = mismatched ? t.mismatchedDevice : t.device; const layout = sourceDevice.createPipelineLayout({ bindGroupLayouts: [] }); const descriptor = { layout, compute: { module: t.getShaderModule('compute', 'main'), entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest(t, isAsync, !mismatched, descriptor); }); g.test('limits,workgroup_storage_size') .desc( ` Tests calling createComputePipeline(Async) validation for compute using <= device.limits.maxComputeWorkgroupStorageSize bytes of workgroup storage. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { type: 'vec4', _typeSize: 16 }, { type: 'mat4x4', _typeSize: 64 }, ]) .beginSubcases() .combine('countDeltaFromLimit', [0, 1]) ) .fn(t => { const { isAsync, type, _typeSize, countDeltaFromLimit } = t.params; const countAtLimit = Math.floor(t.device.limits.maxComputeWorkgroupStorageSize / _typeSize); const count = countAtLimit + countDeltaFromLimit; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` var data: array<${type}, ${count}>; @compute @workgroup_size(64) fn main () { _ = data; } `, }), entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest(t, isAsync, count <= countAtLimit, descriptor); }); g.test('limits,invocations_per_workgroup') .desc( ` Tests calling createComputePipeline(Async) validation for compute using <= device.limits.maxComputeInvocationsPerWorkgroup per workgroup. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('size', [ // Assume maxComputeWorkgroupSizeX/Y >= 129, maxComputeWorkgroupSizeZ >= 33 [128, 1, 2], [129, 1, 2], [2, 128, 1], [2, 129, 1], [1, 8, 32], [1, 8, 33], ]) ) .fn(t => { const { isAsync, size } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` @compute @workgroup_size(${size.join(',')}) fn main () { } `, }), entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest( t, isAsync, size[0] * size[1] * size[2] <= t.device.limits.maxComputeInvocationsPerWorkgroup, descriptor ); }); g.test('limits,invocations_per_workgroup,each_component') .desc( ` Tests calling createComputePipeline(Async) validation for compute workgroup_size attribute has each component no more than their limits. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('size', [ // Assume maxComputeInvocationsPerWorkgroup >= 256 [64], [256, 1, 1], [257, 1, 1], [1, 256, 1], [1, 257, 1], [1, 1, 63], [1, 1, 64], [1, 1, 65], ] as const) ) .fn(t => { const { isAsync, size } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` @compute @workgroup_size(${size.join(',')}) fn main () { } `, }), entryPoint: 'main', }, }; const workgroupX = size[0]; const workgroupY = size[1] ?? 1; const workgroupZ = size[2] ?? 1; const _success = workgroupX <= t.device.limits.maxComputeWorkgroupSizeX && workgroupY <= t.device.limits.maxComputeWorkgroupSizeY && workgroupZ <= t.device.limits.maxComputeWorkgroupSizeZ; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); g.test('overrides,identifier') .desc( ` Tests calling createComputePipeline(Async) validation for overridable constants identifiers. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { constants: {}, _success: true }, { constants: { c0: 0 }, _success: true }, { constants: { c0: 0, c1: 1 }, _success: true }, { constants: { 'c0\0': 0 }, _success: false }, { constants: { c9: 0 }, _success: false }, { constants: { 1: 0 }, _success: true }, { constants: { c3: 0 }, _success: false }, // pipeline constant id is specified for c3 { constants: { 2: 0 }, _success: false }, { constants: { 1000: 0 }, _success: true }, { constants: { 9999: 0 }, _success: false }, { constants: { 1000: 0, c2: 0 }, _success: false }, { constants: { 数: 0 }, _success: true }, { constants: { séquençage: 0 }, _success: false }, // test unicode is not normalized ] as { constants: Record; _success: boolean }[]) ) .fn(t => { const { isAsync, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override c0: bool = true; // type: bool override c1: u32 = 0u; // default override override 数: u32 = 0u; // non-ASCII override séquençage: u32 = 0u; // normalizable unicode (WGSL does not normalize) @id(1000) override c2: u32 = 10u; // default @id(1) override c3: u32 = 11u; // default @compute @workgroup_size(1) fn main () { // make sure the overridable constants are not optimized out _ = u32(c0); _ = u32(c1); _ = u32(c2 + séquençage); _ = u32(c3 + 数); }`, }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); g.test('overrides,uninitialized') .desc( ` Tests calling createComputePipeline(Async) validation for uninitialized overridable constants. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { constants: {}, _success: false }, { constants: { c0: 0, c2: 0, c8: 0 }, _success: false }, // c5 is missing { constants: { c0: 0, c2: 0, c5: 0, c8: 0 }, _success: true }, { constants: { c0: 0, c2: 0, c5: 0, c8: 0, c1: 0 }, _success: true }, ] as { constants: Record; _success: boolean }[]) ) .fn(t => { const { isAsync, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override c0: bool; // type: bool override c1: bool = false; // default override override c2: f32; // type: float32 override c3: f32 = 0.0; // default override override c4: f32 = 4.0; // default override c5: i32; // type: int32 override c6: i32 = 0; // default override override c7: i32 = 7; // default override c8: u32; // type: uint32 override c9: u32 = 0u; // default override @id(1000) override c10: u32 = 10u; // default @compute @workgroup_size(1) fn main () { // make sure the overridable constants are not optimized out _ = u32(c0); _ = u32(c1); _ = u32(c2); _ = u32(c3); _ = u32(c4); _ = u32(c5); _ = u32(c6); _ = u32(c7); _ = u32(c8); _ = u32(c9); _ = u32(c10); }`, }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); g.test('overrides,value,type_error') .desc( ` Tests calling createComputePipeline(Async) validation for constant values like inf, NaN will results in TypeError. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { constants: { cf: 1 }, _success: true }, // control { constants: { cf: NaN }, _success: false }, { constants: { cf: Number.POSITIVE_INFINITY }, _success: false }, { constants: { cf: Number.NEGATIVE_INFINITY }, _success: false }, ] as const) ) .fn(t => { const { isAsync, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override cf: f32 = 0.0; @compute @workgroup_size(1) fn main () { _ = cf; }`, }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor, 'TypeError'); }); g.test('overrides,value,validation_error') .desc( ` Tests calling createComputePipeline(Async) validation for unrepresentable constant values in compute stage. TODO(#2060): test with last_castable_pipeline_override. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { constants: { cu: kValue.u32.min }, _success: true }, { constants: { cu: kValue.u32.min - 1 }, _success: false }, { constants: { cu: kValue.u32.max }, _success: true }, { constants: { cu: kValue.u32.max + 1 }, _success: false }, { constants: { ci: kValue.i32.negative.min }, _success: true }, { constants: { ci: kValue.i32.negative.min - 1 }, _success: false }, { constants: { ci: kValue.i32.positive.max }, _success: true }, { constants: { ci: kValue.i32.positive.max + 1 }, _success: false }, { constants: { cf: kValue.f32.negative.min }, _success: true }, { constants: { cf: kValue.f32.negative.first_non_castable_pipeline_override }, _success: false, }, { constants: { cf: kValue.f32.positive.max }, _success: true }, { constants: { cf: kValue.f32.positive.first_non_castable_pipeline_override }, _success: false, }, // Conversion to boolean can't fail { constants: { cb: Number.MAX_VALUE }, _success: true }, { constants: { cb: kValue.i32.negative.min - 1 }, _success: true }, ] as { constants: Record; _success: boolean }[]) ) .fn(t => { const { isAsync, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override cb: bool = false; override cu: u32 = 0u; override ci: i32 = 0; override cf: f32 = 0.0; @compute @workgroup_size(1) fn main () { _ = cb; _ = cu; _ = ci; _ = cf; }`, }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); g.test('overrides,entry_point,validation_error') .desc( ` Tests that pipeline constant (override) errors only trigger on entry point usage. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('pipeEntryPoint', ['main_success', 'main_pipe_error']) ) .fn(t => { const { isAsync, pipeEntryPoint } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override cu: u32 = 0u; override cx: u32 = 1u/cu; @compute @workgroup_size(1) fn main_success () { _ = cu; } @compute @workgroup_size(1) fn main_pipe_error () { _ = cx; }`, }), entryPoint: pipeEntryPoint, }, }; vtu.doCreateComputePipelineTest(t, isAsync, pipeEntryPoint === 'main_success', descriptor); }); g.test('overrides,value,validation_error,f16') .desc( ` Tests calling createComputePipeline(Async) validation for unrepresentable f16 constant values in compute stage. TODO(#2060): Tighten the cases around the valid/invalid boundary once we have WGSL spec clarity on whether values like f16.positive.last_castable_pipeline_override would be valid. See issue. ` ) .params(u => u // .combine('isAsync', [true, false]) .combineWithParams([ { constants: { cf16: kValue.f16.negative.min }, _success: true }, { constants: { cf16: kValue.f16.negative.first_non_castable_pipeline_override }, _success: false, }, { constants: { cf16: kValue.f16.positive.max }, _success: true }, { constants: { cf16: kValue.f16.positive.first_non_castable_pipeline_override }, _success: false, }, { constants: { cf16: kValue.f32.negative.min }, _success: false }, { constants: { cf16: kValue.f32.positive.max }, _success: false }, { constants: { cf16: kValue.f32.negative.first_non_castable_pipeline_override }, _success: false, }, { constants: { cf16: kValue.f32.positive.first_non_castable_pipeline_override }, _success: false, }, ] as const) ) .fn(t => { t.skipIfDeviceDoesNotHaveFeature('shader-f16'); const { isAsync, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` enable f16; override cf16: f16 = 0.0h; @compute @workgroup_size(1) fn main () { _ = cf16; }`, }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); const kOverridesWorkgroupSizeShaders = { u32: ` override x: u32 = 1u; override y: u32 = 1u; override z: u32 = 1u; @compute @workgroup_size(x, y, z) fn main () { _ = 0u; } `, i32: ` override x: i32 = 1; override y: i32 = 1; override z: i32 = 1; @compute @workgroup_size(x, y, z) fn main () { _ = 0u; } `, }; g.test('overrides,workgroup_size') .desc( ` Tests calling createComputePipeline(Async) validation for overridable constants used for workgroup size. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('type', ['u32', 'i32'] as const) .combineWithParams([ { constants: {}, _success: true }, { constants: { x: 0, y: 0, z: 0 }, _success: false }, { constants: { x: 1, y: -1, z: 1 }, _success: false }, { constants: { x: 1, y: 0, z: 0 }, _success: false }, { constants: { x: 16, y: 1, z: 1 }, _success: true }, ] as { constants: Record; _success: boolean }[]) ) .fn(t => { const { isAsync, type, constants, _success } = t.params; const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: kOverridesWorkgroupSizeShaders[type], }), entryPoint: 'main', constants, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }); g.test('overrides,workgroup_size,limits') .desc( ` Tests calling createComputePipeline(Async) validation for overridable constants for workgroupSize exceeds device limits. ` ) .params(u => u // .combine('isAsync', [true, false]) .combine('type', ['u32', 'i32'] as const) ) .fn(t => { const { isAsync, type } = t.params; const limits = t.device.limits; const testFn = (x: number, y: number, z: number, _success: boolean) => { const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: kOverridesWorkgroupSizeShaders[type], }), entryPoint: 'main', constants: { x, y, z, }, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }; testFn(limits.maxComputeWorkgroupSizeX, 1, 1, true); testFn(limits.maxComputeWorkgroupSizeX + 1, 1, 1, false); testFn(1, limits.maxComputeWorkgroupSizeY, 1, true); testFn(1, limits.maxComputeWorkgroupSizeY + 1, 1, false); testFn(1, 1, limits.maxComputeWorkgroupSizeZ, true); testFn(1, 1, limits.maxComputeWorkgroupSizeZ + 1, false); testFn( limits.maxComputeWorkgroupSizeX, limits.maxComputeWorkgroupSizeY, limits.maxComputeWorkgroupSizeZ, limits.maxComputeWorkgroupSizeX * limits.maxComputeWorkgroupSizeY * limits.maxComputeWorkgroupSizeZ <= limits.maxComputeInvocationsPerWorkgroup ); }); g.test('overrides,workgroup_size,limits,workgroup_storage_size') .desc( ` Tests calling createComputePipeline(Async) validation for overridable constants for workgroupStorageSize exceeds device limits. ` ) .params(u => u // .combine('isAsync', [true, false]) ) .fn(t => { const { isAsync } = t.params; const limits = t.device.limits; const kVec4Size = 16; const maxVec4Count = limits.maxComputeWorkgroupStorageSize / kVec4Size; const kMat4Size = 64; const maxMat4Count = limits.maxComputeWorkgroupStorageSize / kMat4Size; const testFn = (vec4Count: number, mat4Count: number, _success: boolean) => { const descriptor = { layout: 'auto' as const, compute: { module: t.device.createShaderModule({ code: ` override a: u32; override b: u32; ${vec4Count <= 0 ? '' : 'var vec4_data: array, a>;'} ${mat4Count <= 0 ? '' : 'var mat4_data: array, b>;'} @compute @workgroup_size(1) fn main() { ${vec4Count <= 0 ? '' : '_ = vec4_data[0];'} ${mat4Count <= 0 ? '' : '_ = mat4_data[0];'} }`, }), entryPoint: 'main', constants: { a: vec4Count, b: mat4Count, }, }, }; vtu.doCreateComputePipelineTest(t, isAsync, _success, descriptor); }; testFn(1, 1, true); testFn(maxVec4Count + 1, 0, false); testFn(0, maxMat4Count + 1, false); }); g.test('resource_compatibility') .desc( 'Tests validation of resource (bind group) compatibility between pipeline layout and WGSL shader' ) .params(u => u // .combine('apiResource', keysOf(kAPIResources)) .beginSubcases() .combine('isAsync', [true, false] as const) .combine('wgslResource', keysOf(kAPIResources)) ) .fn(t => { const apiResource = kAPIResources[t.params.apiResource]; const wgslResource = kAPIResources[t.params.wgslResource]; t.skipIf( wgslResource.storageTexture !== undefined && wgslResource.storageTexture.access !== 'write-only' && !t.hasLanguageFeature('readonly_and_readwrite_storage_textures'), 'Storage textures require language feature' ); t.skipIfTextureViewDimensionNotSupported(wgslResource.texture?.viewDimension); const layout = t.device.createPipelineLayout({ bindGroupLayouts: [ getAPIBindGroupLayoutForResource(t.device, GPUShaderStage.COMPUTE, apiResource), ], }); const descriptor = { layout, compute: { module: t.device.createShaderModule({ code: getWGSLShaderForResource('compute', wgslResource), }), entryPoint: 'main', }, }; vtu.doCreateComputePipelineTest( t, t.params.isAsync, doResourcesMatch(apiResource, wgslResource), descriptor ); }); g.test('storage_texture,format') .desc( ` Test that a pipeline with auto layout and storage texture access combo that is not supported generates a validation error at createComputePipeline(Async) ` ) .params(u => u // .combine('format', kPossibleStorageTextureFormats) .beginSubcases() .combine('isAsync', [true, false] as const) .combine('access', ['read', 'write', 'read_write'] as const) .combine('dimension', ['1d', '2d', '3d'] as const) ) .fn(t => { const { format, isAsync, access, dimension } = t.params; t.skipIfTextureFormatNotSupported(format); const code = ` @group(0) @binding(0) var tex: texture_storage_${dimension}<${format}, ${access}>; @compute @workgroup_size(1) fn main() { _ = tex; } `; const module = t.device.createShaderModule({ code }); const success = isTextureFormatUsableWithStorageAccessMode(t.device.features, format, access); const descriptor: GPUComputePipelineDescriptor = { layout: 'auto', compute: { module }, }; vtu.doCreateComputePipelineTest(t, isAsync, success, descriptor); });