| import { Fixture } from '../common/framework/fixture.js'; |
| import { getGPU } from '../common/util/navigator_gpu.js'; |
| import { assert, raceWithRejectOnTimeout } from '../common/util/util.js'; |
| |
| /** |
| * A test class to help test error scopes and uncapturederror events. |
| */ |
| export class ErrorTest extends Fixture { |
| _device: GPUDevice | undefined = undefined; |
| |
| get device(): GPUDevice { |
| assert(this._device !== undefined); |
| return this._device; |
| } |
| |
| override async init(): Promise<void> { |
| await super.init(); |
| const gpu = getGPU(this.rec); |
| const adapter = await gpu.requestAdapter(); |
| assert(adapter !== null); |
| |
| // We need to max out the adapter limits related to texture dimensions to more reliably cause an |
| // OOM error when asked for it, so set that on the device now. |
| const device = await this.requestDeviceTracked(adapter, { |
| requiredLimits: { |
| maxTextureDimension2D: adapter.limits.maxTextureDimension2D, |
| maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers, |
| }, |
| }); |
| assert(device !== null); |
| this._device = device; |
| } |
| |
| /** |
| * Generates an error of the given filter type. For now, the errors are generated by calling a |
| * known code-path to cause the error. This can be updated in the future should there be a more |
| * direct way to inject errors. |
| */ |
| generateError(filter: GPUErrorFilter): void { |
| switch (filter) { |
| case 'out-of-memory': |
| this.trackForCleanup( |
| this.device.createTexture({ |
| // One of the largest formats. With the base limits, the texture will be 256 GiB. |
| format: 'rgba32float', |
| usage: GPUTextureUsage.COPY_DST, |
| size: [ |
| this.device.limits.maxTextureDimension2D, |
| this.device.limits.maxTextureDimension2D, |
| this.device.limits.maxTextureArrayLayers, |
| ], |
| }) |
| ); |
| break; |
| case 'validation': |
| // Generating a validation error by passing in an invalid usage when creating a buffer. |
| this.trackForCleanup( |
| this.device.createBuffer({ |
| size: 1024, |
| usage: 0xffff, // Invalid GPUBufferUsage |
| }) |
| ); |
| break; |
| } |
| // MAINTENANCE_TODO: This is a workaround for Chromium not flushing. Remove when not needed. |
| this.device.queue.submit([]); |
| } |
| |
| /** |
| * Checks whether the error is of the type expected given the filter. |
| */ |
| isInstanceOfError(filter: GPUErrorFilter, error: GPUError | null): boolean { |
| switch (filter) { |
| case 'out-of-memory': |
| return error instanceof GPUOutOfMemoryError; |
| case 'validation': |
| return error instanceof GPUValidationError; |
| case 'internal': |
| return error instanceof GPUInternalError; |
| } |
| } |
| |
| /** |
| * Pop `count` error scopes, and assert they all return `null`. Chunks the |
| * awaits so we only `Promise.all` 200 scopes at a time, instead of stalling |
| * on a huge `Promise.all` all at once. This helps Chromium's "heartbeat" |
| * mechanism know that the test is still running (and not hung). |
| */ |
| async chunkedPopManyErrorScopes(count: number) { |
| const promises = []; |
| for (let i = 0; i < count; i++) { |
| promises.push(this.device.popErrorScope()); |
| if (promises.length >= 200) { |
| this.expect((await Promise.all(promises)).every(e => e === null)); |
| promises.length = 0; |
| } |
| } |
| this.expect((await Promise.all(promises)).every(e => e === null)); |
| } |
| |
| /** |
| * Expect an uncapturederror event to occur. Note: this MUST be awaited, because |
| * otherwise it could erroneously pass by capturing an error from later in the test. |
| */ |
| async expectUncapturedError( |
| fn: Function, |
| useOnuncapturederror = false |
| ): Promise<GPUUncapturedErrorEvent> { |
| return this.immediateAsyncExpectation(() => { |
| const promise: Promise<GPUUncapturedErrorEvent> = new Promise(resolve => { |
| const eventListener = (event: GPUUncapturedErrorEvent) => { |
| // Don't emit error to console. |
| event.preventDefault(); |
| // Unregister before resolving so we can be certain these are cleaned |
| // up before the next test. |
| if (useOnuncapturederror) { |
| this.device.onuncapturederror = null; |
| } else { |
| this.device.removeEventListener('uncapturederror', eventListener); |
| } |
| |
| this.debug(`Got uncaptured error event with ${event.error}`); |
| resolve(event); |
| }; |
| |
| if (useOnuncapturederror) { |
| this.device.onuncapturederror = eventListener; |
| } else { |
| this.device.addEventListener('uncapturederror', eventListener, { once: true }); |
| } |
| }); |
| |
| fn(); |
| |
| const kTimeoutMS = 1000; |
| return raceWithRejectOnTimeout( |
| promise, |
| kTimeoutMS, |
| 'Timeout occurred waiting for uncaptured error' |
| ); |
| }); |
| } |
| } |