diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts new file mode 100644 index 0000000000..ad6d030251 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/validation/validation_test.ts @@ -0,0 +1,448 @@ +import { + ValidBindableResource, + BindableResource, + kMaxQueryCount, + ShaderStageKey, +} from '../../capability_info.js'; +import { GPUTest, ResourceState } from '../../gpu_test.js'; + +/** + * Base fixture for WebGPU validation tests. + */ +export class ValidationTest extends GPUTest { + /** + * Create a GPUTexture in the specified state. + * A `descriptor` may optionally be passed, which is used when `state` is not `'invalid'`. + */ + createTextureWithState( + state: ResourceState, + descriptor?: Readonly<GPUTextureDescriptor> + ): GPUTexture { + descriptor = descriptor ?? { + size: { width: 1, height: 1, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT, + }; + + switch (state) { + case 'valid': + return this.trackForCleanup(this.device.createTexture(descriptor)); + case 'invalid': + return this.getErrorTexture(); + case 'destroyed': { + const texture = this.device.createTexture(descriptor); + texture.destroy(); + return texture; + } + } + } + + /** + * Create a GPUTexture in the specified state. A `descriptor` may optionally be passed; + * if `state` is `'invalid'`, it will be modified to add an invalid combination of usages. + */ + createBufferWithState( + state: ResourceState, + descriptor?: Readonly<GPUBufferDescriptor> + ): GPUBuffer { + descriptor = descriptor ?? { + size: 4, + usage: GPUBufferUsage.VERTEX, + }; + + switch (state) { + case 'valid': + return this.trackForCleanup(this.device.createBuffer(descriptor)); + + case 'invalid': { + // Make the buffer invalid because of an invalid combination of usages but keep the + // descriptor passed as much as possible (for mappedAtCreation and friends). + this.device.pushErrorScope('validation'); + const buffer = this.device.createBuffer({ + ...descriptor, + usage: descriptor.usage | GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_SRC, + }); + void this.device.popErrorScope(); + return buffer; + } + case 'destroyed': { + const buffer = this.device.createBuffer(descriptor); + buffer.destroy(); + return buffer; + } + } + } + + /** + * Create a GPUQuerySet in the specified state. + * A `descriptor` may optionally be passed, which is used when `state` is not `'invalid'`. + */ + createQuerySetWithState( + state: ResourceState, + desc?: Readonly<GPUQuerySetDescriptor> + ): GPUQuerySet { + const descriptor = { type: 'occlusion' as const, count: 2, ...desc }; + + switch (state) { + case 'valid': + return this.trackForCleanup(this.device.createQuerySet(descriptor)); + case 'invalid': { + // Make the queryset invalid because of the count out of bounds. + descriptor.count = kMaxQueryCount + 1; + return this.expectGPUError('validation', () => this.device.createQuerySet(descriptor)); + } + case 'destroyed': { + const queryset = this.device.createQuerySet(descriptor); + queryset.destroy(); + return queryset; + } + } + } + + /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage. */ + getStorageBuffer(): GPUBuffer { + return this.trackForCleanup( + this.device.createBuffer({ size: 1024, usage: GPUBufferUsage.STORAGE }) + ); + } + + /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage. */ + getUniformBuffer(): GPUBuffer { + return this.trackForCleanup( + this.device.createBuffer({ size: 1024, usage: GPUBufferUsage.UNIFORM }) + ); + } + + /** Return an invalid GPUBuffer. */ + getErrorBuffer(): GPUBuffer { + return this.createBufferWithState('invalid'); + } + + /** Return an invalid GPUSampler. */ + getErrorSampler(): GPUSampler { + this.device.pushErrorScope('validation'); + const sampler = this.device.createSampler({ lodMinClamp: -1 }); + void this.device.popErrorScope(); + return sampler; + } + + /** + * Return an arbitrarily-configured GPUTexture with the `TEXTURE_BINDING` usage and specified + * sampleCount. The `RENDER_ATTACHMENT` usage will also be specified if sampleCount > 1 as is + * required by WebGPU SPEC. + */ + getSampledTexture(sampleCount: number = 1): GPUTexture { + const usage = + sampleCount > 1 + ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT + : GPUTextureUsage.TEXTURE_BINDING; + return this.trackForCleanup( + this.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage, + sampleCount, + }) + ); + } + + /** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */ + getStorageTexture(): GPUTexture { + return this.trackForCleanup( + this.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.STORAGE_BINDING, + }) + ); + } + + /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage. */ + getRenderTexture(sampleCount: number = 1): GPUTexture { + return this.trackForCleanup( + this.device.createTexture({ + size: { width: 16, height: 16, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount, + }) + ); + } + + /** Return an invalid GPUTexture. */ + getErrorTexture(): GPUTexture { + this.device.pushErrorScope('validation'); + const texture = this.device.createTexture({ + size: { width: 0, height: 0, depthOrArrayLayers: 0 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING, + }); + void this.device.popErrorScope(); + return texture; + } + + /** Return an invalid GPUTextureView (created from an invalid GPUTexture). */ + getErrorTextureView(): GPUTextureView { + this.device.pushErrorScope('validation'); + const view = this.getErrorTexture().createView(); + void this.device.popErrorScope(); + return view; + } + + /** + * Return an arbitrary object of the specified {@link webgpu/capability_info!BindableResource} type + * (e.g. `'errorBuf'`, `'nonFiltSamp'`, `sampledTexMS`, etc.) + */ + getBindingResource(bindingType: BindableResource): GPUBindingResource { + switch (bindingType) { + case 'errorBuf': + return { buffer: this.getErrorBuffer() }; + case 'errorSamp': + return this.getErrorSampler(); + case 'errorTex': + return this.getErrorTextureView(); + case 'uniformBuf': + return { buffer: this.getUniformBuffer() }; + case 'storageBuf': + return { buffer: this.getStorageBuffer() }; + case 'filtSamp': + return this.device.createSampler({ minFilter: 'linear' }); + case 'nonFiltSamp': + return this.device.createSampler(); + case 'compareSamp': + return this.device.createSampler({ compare: 'never' }); + case 'sampledTex': + return this.getSampledTexture(1).createView(); + case 'sampledTexMS': + return this.getSampledTexture(4).createView(); + case 'storageTex': + return this.getStorageTexture().createView(); + } + } + + /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage from mismatched device. */ + getDeviceMismatchedStorageBuffer(): GPUBuffer { + return this.trackForCleanup( + this.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE }) + ); + } + + /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage from mismatched device. */ + getDeviceMismatchedUniformBuffer(): GPUBuffer { + return this.trackForCleanup( + this.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM }) + ); + } + + /** Return a GPUTexture with descriptor from mismatched device. */ + getDeviceMismatchedTexture(descriptor: GPUTextureDescriptor): GPUTexture { + return this.trackForCleanup(this.mismatchedDevice.createTexture(descriptor)); + } + + /** Return an arbitrarily-configured GPUTexture with the `SAMPLED` usage from mismatched device. */ + getDeviceMismatchedSampledTexture(sampleCount: number = 1): GPUTexture { + return this.getDeviceMismatchedTexture({ + size: { width: 4, height: 4, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING, + sampleCount, + }); + } + + /** Return an arbitrarily-configured GPUTexture with the `STORAGE` usage from mismatched device. */ + getDeviceMismatchedStorageTexture(): GPUTexture { + return this.getDeviceMismatchedTexture({ + size: { width: 4, height: 4, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.STORAGE_BINDING, + }); + } + + /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage from mismatched device. */ + getDeviceMismatchedRenderTexture(sampleCount: number = 1): GPUTexture { + return this.getDeviceMismatchedTexture({ + size: { width: 4, height: 4, depthOrArrayLayers: 1 }, + format: 'rgba8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount, + }); + } + + getDeviceMismatchedBindingResource(bindingType: ValidBindableResource): GPUBindingResource { + switch (bindingType) { + case 'uniformBuf': + return { buffer: this.getDeviceMismatchedStorageBuffer() }; + case 'storageBuf': + return { buffer: this.getDeviceMismatchedUniformBuffer() }; + case 'filtSamp': + return this.mismatchedDevice.createSampler({ minFilter: 'linear' }); + case 'nonFiltSamp': + return this.mismatchedDevice.createSampler(); + case 'compareSamp': + return this.mismatchedDevice.createSampler({ compare: 'never' }); + case 'sampledTex': + return this.getDeviceMismatchedSampledTexture(1).createView(); + case 'sampledTexMS': + return this.getDeviceMismatchedSampledTexture(4).createView(); + case 'storageTex': + return this.getDeviceMismatchedStorageTexture().createView(); + } + } + + /** Return a no-op shader code snippet for the specified shader stage. */ + getNoOpShaderCode(stage: ShaderStageKey): string { + switch (stage) { + case 'VERTEX': + return ` + @vertex fn main() -> @builtin(position) vec4<f32> { + return vec4<f32>(); + } + `; + case 'FRAGMENT': + return `@fragment fn main() {}`; + case 'COMPUTE': + return `@compute @workgroup_size(1) fn main() {}`; + } + } + + /** Create a GPURenderPipeline in the specified state. */ + createRenderPipelineWithState(state: 'valid' | 'invalid'): GPURenderPipeline { + return state === 'valid' ? this.createNoOpRenderPipeline() : this.createErrorRenderPipeline(); + } + + /** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */ + createNoOpRenderPipeline( + layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto' + ): GPURenderPipeline { + return this.device.createRenderPipeline({ + layout, + vertex: { + module: this.device.createShaderModule({ + code: this.getNoOpShaderCode('VERTEX'), + }), + entryPoint: 'main', + }, + fragment: { + module: this.device.createShaderModule({ + code: this.getNoOpShaderCode('FRAGMENT'), + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm', writeMask: 0 }], + }, + primitive: { topology: 'triangle-list' }, + }); + } + + /** Return an invalid GPURenderPipeline. */ + createErrorRenderPipeline(): GPURenderPipeline { + this.device.pushErrorScope('validation'); + const pipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: '', + }), + entryPoint: '', + }, + }); + void this.device.popErrorScope(); + return pipeline; + } + + /** Return a GPUComputePipeline with a no-op shader. */ + createNoOpComputePipeline( + layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto' + ): GPUComputePipeline { + return this.device.createComputePipeline({ + layout, + compute: { + module: this.device.createShaderModule({ + code: this.getNoOpShaderCode('COMPUTE'), + }), + entryPoint: 'main', + }, + }); + } + + /** Return an invalid GPUComputePipeline. */ + createErrorComputePipeline(): GPUComputePipeline { + this.device.pushErrorScope('validation'); + const pipeline = this.device.createComputePipeline({ + layout: 'auto', + compute: { + module: this.device.createShaderModule({ + code: '', + }), + entryPoint: '', + }, + }); + void this.device.popErrorScope(); + return pipeline; + } + + /** Return an invalid GPUShaderModule. */ + createInvalidShaderModule(): GPUShaderModule { + this.device.pushErrorScope('validation'); + const code = 'deadbeaf'; // Something make no sense + const shaderModule = this.device.createShaderModule({ code }); + void this.device.popErrorScope(); + return shaderModule; + } + + /** Helper for testing createRenderPipeline(Async) validation */ + doCreateRenderPipelineTest( + isAsync: boolean, + _success: boolean, + descriptor: GPURenderPipelineDescriptor, + errorTypeName: 'OperationError' | 'TypeError' = 'OperationError' + ) { + if (isAsync) { + if (_success) { + this.shouldResolve(this.device.createRenderPipelineAsync(descriptor)); + } else { + this.shouldReject(errorTypeName, this.device.createRenderPipelineAsync(descriptor)); + } + } else { + if (errorTypeName === 'OperationError') { + this.expectValidationError(() => { + this.device.createRenderPipeline(descriptor); + }, !_success); + } else { + this.shouldThrow(_success ? false : errorTypeName, () => { + this.device.createRenderPipeline(descriptor); + }); + } + } + } + + /** Helper for testing createComputePipeline(Async) validation */ + doCreateComputePipelineTest( + isAsync: boolean, + _success: boolean, + descriptor: GPUComputePipelineDescriptor, + errorTypeName: 'OperationError' | 'TypeError' = 'OperationError' + ) { + if (isAsync) { + if (_success) { + this.shouldResolve(this.device.createComputePipelineAsync(descriptor)); + } else { + this.shouldReject(errorTypeName, this.device.createComputePipelineAsync(descriptor)); + } + } else { + if (errorTypeName === 'OperationError') { + this.expectValidationError(() => { + this.device.createComputePipeline(descriptor); + }, !_success); + } else { + this.shouldThrow(_success ? false : errorTypeName, () => { + this.device.createComputePipeline(descriptor); + }); + } + } + } +} |