|
58 | 58 | let listEl: HTMLDivElement | null = $state(null); |
59 | 59 | let textareaEl: HTMLTextAreaElement | null = $state(null); |
60 | 60 |
|
| 61 | + const getWebGPU = () => |
| 62 | + typeof navigator === 'undefined' ? null : ((navigator as Navigator & { gpu?: GPU }).gpu ?? null); |
| 63 | +
|
| 64 | + const installWebGPUAdapterFallback = () => { |
| 65 | + const gpu = getWebGPU() as (GPU & { __betterzeriyaAdapterFallback?: true }) | null; |
| 66 | + if (!gpu || gpu.__betterzeriyaAdapterFallback) return; |
| 67 | +
|
| 68 | + const requestAdapter = gpu.requestAdapter.bind(gpu); |
| 69 | + try { |
| 70 | + Object.defineProperty(gpu, 'requestAdapter', { |
| 71 | + configurable: true, |
| 72 | + value: async (options?: GPURequestAdapterOptions) => { |
| 73 | + const adapter = await requestAdapter(options); |
| 74 | + if (adapter || !options?.powerPreference) { |
| 75 | + return adapter; |
| 76 | + } |
| 77 | + return ( |
| 78 | + (await requestAdapter()) ?? |
| 79 | + (await requestAdapter({ |
| 80 | + powerPreference: |
| 81 | + options.powerPreference === 'high-performance' ? 'low-power' : 'high-performance' |
| 82 | + })) |
| 83 | + ); |
| 84 | + } |
| 85 | + }); |
| 86 | + gpu.__betterzeriyaAdapterFallback = true; |
| 87 | + } catch {} |
| 88 | + }; |
| 89 | +
|
61 | 90 | const isCloseToBottom = () => { |
62 | 91 | if (!listEl) return true; |
63 | 92 | return listEl.scrollHeight - listEl.scrollTop - listEl.clientHeight < 80; |
|
69 | 98 | }; |
70 | 99 |
|
71 | 100 | const detectWebGPU = () => { |
72 | | - if (typeof navigator === 'undefined') return false; |
73 | | - return 'gpu' in navigator; |
| 101 | + return Boolean(getWebGPU()); |
| 102 | + }; |
| 103 | +
|
| 104 | + const verifyWebGPUAdapter = async () => { |
| 105 | + const gpu = getWebGPU(); |
| 106 | + if (!gpu) return false; |
| 107 | + const adapter = |
| 108 | + (await gpu.requestAdapter({ powerPreference: 'high-performance' })) ?? |
| 109 | + (await gpu.requestAdapter()) ?? |
| 110 | + (await gpu.requestAdapter({ powerPreference: 'low-power' })); |
| 111 | + return Boolean(adapter); |
74 | 112 | }; |
75 | 113 |
|
76 | 114 | const loadEngine = async () => { |
|
80 | 118 | loadProgress = 0; |
81 | 119 | loadText = 'モデルを準備しています…'; |
82 | 120 | try { |
| 121 | + installWebGPUAdapterFallback(); |
83 | 122 | const { CreateMLCEngine, prebuiltAppConfig } = await import('@mlc-ai/web-llm'); |
84 | 123 | const next = await CreateMLCEngine(selectedModel, { |
85 | 124 | initProgressCallback: (report: { progress: number; text: string }) => { |
|
95 | 134 | loadText = '準備完了'; |
96 | 135 | } catch (caught) { |
97 | 136 | engine = null; |
98 | | - error = caught instanceof Error ? caught.message : 'モデル読み込みに失敗しました'; |
| 137 | + const message = caught instanceof Error ? caught.message : 'モデル読み込みに失敗しました'; |
| 138 | + error = message.includes('Unable to find a compatible GPU') |
| 139 | + ? 'WebGPU adapter を取得できませんでした。ブラウザのハードウェアアクセラレーション、GPUドライバ、chrome://gpu の WebGPU 状態を確認してください。' |
| 140 | + : message; |
99 | 141 | } finally { |
100 | 142 | engineLoading = false; |
101 | 143 | } |
|
205 | 247 | webgpuSupported = detectWebGPU(); |
206 | 248 | if (!webgpuSupported) { |
207 | 249 | error = 'このブラウザは WebGPU に未対応です。Chrome / Edge デスクトップ版でお試しください。'; |
| 250 | + return; |
208 | 251 | } |
| 252 | + installWebGPUAdapterFallback(); |
| 253 | + void verifyWebGPUAdapter().then((supported) => { |
| 254 | + webgpuSupported = supported; |
| 255 | + if (!supported) { |
| 256 | + error = |
| 257 | + 'WebGPU adapter を取得できませんでした。ブラウザのハードウェアアクセラレーション、GPUドライバ、chrome://gpu の WebGPU 状態を確認してください。'; |
| 258 | + } |
| 259 | + }); |
209 | 260 | }); |
210 | 261 | </script> |
211 | 262 |
|
|
0 commit comments