diff --git a/src/style/_app-shell.scss b/src/style/_app-shell.scss index abf8824..f32be5d 100644 --- a/src/style/_app-shell.scss +++ b/src/style/_app-shell.scss @@ -121,12 +121,15 @@ html > body { position: absolute; top: 0; left: 0; + max-width: calc(100% - var(--normal-margin) * 2); margin: var(--normal-margin); z-index: 5; pre { font-size: 20px; color: red; + white-space: pre-wrap; + overflow-wrap: anywhere; } } } diff --git a/src/utils/graphics/initialize-gpu.ts b/src/utils/graphics/initialize-gpu.ts index 9108060..1fdc9bf 100644 --- a/src/utils/graphics/initialize-gpu.ts +++ b/src/utils/graphics/initialize-gpu.ts @@ -15,7 +15,30 @@ const REQUESTED_LIMIT_NAMES = [ 'maxComputeWorkgroupsPerDimension', ] as const satisfies ReadonlyArray; -const getRequiredLimits = ( +interface AdapterRequestAttempt { + label: string; + options?: GPURequestAdapterOptions; +} + +const ADAPTER_REQUEST_ATTEMPTS: ReadonlyArray = [ + { + label: 'compatibility-default', + options: { featureLevel: 'compatibility' }, + }, + { + label: 'core-default', + }, + { + label: 'compatibility-high-performance', + options: { featureLevel: 'compatibility', powerPreference: 'high-performance' }, + }, + { + label: 'core-high-performance', + options: { powerPreference: 'high-performance' }, + }, +] as const; + +const getRelevantLimits = ( limits: GPUSupportedLimits ): Record<(typeof REQUESTED_LIMIT_NAMES)[number], number> => Object.fromEntries(REQUESTED_LIMIT_NAMES.map((name) => [name, limits[name]])) as Record< @@ -42,25 +65,64 @@ const getAdapterInfo = (adapter: GPUAdapter): Record => { } }; -const requestAdapter = async ( - gpu: GPU, - options?: GPURequestAdapterOptions -): Promise => { - try { - return await gpu.requestAdapter(options); - } catch (error) { - throw new RuntimeError( - ErrorCode.WEBGPU_ADAPTER_UNAVAILABLE, - 'Could not request a WebGPU adapter.', - { - cause: error, - details: { - causeMessage: getErrorMessage(error), - powerPreference: options?.powerPreference ?? 'default', - }, - } - ); +const getRequiredFeatures = (adapter: GPUAdapter): Array => { + const requiredFeatures: Array = []; + + if (adapter.features.has('core-features-and-limits')) { + requiredFeatures.push('core-features-and-limits'); } + + return requiredFeatures; +}; + +type AdapterRequestOutcome = 'adapter' | 'unavailable' | 'error'; + +const describeAdapterRequest = ( + attempt: AdapterRequestAttempt, + outcome: AdapterRequestOutcome, + causeMessage?: string +): Record => ({ + label: attempt.label, + featureLevel: attempt.options?.featureLevel ?? 'core', + powerPreference: attempt.options?.powerPreference ?? 'default', + outcome, + ...(causeMessage === undefined ? {} : { causeMessage }), +}); + +const requestAdapter = async ( + gpu: GPU +): Promise<{ + adapter: GPUAdapter | null; + attempts: Array>; +}> => { + const attempts: Array> = []; + + for (const attempt of ADAPTER_REQUEST_ATTEMPTS) { + try { + const adapter = await gpu.requestAdapter(attempt.options); + attempts.push(describeAdapterRequest(attempt, adapter ? 'adapter' : 'unavailable')); + + if (adapter) { + return { adapter, attempts }; + } + } catch (error) { + attempts.push(describeAdapterRequest(attempt, 'error', getErrorMessage(error))); + } + } + + return { adapter: null, attempts }; +}; + +const formatAdapterAttemptSummary = ( + attempts: Array> +): string => { + if (attempts.length === 0) { + return 'No adapter requests were attempted.'; + } + + return `Adapter attempts: ${attempts + .map((attempt) => `${attempt.label}: ${attempt.outcome}`) + .join('; ')}`; }; export const initializeGpu = async (): Promise => { @@ -81,35 +143,43 @@ export const initializeGpu = async (): Promise => { }); } - const adapter = - (await requestAdapter(gpu, { - powerPreference: 'high-performance', - })) ?? (await requestAdapter(gpu)); + const { adapter, attempts } = await requestAdapter(gpu); + ErrorHandler.addMetadata('webgpuAdapterRequest', { + attempts, + selectedAttempt: attempts[attempts.length - 1]?.label ?? null, + }); if (!adapter) { throw new RuntimeError( ErrorCode.WEBGPU_ADAPTER_UNAVAILABLE, - 'WebGPU is available, but this browser could not provide a compatible GPU adapter.' + [ + 'WebGPU is available, but this browser could not provide a compatible GPU adapter.', + formatAdapterAttemptSummary(attempts), + ].join('\n'), + { + details: { + attempts, + hasNavigatorGpu: true, + isSecureContext: window.isSecureContext, + platform: navigator.platform, + userAgent: navigator.userAgent, + }, + } ); } - const requiredLimits = getRequiredLimits(adapter.limits); - const requiredFeatures: Array = []; - if (adapter.features.has('timestamp-query')) { - requiredFeatures.push('timestamp-query'); - } + const requiredFeatures = getRequiredFeatures(adapter); ErrorHandler.addMetadata('webgpuAdapter', { features: Array.from(adapter.features).sort(), info: getAdapterInfo(adapter), requiredFeatures, - requiredLimits, + relevantLimits: getRelevantLimits(adapter.limits), }); let gpuDevice: GPUDevice; try { gpuDevice = await adapter.requestDevice({ requiredFeatures, - requiredLimits, }); } catch (error) { throw new RuntimeError( @@ -120,7 +190,6 @@ export const initializeGpu = async (): Promise => { details: { causeMessage: getErrorMessage(error), requiredFeatures, - requiredLimits, }, } );