tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

navigator_gpu.ts (7779B)


      1 // eslint-disable-next-line import/no-restricted-paths
      2 import { TestCaseRecorder } from '../framework/fixture.js';
      3 import { globalTestConfig } from '../framework/test_config.js';
      4 
      5 import { ErrorWithExtra, assert, hasFeature, objectEquals } from './util.js';
      6 
      7 /**
      8 * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
      9 * Throws an exception if not found.
     10 */
     11 function defaultGPUProvider(): GPU {
     12  assert(
     13    typeof navigator !== 'undefined' && navigator.gpu !== undefined,
     14    'No WebGPU implementation found'
     15  );
     16  return navigator.gpu;
     17 }
     18 
     19 /**
     20 * GPUProvider is a function that creates and returns a new GPU instance.
     21 * May throw an exception if a GPU cannot be created.
     22 */
     23 export type GPUProvider = () => GPU;
     24 
     25 let gpuProvider: GPUProvider = defaultGPUProvider;
     26 
     27 /**
     28 * Sets the function to create and return a new GPU instance.
     29 */
     30 export function setGPUProvider(provider: GPUProvider) {
     31  assert(impl === undefined, 'setGPUProvider() should not be after getGPU()');
     32  gpuProvider = provider;
     33 }
     34 
     35 let impl: GPU | undefined = undefined;
     36 let s_defaultLimits: Record<string, GPUSize64> | undefined = undefined;
     37 
     38 let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined;
     39 
     40 export function setDefaultRequestAdapterOptions(options: GPURequestAdapterOptions) {
     41  // It's okay to call this if you don't change the options
     42  if (objectEquals(options, defaultRequestAdapterOptions)) {
     43    return;
     44  }
     45  if (impl) {
     46    throw new Error('must call setDefaultRequestAdapterOptions before getGPU');
     47  }
     48  defaultRequestAdapterOptions = { ...options };
     49 }
     50 
     51 export function getDefaultRequestAdapterOptions() {
     52  return defaultRequestAdapterOptions;
     53 }
     54 
     55 function copyLimits(objLike: GPUSupportedLimits) {
     56  const obj: Record<string, number> = {};
     57  for (const key in objLike) {
     58    obj[key] = (objLike as unknown as Record<string, number>)[key];
     59  }
     60  return obj;
     61 }
     62 
     63 /**
     64 * Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
     65 * Throws an exception if not found.
     66 */
     67 export function getGPU(recorder: TestCaseRecorder | null): GPU {
     68  if (impl) {
     69    return impl;
     70  }
     71 
     72  impl = gpuProvider();
     73 
     74  if (globalTestConfig.enforceDefaultLimits) {
     75    // eslint-disable-next-line @typescript-eslint/unbound-method
     76    const origRequestAdapterFn = impl.requestAdapter;
     77    // eslint-disable-next-line @typescript-eslint/unbound-method
     78    const origRequestDeviceFn = GPUAdapter.prototype.requestDevice;
     79 
     80    Object.defineProperty(impl, 'requestAdapter', {
     81      configurable: true,
     82      async value(options?: GPURequestAdapterOptions) {
     83        if (!s_defaultLimits) {
     84          const tempAdapter = await origRequestAdapterFn.call(this, {
     85            ...defaultRequestAdapterOptions,
     86            ...options,
     87          });
     88          // eslint-disable-next-line no-restricted-syntax
     89          const tempDevice = await tempAdapter?.requestDevice();
     90          s_defaultLimits = copyLimits(tempDevice!.limits);
     91          tempDevice?.destroy();
     92        }
     93        const adapter = await origRequestAdapterFn.call(this, {
     94          ...defaultRequestAdapterOptions,
     95          ...options,
     96        });
     97        if (adapter) {
     98          const limits = Object.fromEntries(
     99            Object.entries(s_defaultLimits).map(([key, v]) => [key, v])
    100          );
    101 
    102          Object.defineProperty(adapter, 'limits', {
    103            get() {
    104              return limits;
    105            },
    106          });
    107        }
    108        return adapter;
    109      },
    110    });
    111 
    112    const enforceDefaultLimits = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => {
    113      if (desc?.requiredLimits) {
    114        for (const [key, value] of Object.entries(desc.requiredLimits)) {
    115          const limit = s_defaultLimits![key];
    116          if (limit !== undefined && value !== undefined) {
    117            const [beyondLimit, condition] = key.startsWith('max')
    118              ? [value > limit, 'greater']
    119              : [value < limit, 'less'];
    120            if (beyondLimit) {
    121              throw new DOMException(
    122                `requestedLimit ${value} for ${key} is ${condition} than adapter limit ${limit}`,
    123                'OperationError'
    124              );
    125            }
    126          }
    127        }
    128      }
    129    };
    130 
    131    GPUAdapter.prototype.requestDevice = async function (
    132      this: GPUAdapter,
    133      desc?: GPUDeviceDescriptor | undefined
    134    ) {
    135      // We need to enforce the default limits because even though we patched the adapter to
    136      // show defaults for adapter.limits, there are tests that test we throw when we request more than the max.
    137      // In other words.
    138      //
    139      //   adapter.requestDevice({ requiredLimits: {
    140      //     maxXXX: adapter.limits.maxXXX + 1,  // should throw
    141      //   });
    142      //
    143      // But unless we enforce this manually, it won't actually throw if the adapter's
    144      // true limits are higher than we patched above.
    145      enforceDefaultLimits(this, desc);
    146      return await origRequestDeviceFn.call(this, desc);
    147    };
    148  }
    149 
    150  if (globalTestConfig.blockAllFeatures) {
    151    // eslint-disable-next-line @typescript-eslint/unbound-method
    152    const origRequestAdapterFn = impl.requestAdapter;
    153    // eslint-disable-next-line @typescript-eslint/unbound-method
    154    const origRequestDeviceFn = GPUAdapter.prototype.requestDevice;
    155 
    156    Object.defineProperty(impl, 'requestAdapter', {
    157      configurable: true,
    158      async value(options?: GPURequestAdapterOptions) {
    159        const adapter = await origRequestAdapterFn.call(this, {
    160          ...defaultRequestAdapterOptions,
    161          ...options,
    162        });
    163        if (adapter) {
    164          Object.defineProperty(adapter, 'features', {
    165            enumerable: false,
    166            value: new Set(
    167              hasFeature(adapter.features, 'core-features-and-limits')
    168                ? ['core-features-and-limits']
    169                : []
    170            ),
    171          });
    172        }
    173        return adapter;
    174      },
    175    });
    176 
    177    const enforceBlockedFeatures = (adapter: GPUAdapter, desc: GPUDeviceDescriptor | undefined) => {
    178      if (desc?.requiredFeatures) {
    179        for (const [feature] of desc.requiredFeatures) {
    180          // Note: This adapter has had its features property over-ridden and will only return
    181          // have nothing or 'core-features-and-limits'.
    182          // eslint-disable-next-line no-restricted-syntax
    183          if (!adapter.features.has(feature)) {
    184            throw new TypeError(`requested feature ${feature} does not exist on adapter`);
    185          }
    186        }
    187      }
    188    };
    189 
    190    GPUAdapter.prototype.requestDevice = async function (
    191      this: GPUAdapter,
    192      desc?: GPUDeviceDescriptor | undefined
    193    ) {
    194      // We need to enforce the feature block because even though we patched the adapter to
    195      // advertise no features they still exist on the real adapter.
    196      enforceBlockedFeatures(this, desc);
    197      return await origRequestDeviceFn.call(this, desc);
    198    };
    199  }
    200 
    201  if (defaultRequestAdapterOptions) {
    202    // eslint-disable-next-line @typescript-eslint/unbound-method
    203    const origRequestAdapterFn = impl.requestAdapter;
    204 
    205    // eslint-disable-next-line @typescript-eslint/unbound-method
    206    Object.defineProperty(impl, 'requestAdapter', {
    207      configurable: true,
    208      async value(options?: GPURequestAdapterOptions) {
    209        const adapter = await origRequestAdapterFn.call(this, {
    210          ...defaultRequestAdapterOptions,
    211          ...options,
    212        });
    213        if (recorder && adapter) {
    214          const adapterInfo = adapter.info;
    215          const infoString = `Adapter: ${adapterInfo.vendor} / ${adapterInfo.architecture} / ${adapterInfo.device}`;
    216          recorder.debug(new ErrorWithExtra(infoString, () => ({ adapterInfo })));
    217        }
    218        return adapter;
    219      },
    220    });
    221  }
    222 
    223  return impl;
    224 }