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 { 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 { return this.immediateAsyncExpectation(() => { const promise: Promise = 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' ); }); } }