blob: 6e6802d95f039577be21df1d6b1f79923f2eaff6 [file] [log] [blame]
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(format: GPUTextureFormat): GPUTexture {
return this.trackForCleanup(
this.device.createTexture({
size: { width: 16, height: 16, depthOrArrayLayers: 1 },
format,
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 'readonlyStorageTex':
case 'writeonlyStorageTex':
case 'readwriteStorageTex':
return this.getStorageTexture('r32float').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(format: GPUTextureFormat): GPUTexture {
return this.getDeviceMismatchedTexture({
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. */
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 'readonlyStorageTex':
case 'writeonlyStorageTex':
case 'readwriteStorageTex':
return this.getDeviceMismatchedStorageTexture('r32float').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',
colorFormat: GPUTextureFormat = 'rgba8unorm'
): 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: colorFormat, 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: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError'
) {
if (isAsync) {
if (_success) {
this.shouldResolve(this.device.createRenderPipelineAsync(descriptor));
} else {
this.shouldReject(errorTypeName, this.device.createRenderPipelineAsync(descriptor));
}
} else {
if (errorTypeName === 'GPUPipelineError') {
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: 'GPUPipelineError' | 'TypeError' = 'GPUPipelineError'
) {
if (isAsync) {
if (_success) {
this.shouldResolve(this.device.createComputePipelineAsync(descriptor));
} else {
this.shouldReject(errorTypeName, this.device.createComputePipelineAsync(descriptor));
}
} else {
if (errorTypeName === 'GPUPipelineError') {
this.expectValidationError(() => {
this.device.createComputePipeline(descriptor);
}, !_success);
} else {
this.shouldThrow(_success ? false : errorTypeName, () => {
this.device.createComputePipeline(descriptor);
});
}
}
}
}