blob: e6ac7299922d96bf5510ab99e9e40b178e113ea1 [file] [log] [blame]
/**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ import { SkipTestCase } from '../../common/framework/fixture.js';
import {
assert,
raceWithRejectOnTimeout,
unreachable,
assertReject,
} from '../../common/framework/util/util.js';
import { DefaultLimits } from '../constants.js';
import { getGPU } from './navigator_gpu.js';
class TestFailedButDeviceReusable extends Error {}
export class TestOOMedShouldAttemptGC extends Error {}
export class DevicePool {
/** Device with no descriptor. */
defaultHolder = 'uninitialized';
/** Devices with descriptors. */
nonDefaultHolders = new DescriptorToHolderMap();
/** Request a device from the pool. */
async reserve(descriptor) {
// Always attempt to initialize default device, to see if it succeeds.
if (this.defaultHolder === 'uninitialized') {
try {
this.defaultHolder = await DeviceHolder.create(undefined);
} catch (ex) {
this.defaultHolder = 'failed';
}
}
assert(this.defaultHolder !== 'failed', 'WebGPU device failed to initialize; not retrying');
let holder;
if (descriptor === undefined) {
holder = this.defaultHolder;
} else {
holder = await this.nonDefaultHolders.getOrCreate(descriptor);
}
assert(holder.state === 'free', 'Device was in use on DevicePool.acquire');
holder.state = 'reserved';
return holder;
}
// When a test is done using a device, it's released back into the pool.
// This waits for error scopes, checks their results, and checks for various error conditions.
async release(holder) {
assert(this.defaultHolder instanceof DeviceHolder);
assert(holder instanceof DeviceHolder);
assert(holder.state !== 'free', 'trying to release a device while already released');
try {
await holder.ensureRelease();
// (Hopefully if the device was lost, it has been reported by the time endErrorScopes()
// has finished (or timed out). If not, it could cause a finite number of extra test
// failures following this one (but should recover eventually).)
const lostReason = holder.lostReason;
if (lostReason !== undefined) {
// Fail the current test.
unreachable(`Device was lost: ${lostReason}`);
}
} catch (ex) {
// Any error that isn't explicitly TestFailedButDeviceReusable forces a new device to be
// created for the next test.
if (!(ex instanceof TestFailedButDeviceReusable)) {
if (holder === this.defaultHolder) {
this.defaultHolder = 'uninitialized';
} else {
this.nonDefaultHolders.deleteByDevice(holder.device);
}
// TODO: device.destroy()
}
throw ex;
} finally {
// Mark the holder as free. (This only has an effect if the pool still has the holder.)
// This could be done at the top but is done here to guard against async-races during release.
holder.state = 'free';
}
}
}
/**
* Map from GPUDeviceDescriptor to DeviceHolder.
*/
class DescriptorToHolderMap {
unsupported = new Set();
holders = new Map();
/** Deletes an item from the map by GPUDevice value. */
deleteByDevice(device) {
for (const [k, v] of this.holders) {
if (v.device === device) {
this.holders.delete(k);
return;
}
}
}
/**
* Gets a DeviceHolder from the map if it exists; otherwise, calls create() to create one,
* inserts it, and returns it.
*
* Throws SkipTestCase if devices with this descriptor are unsupported.
*/
async getOrCreate(uncanonicalizedDescriptor) {
const [descriptor, key] = canonicalizeDescriptor(uncanonicalizedDescriptor);
// Never retry unsupported configurations.
if (this.unsupported.has(key)) {
throw new SkipTestCase(
`GPUDeviceDescriptor previously failed: ${JSON.stringify(descriptor)}`
);
}
// Search for an existing device with the same descriptor.
{
const value = this.holders.get(key);
if (value) {
// Move it to the end of the Map (most-recently-used).
this.holders.delete(key);
this.holders.set(key, value);
return value;
}
}
// No existing item was found; add a new one.
let value;
try {
value = await DeviceHolder.create(descriptor);
} catch (ex) {
var _ex$message;
this.unsupported.add(key);
throw new SkipTestCase(
`GPUDeviceDescriptor not supported: ${JSON.stringify(descriptor)}\n${
(_ex$message = ex === null || ex === void 0 ? void 0 : ex.message) !== null &&
_ex$message !== void 0
? _ex$message
: ''
}`
);
}
this.insertAndCleanUp(key, value);
return value;
}
/** Insert an entry, then remove the least-recently-used items if there are too many. */
insertAndCleanUp(key, value) {
this.holders.set(key, value);
const kMaxEntries = 5;
if (this.holders.size > kMaxEntries) {
// Delete the first (least recently used) item in the set.
for (const [key] of this.holders) {
this.holders.delete(key);
return;
}
}
}
}
/**
* Make a stringified map-key from a GPUDeviceDescriptor.
* Tries to make sure all defaults are resolved, first - but it's okay if some are missed
* (it just means some GPUDevice objects won't get deduplicated).
*/
function canonicalizeDescriptor(desc) {
const extensionsCanonicalized = desc.extensions ? Array.from(desc.extensions).sort() : [];
const limits = { ...desc.limits };
const limitsCanonicalized = { ...DefaultLimits };
for (const k of Object.keys(limits)) {
if (limits[k] !== undefined) {
limitsCanonicalized[k] = limits[k];
}
}
// Type ensures every field is carried through.
const descriptorCanonicalized = {
extensions: extensionsCanonicalized,
limits: limitsCanonicalized,
};
return [descriptorCanonicalized, JSON.stringify(descriptorCanonicalized)];
}
/**
* DeviceHolder has three states:
* - 'free': Free to be used for a new test.
* - 'reserved': Reserved by a running test, but has not had error scopes created yet.
* - 'acquired': Reserved by a running test, and has had error scopes created.
*/
/**
* Holds a GPUDevice and tracks its state (free/reserved/acquired) and handles device loss.
*/
class DeviceHolder {
state = 'free';
// initially undefined; becomes set when the device is lost
// Gets a device and creates a DeviceHolder.
// If the device is lost, DeviceHolder.lostReason gets set.
static async create(descriptor) {
const gpu = getGPU();
const adapter = await gpu.requestAdapter();
assert(adapter !== null, 'requestAdapter returned null');
const device = await adapter.requestDevice(descriptor);
assert(device !== null, 'requestDevice returned null');
return new DeviceHolder(device);
}
constructor(device) {
this.device = device;
this.device.lost.then(ev => {
this.lostReason = ev.message;
});
}
acquire() {
assert(this.state === 'reserved');
this.state = 'acquired';
this.device.pushErrorScope('out-of-memory');
this.device.pushErrorScope('validation');
return this.device;
}
async ensureRelease() {
const kPopErrorScopeTimeoutMS = 5000;
assert(this.state !== 'free');
try {
if (this.state === 'acquired') {
// Time out if popErrorScope never completes. This could happen due to a browser bug - e.g.,
// as of this writing, on Chrome GPU process crash, popErrorScope just hangs.
await raceWithRejectOnTimeout(
this.release(),
kPopErrorScopeTimeoutMS,
'finalization popErrorScope timed out'
);
}
} finally {
this.state = 'free';
}
}
async release() {
// End the whole-test error scopes. Check that there are no extra error scopes, and that no
// otherwise-uncaptured errors occurred during the test.
let gpuValidationError;
let gpuOutOfMemoryError;
try {
// May reject if the device was lost.
gpuValidationError = await this.device.popErrorScope();
gpuOutOfMemoryError = await this.device.popErrorScope();
} catch (ex) {
assert(
this.lostReason !== undefined,
'popErrorScope failed; should only happen if device has been lost'
);
throw ex;
}
await assertReject(
this.device.popErrorScope(),
'There was an extra error scope on the stack after a test'
);
if (gpuValidationError !== null) {
assert(gpuValidationError instanceof GPUValidationError);
// Allow the device to be reused.
throw new TestFailedButDeviceReusable(
`Unexpected validation error occurred: ${gpuValidationError.message}`
);
}
if (gpuOutOfMemoryError !== null) {
assert(gpuOutOfMemoryError instanceof GPUOutOfMemoryError);
// Don't allow the device to be reused; unexpected OOM could break the device.
throw new TestOOMedShouldAttemptGC('Unexpected out-of-memory error occurred');
}
}
}