blob: e266cbad8d95d67cb287cb1295a51c815ab9ea20 [file] [log] [blame]
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'
);
});
}
}