| import { |
| ValidBindableResource, |
| BindableResource, |
| kMaxQueryCount, |
| ShaderStageKey, |
| } from '../../capability_info.js'; |
| import { ResourceState, GPUTest } from '../../gpu_test.js'; |
| |
| export function createTextureWithState( |
| t: GPUTest, |
| 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 t.createTextureTracked(descriptor); |
| case 'invalid': |
| return getErrorTexture(t); |
| case 'destroyed': { |
| const texture = t.createTextureTracked(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. |
| */ |
| export function createBufferWithState( |
| t: GPUTest, |
| state: ResourceState, |
| descriptor?: Readonly<GPUBufferDescriptor> |
| ): GPUBuffer { |
| descriptor = descriptor ?? { |
| size: 4, |
| usage: GPUBufferUsage.VERTEX, |
| }; |
| |
| switch (state) { |
| case 'valid': |
| return t.createBufferTracked(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). |
| t.device.pushErrorScope('validation'); |
| const buffer = t.createBufferTracked({ |
| ...descriptor, |
| usage: descriptor.usage | GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_SRC, |
| }); |
| void t.device.popErrorScope(); |
| return buffer; |
| } |
| case 'destroyed': { |
| const buffer = t.createBufferTracked(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'`. |
| */ |
| export function createQuerySetWithState( |
| t: GPUTest, |
| state: ResourceState, |
| desc?: Readonly<GPUQuerySetDescriptor> |
| ): GPUQuerySet { |
| const descriptor = { type: 'occlusion' as const, count: 2, ...desc }; |
| |
| switch (state) { |
| case 'valid': |
| return t.createQuerySetTracked(descriptor); |
| case 'invalid': { |
| // Make the queryset invalid because of the count out of bounds. |
| descriptor.count = kMaxQueryCount + 1; |
| return t.expectGPUError('validation', () => t.createQuerySetTracked(descriptor)); |
| } |
| case 'destroyed': { |
| const queryset = t.createQuerySetTracked(descriptor); |
| queryset.destroy(); |
| return queryset; |
| } |
| } |
| } |
| |
| /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage. */ |
| export function getStorageBuffer(t: GPUTest): GPUBuffer { |
| return t.createBufferTracked({ size: 1024, usage: GPUBufferUsage.STORAGE }); |
| } |
| |
| /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage. */ |
| export function getUniformBuffer(t: GPUTest): GPUBuffer { |
| return t.createBufferTracked({ size: 1024, usage: GPUBufferUsage.UNIFORM }); |
| } |
| |
| /** Return an invalid GPUBuffer. */ |
| export function getErrorBuffer(t: GPUTest): GPUBuffer { |
| return createBufferWithState(t, 'invalid'); |
| } |
| |
| /** Return an invalid GPUSampler. */ |
| export function getErrorSampler(t: GPUTest): GPUSampler { |
| t.device.pushErrorScope('validation'); |
| const sampler = t.device.createSampler({ lodMinClamp: -1 }); |
| void t.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. |
| */ |
| export function getSampledTexture(t: GPUTest, sampleCount: number = 1): GPUTexture { |
| const usage = |
| sampleCount > 1 |
| ? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT |
| : GPUTextureUsage.TEXTURE_BINDING; |
| return t.createTextureTracked({ |
| size: { width: 16, height: 16, depthOrArrayLayers: 1 }, |
| format: 'rgba8unorm', |
| usage, |
| sampleCount, |
| }); |
| } |
| |
| /** Return an arbitrarily-configured GPUTexture with the `STORAGE_BINDING` usage. */ |
| function getStorageTexture(t: GPUTest, format: GPUTextureFormat): GPUTexture { |
| return t.createTextureTracked({ |
| size: { width: 16, height: 16, depthOrArrayLayers: 1 }, |
| format, |
| usage: GPUTextureUsage.STORAGE_BINDING, |
| }); |
| } |
| |
| /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage. */ |
| export function getRenderTexture(t: GPUTest, sampleCount: number = 1): GPUTexture { |
| return t.createTextureTracked({ |
| size: { width: 16, height: 16, depthOrArrayLayers: 1 }, |
| format: 'rgba8unorm', |
| usage: GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount, |
| }); |
| } |
| |
| /** Return an invalid GPUTexture. */ |
| export function getErrorTexture(t: GPUTest): GPUTexture { |
| t.device.pushErrorScope('validation'); |
| const texture = t.createTextureTracked({ |
| size: { width: 0, height: 0, depthOrArrayLayers: 0 }, |
| format: 'rgba8unorm', |
| usage: GPUTextureUsage.TEXTURE_BINDING, |
| }); |
| void t.device.popErrorScope(); |
| return texture; |
| } |
| |
| /** Return an invalid GPUTextureView (created from an invalid GPUTexture). */ |
| export function getErrorTextureView(t: GPUTest): GPUTextureView { |
| t.device.pushErrorScope('validation'); |
| const view = getErrorTexture(t).createView(); |
| void t.device.popErrorScope(); |
| return view; |
| } |
| |
| /** |
| * Return an arbitrary object of the specified {@link webgpu/capability_info!BindableResource} type |
| * (e.g. `'errorBuf'`, `'nonFiltSamp'`, `sampledTexMS`, etc.) |
| */ |
| export function getBindingResource(t: GPUTest, bindingType: BindableResource): GPUBindingResource { |
| switch (bindingType) { |
| case 'errorBuf': |
| return { buffer: getErrorBuffer(t) }; |
| case 'errorSamp': |
| return getErrorSampler(t); |
| case 'errorTex': |
| return getErrorTextureView(t); |
| case 'uniformBuf': |
| return { buffer: getUniformBuffer(t) }; |
| case 'storageBuf': |
| return { buffer: getStorageBuffer(t) }; |
| case 'filtSamp': |
| return t.device.createSampler({ minFilter: 'linear' }); |
| case 'nonFiltSamp': |
| return t.device.createSampler(); |
| case 'compareSamp': |
| return t.device.createSampler({ compare: 'never' }); |
| case 'sampledTex': |
| return getSampledTexture(t, 1).createView(); |
| case 'sampledTexMS': |
| return getSampledTexture(t, 4).createView(); |
| case 'readonlyStorageTex': |
| case 'writeonlyStorageTex': |
| case 'readwriteStorageTex': |
| return getStorageTexture(t, 'r32float').createView(); |
| } |
| } |
| |
| /** Create an arbitrarily-sized GPUBuffer with the STORAGE usage from mismatched device. */ |
| export function getDeviceMismatchedStorageBuffer(t: GPUTest): GPUBuffer { |
| return t.trackForCleanup( |
| t.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE }) |
| ); |
| } |
| |
| /** Create an arbitrarily-sized GPUBuffer with the UNIFORM usage from mismatched device. */ |
| export function getDeviceMismatchedUniformBuffer(t: GPUTest): GPUBuffer { |
| return t.trackForCleanup( |
| t.mismatchedDevice.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM }) |
| ); |
| } |
| |
| /** Return a GPUTexture with descriptor from mismatched device. */ |
| export function getDeviceMismatchedTexture( |
| t: GPUTest, |
| descriptor: GPUTextureDescriptor |
| ): GPUTexture { |
| return t.trackForCleanup(t.mismatchedDevice.createTexture(descriptor)); |
| } |
| |
| /** Return an arbitrarily-configured GPUTexture with the `SAMPLED` usage from mismatched device. */ |
| export function getDeviceMismatchedSampledTexture(t: GPUTest, sampleCount: number = 1): GPUTexture { |
| return getDeviceMismatchedTexture(t, { |
| 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. */ |
| export function getDeviceMismatchedStorageTexture( |
| t: GPUTest, |
| format: GPUTextureFormat |
| ): GPUTexture { |
| return getDeviceMismatchedTexture(t, { |
| size: { width: 4, height: 4, depthOrArrayLayers: 1 }, |
| format, |
| usage: GPUTextureUsage.STORAGE_BINDING, |
| }); |
| } |
| |
| /** Return an arbitrarily-configured GPUTexture with the `RENDER_ATTACHMENT` usage from mismatched device. */ |
| export function getDeviceMismatchedRenderTexture(t: GPUTest, sampleCount: number = 1): GPUTexture { |
| return getDeviceMismatchedTexture(t, { |
| size: { width: 4, height: 4, depthOrArrayLayers: 1 }, |
| format: 'rgba8unorm', |
| usage: GPUTextureUsage.RENDER_ATTACHMENT, |
| sampleCount, |
| }); |
| } |
| |
| export function getDeviceMismatchedBindingResource( |
| t: GPUTest, |
| bindingType: ValidBindableResource |
| ): GPUBindingResource { |
| switch (bindingType) { |
| case 'uniformBuf': |
| return { buffer: getDeviceMismatchedUniformBuffer(t) }; |
| case 'storageBuf': |
| return { buffer: getDeviceMismatchedStorageBuffer(t) }; |
| case 'filtSamp': |
| return t.mismatchedDevice.createSampler({ minFilter: 'linear' }); |
| case 'nonFiltSamp': |
| return t.mismatchedDevice.createSampler(); |
| case 'compareSamp': |
| return t.mismatchedDevice.createSampler({ compare: 'never' }); |
| case 'sampledTex': |
| return getDeviceMismatchedSampledTexture(t, 1).createView(); |
| case 'sampledTexMS': |
| return getDeviceMismatchedSampledTexture(t, 4).createView(); |
| case 'readonlyStorageTex': |
| case 'writeonlyStorageTex': |
| case 'readwriteStorageTex': |
| return getDeviceMismatchedStorageTexture(t, 'r32float').createView(); |
| } |
| } |
| |
| /** Return a no-op shader code snippet for the specified shader stage. */ |
| export function 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. */ |
| export function createRenderPipelineWithState( |
| t: GPUTest, |
| state: 'valid' | 'invalid' |
| ): GPURenderPipeline { |
| return state === 'valid' ? createNoOpRenderPipeline(t) : createErrorRenderPipeline(t); |
| } |
| |
| /** Return a GPURenderPipeline with default options and no-op vertex and fragment shaders. */ |
| export function createNoOpRenderPipeline( |
| t: GPUTest, |
| layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto', |
| colorFormat: GPUTextureFormat = 'rgba8unorm' |
| ): GPURenderPipeline { |
| return t.device.createRenderPipeline({ |
| layout, |
| vertex: { |
| module: t.device.createShaderModule({ |
| code: getNoOpShaderCode('VERTEX'), |
| }), |
| entryPoint: 'main', |
| }, |
| fragment: { |
| module: t.device.createShaderModule({ |
| code: getNoOpShaderCode('FRAGMENT'), |
| }), |
| entryPoint: 'main', |
| targets: [{ format: colorFormat, writeMask: 0 }], |
| }, |
| primitive: { topology: 'triangle-list' }, |
| }); |
| } |
| |
| /** Return an invalid GPURenderPipeline. */ |
| export function createErrorRenderPipeline(t: GPUTest): GPURenderPipeline { |
| t.device.pushErrorScope('validation'); |
| const pipeline = t.device.createRenderPipeline({ |
| layout: 'auto', |
| vertex: { |
| module: t.device.createShaderModule({ |
| code: '', |
| }), |
| entryPoint: '', |
| }, |
| }); |
| void t.device.popErrorScope(); |
| return pipeline; |
| } |
| |
| /** Return a GPUComputePipeline with a no-op shader. */ |
| export function createNoOpComputePipeline( |
| t: GPUTest, |
| layout: GPUPipelineLayout | GPUAutoLayoutMode = 'auto' |
| ): GPUComputePipeline { |
| return t.device.createComputePipeline({ |
| layout, |
| compute: { |
| module: t.device.createShaderModule({ |
| code: getNoOpShaderCode('COMPUTE'), |
| }), |
| entryPoint: 'main', |
| }, |
| }); |
| } |
| |
| /** Return an invalid GPUComputePipeline. */ |
| export function createErrorComputePipeline(t: GPUTest): GPUComputePipeline { |
| t.device.pushErrorScope('validation'); |
| const pipeline = t.device.createComputePipeline({ |
| layout: 'auto', |
| compute: { |
| module: t.device.createShaderModule({ |
| code: '', |
| }), |
| entryPoint: '', |
| }, |
| }); |
| void t.device.popErrorScope(); |
| return pipeline; |
| } |
| |
| /** Return an invalid GPUShaderModule. */ |
| export function createInvalidShaderModule(t: GPUTest): GPUShaderModule { |
| t.device.pushErrorScope('validation'); |
| const code = 'deadbeaf'; // Something make no sense |
| const shaderModule = t.device.createShaderModule({ code }); |
| void t.device.popErrorScope(); |
| return shaderModule; |
| } |
| |
| /** Helper for testing createRenderPipeline(Async) validation */ |
| export function doCreateRenderPipelineTest( |
| t: GPUTest, |
| isAsync: boolean, |
| _success: boolean, |
| descriptor: GPURenderPipelineDescriptor, |
| errorTypeName: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError' |
| ) { |
| if (isAsync) { |
| if (_success) { |
| t.shouldResolve(t.device.createRenderPipelineAsync(descriptor)); |
| } else { |
| t.shouldReject(errorTypeName, t.device.createRenderPipelineAsync(descriptor)); |
| } |
| } else { |
| if (errorTypeName === 'GPUPipelineError') { |
| t.expectValidationError(() => { |
| t.device.createRenderPipeline(descriptor); |
| }, !_success); |
| } else { |
| t.shouldThrow(_success ? false : errorTypeName, () => { |
| t.device.createRenderPipeline(descriptor); |
| }); |
| } |
| } |
| } |
| |
| /** Helper for testing createComputePipeline(Async) validation */ |
| export function doCreateComputePipelineTest( |
| t: GPUTest, |
| isAsync: boolean, |
| _success: boolean, |
| descriptor: GPUComputePipelineDescriptor, |
| errorTypeName: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError' |
| ) { |
| if (isAsync) { |
| if (_success) { |
| t.shouldResolve(t.device.createComputePipelineAsync(descriptor)); |
| } else { |
| t.shouldReject(errorTypeName, t.device.createComputePipelineAsync(descriptor)); |
| } |
| } else { |
| if (errorTypeName === 'GPUPipelineError') { |
| t.expectValidationError(() => { |
| t.device.createComputePipeline(descriptor); |
| }, !_success); |
| } else { |
| t.shouldThrow(_success ? false : errorTypeName, () => { |
| t.device.createComputePipeline(descriptor); |
| }); |
| } |
| } |
| } |