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 }