// eslint-disable-next-line import/no-restricted-paths import { TestCaseRecorder } from '../framework/fixture.js'; import { globalTestConfig } from '../framework/test_config.js'; import { ErrorWithExtra, assert, hasFeature, objectEquals } from './util.js'; /** * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations). * Throws an exception if not found. */ function defaultGPUProvider(): GPU { assert( typeof navigator !== 'undefined' && navigator.gpu !== undefined, 'No WebGPU implementation found' ); return navigator.gpu; } /** * GPUProvider is a function that creates and returns a new GPU instance. * May throw an exception if a GPU cannot be created. */ export type GPUProvider = () => GPU; let gpuProvider: GPUProvider = defaultGPUProvider; /** * Sets the function to create and return a new GPU instance. */ export function setGPUProvider(provider: GPUProvider) { assert(impl === undefined, 'setGPUProvider() should not be after getGPU()'); gpuProvider = provider; } let impl: GPU | undefined = undefined; let s_defaultLimits: Record | undefined = undefined; let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined; export function setDefaultRequestAdapterOptions(options: GPURequestAdapterOptions) { // It's okay to call this if you don't change the options if (objectEquals(options, defaultRequestAdapterOptions)) { return; } if (impl) { throw new Error('must call setDefaultRequestAdapterOptions before getGPU'); } defaultRequestAdapterOptions = { ...options }; } export function getDefaultRequestAdapterOptions() { return defaultRequestAdapterOptions; } function copyLimits(objLike: GPUSupportedLimits) { const obj: Record = {}; for (const key in objLike) { obj[key] = (objLike as unknown as Record)[key]; } return obj; } /** * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations). * Throws an exception if not found. */ export function getGPU(recorder: TestCaseRecorder | null): GPU { if (impl) { return impl; } impl = gpuProvider(); if (globalTestConfig.enforceDefaultLimits) { // eslint-disable-next-line @typescript-eslint/unbound-method const origRequestAdapterFn = impl.requestAdapter; // eslint-disable-next-line @typescript-eslint/unbound-method const origRequestDeviceFn = GPUAdapter.prototype.requestDevice; Object.defineProperty(impl, 'requestAdapter', { configurable: true, async value(options?: GPURequestAdapterOptions) { if (!s_defaultLimits) { const tempAdapter = await origRequestAdapterFn.call(this, { ...defaultRequestAdapterOptions, ...options, }); // eslint-disable-next-line no-restricted-syntax const tempDevice = await tempAdapter?.requestDevice(); s_defaultLimits = copyLimits(tempDevice!.limits); tempDevice?.destroy(); } const adapter = await origRequestAdapterFn.call(this, { ...defaultRequestAdapterOptions, ...options, }); if (adapter) { const limits = Object.fromEntries( Object.entries(s_defaultLimits).map(([key, v]) => [key, v]) ); Object.defineProperty(adapter, 'limits', { get() { return limits; }, }); } return adapter; }, }); const enforceDefaultLimits = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => { if (desc?.requiredLimits) { for (const [key, value] of Object.entries(desc.requiredLimits)) { const limit = s_defaultLimits![key]; if (limit !== undefined && value !== undefined) { const [beyondLimit, condition] = key.startsWith('max') ? [value > limit, 'greater'] : [value < limit, 'less']; if (beyondLimit) { throw new DOMException( `requestedLimit ${value} for ${key} is ${condition} than adapter limit ${limit}`, 'OperationError' ); } } } } }; GPUAdapter.prototype.requestDevice = async function ( this: GPUAdapter, desc?: GPUDeviceDescriptor | undefined ) { // We need to enforce the default limits because even though we patched the adapter to // show defaults for adapter.limits, there are tests that test we throw when we request more than the max. // In other words. // // adapter.requestDevice({ requiredLimits: { // maxXXX: adapter.limits.maxXXX + 1, // should throw // }); // // But unless we enforce this manually, it won't actually throw if the adapter's // true limits are higher than we patched above. enforceDefaultLimits(this, desc); return await origRequestDeviceFn.call(this, desc); }; } if (globalTestConfig.blockAllFeatures) { // eslint-disable-next-line @typescript-eslint/unbound-method const origRequestAdapterFn = impl.requestAdapter; // eslint-disable-next-line @typescript-eslint/unbound-method const origRequestDeviceFn = GPUAdapter.prototype.requestDevice; Object.defineProperty(impl, 'requestAdapter', { configurable: true, async value(options?: GPURequestAdapterOptions) { const adapter = await origRequestAdapterFn.call(this, { ...defaultRequestAdapterOptions, ...options, }); if (adapter) { Object.defineProperty(adapter, 'features', { enumerable: false, value: new Set( hasFeature(adapter.features, 'core-features-and-limits') ? ['core-features-and-limits'] : [] ), }); } return adapter; }, }); const enforceBlockedFeatures = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => { if (desc?.requiredFeatures) { for (const [feature] of desc.requiredFeatures) { // Note: This adapter has had its features property over-ridden and will only return // have nothing or 'core-features-and-limits'. // eslint-disable-next-line no-restricted-syntax if (!adapter.features.has(feature)) { throw new TypeError(`requested feature ${feature} does not exist on adapter`); } } } }; GPUAdapter.prototype.requestDevice = async function ( this: GPUAdapter, desc?: GPUDeviceDescriptor | undefined ) { // We need to enforce the feature block because even though we patched the adapter to // advertise no features they still exist on the real adapter. enforceBlockedFeatures(this, desc); return await origRequestDeviceFn.call(this, desc); }; } if (defaultRequestAdapterOptions) { // eslint-disable-next-line @typescript-eslint/unbound-method const origRequestAdapterFn = impl.requestAdapter; // eslint-disable-next-line @typescript-eslint/unbound-method Object.defineProperty(impl, 'requestAdapter', { configurable: true, async value(options?: GPURequestAdapterOptions) { const adapter = await origRequestAdapterFn.call(this, { ...defaultRequestAdapterOptions, ...options, }); if (recorder && adapter) { const adapterInfo = adapter.info; const infoString = `Adapter: ${adapterInfo.vendor} / ${adapterInfo.architecture} / ${adapterInfo.device}`; recorder.debug(new ErrorWithExtra(infoString, () => ({ adapterInfo }))); } return adapter; }, }); } return impl; }