WebGPU未対応の場合にカスタマイズ可能なエラーメッセージをReact Contextでブラウザ画面上に表示する

# WebGPUが未サポートのブラウザでは「使えません」と表示したい

WebGPUを使うアプリのつくりかたを最初に学ぶ際には、次のようなコードを目にすることだろうと思います。

```JavaScript
async function init() {
  if (!navigator.gpu) {
    throw Error("WebGPU not supported.");
  }

  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    throw Error("Couldn't request WebGPU adapter.");
  }

  // Create a GPUDevice
  let device = await adapter.requestDevice(descriptor);

  // Use lost to handle lost devices
  device.lost.then((info) => {
    console.error(`WebGPU device was lost: ${info.message}`);
    device = null;

    if (info.reason !== "destroyed") {
      init();
    }
  });

  // ...
}
```

mdn web docsの [GPUDevice: lost property](https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/lost) や、Google codelabsの「[初めてのWebGPUアプリ 3. WebGPU を初期化する](https://codelabs.developers.google.com/your-first-webgpu-app?hl=ja#2)」などで、こうした内容でのコードが示されています。要するに、navigator.gpuプロパティが存在するかどうかをチェックし、WebGPUが未サポートであることを検出し、consoleに（こっそりと）エラーを出力する、という動作です。


ただし、通常のユーザーがデベロッパーコンソールを開いていることは期待できません。また、プログラムの内容によっては、単にデベロッパーコンソールを開いているだけでも処理のパフォーマンスが顕著に低下します。

WebGPUが未サポートのブラウザを使っているユーザに対しては、画面上で明示的に「この環境では使えません」という内容の警告を表示してあげるようにしたいところです。さらには、コンピュートシェーダーで処理をするような内容では、WebAssemblyを用いた実装にフォールバックする、画面描画はcanvasを用いた実装にフォールバックする...というような作り込みをすることについても、検討をするべきかもしれません。

というわけで、React Contextで、WebGPUが利用できる場合にだけGPUDeviceのインスタンスを提供するとともに、WebGPUが利用できない場合にはReactNodeで指定したエラーメッセージを画面に表示できるようなものを作ることにしました。

# useWebGPUDevice.tsx

以下のコードをコピペして使うか、NPMパッケージとしてインストールしてください。

```bash
pnpm add react-webgpu-context
```

ライブラリをGitHubリポジトリとしても公開しています。

https://github.com/kubohiroya/react-webgpu-context


```TypeScript:useWebGPUDevice.tsx
import React, { ReactNode, useLayoutEffect, useState } from 'react';

type WebGPUDeviceContextType = GPUDevice | null;

export const WebGPUDeviceContext =
  React.createContext<WebGPUDeviceContextType>(null);

export const WebGPUDeviceContextProvider = ({
  loadingMessage,
  notSupportedMessage,
  children,
}: {
  loadingMessage?: ReactNode;
  notSupportedMessage?: ReactNode;
  children?: ReactNode;
}) => {
  const [device, setDevice] = useState<GPUDevice | null>(null);
  const [isWebGPUSupported, setIsWebGPUSupported] = useState<boolean | null>(
    null
  );

  useLayoutEffect(() => {
    (async () => {
      const initWebGPU = async function (
        callback: (device: GPUDevice | undefined) => void
      ) {
        const requestDevice = async (): Promise<GPUDevice | undefined> => {
          if (!navigator.gpu || !navigator.gpu.requestAdapter) {
            setIsWebGPUSupported(false);
            return;
          }
          try {
            const adapter = await navigator.gpu.requestAdapter();
            if (!adapter) {
              setIsWebGPUSupported(false);
              return;
            }

            const device = await adapter.requestDevice();
            device.lost.then(async (info) => {
              console.error(
                `WebGPU device was lost: ${info.message}: ${info.reason}`
              );

              // 'reason' will be 'destroyed' if we intentionally destroy the device.
              if (info.reason !== 'destroyed') {
                // try again
                console.error('Trying to recreate the device...');
                return callback(await requestDevice());
              }
            });
            return device;
          } catch (ex: any) {
            console.error('Trying to recreate the device...' + ex);
            return await requestDevice();
          }
        };

        callback(await requestDevice());
      };
      await initWebGPU((device: GPUDevice | undefined) => {
        if (!navigator.gpu || !device) {
          setIsWebGPUSupported(false);
          return;
        }
        setIsWebGPUSupported(true);
        setDevice(device);
      });
    })();

    return () => {
      device?.destroy();
    };
  }, []);

  if (isWebGPUSupported === null) {
    return loadingMessage ? loadingMessage : <p>Loading...</p>;
  }
  if (!isWebGPUSupported) {
    return notSupportedMessage ? (
      notSupportedMessage
    ) : (
      <p>WebGPU is not supported on this browser.</p>
    );
  }

  return !device ? null : (
    <WebGPUDeviceContext.Provider value={device}>
      {children}
    </WebGPUDeviceContext.Provider>
  );
};

export const useWebGPUDevice = () => {
  const device = React.useContext(WebGPUDeviceContext);
  if (device == null) {
    throw new Error(
      'useWebGPUDevice must be used within a WebGPUDeviceContextProvider'
    );
  }
  return device;
};

```

# WebGPUDeviceContextProviderの設置

WebGPUを使うコンポーネント(複数個のWebGPUを使うコンポーネント)の祖先要素として、`WebGPUDeviceContextProvider`コンポーネントを設置してください。
このとき、次のようなプロパティを指定できます。

* `loadingMessage`プロパティ: 読み込み中に表示するコンポーネント
* `notSupportedMessage`プロパティ: WebGPUが使えない環境であることを表示するコンポーネント

コード例は次のようになります。

```TypeScript:app.tsx
import { WebGPUDeviceContextProvider } from './useWebGPUDevice';
import {WebGPUApp} from './WebGpuApp'
export const App = ()=>{
  return (
    <WebGPUDeviceContextProvider
      loadingMessage={(<p>読み込み中</p>)}
      notSupportedMessage={(<p>あなたのブラウザではWebGPUが利用できません</p>)}>
        <WebGPUApp />
    </WebGPUDeviceContextProvider>
  );
}
```

このようにWebGPUDeviceContextProviderを配置することで、WebGPUアプリを使える場合・使えない場合のそれぞれに対応したコンポーネントを表示できるようになります。

# React hookによるGPUDeviceインスタンスの参照

WebGPUを用いるアプリ内では、次のように、`useWebGPUDevice`フックを利用すると、祖先の`WebGPUDeviceContextProvider`で取得済みの`GPUDevice`のインスタンスを参照することができます。

```TypeScript:WebGPUApp.tsx
import { useWebGPUDevice } from './useWebGPUDevice';

export const WebGPUApp = ()=>{
  const device: GPUDevice = useWebGPUDevice();
  // deviceを使っていろいろする
  // 略
}
```

# 利用例

この記事で紹介した`WebGPUDeviceContextProvider`の中に配置した形で、[WebGPUコンピュートシェーダとWebGPUによる画面描画を用いたアプリ](https://kubohiroya.github.io/webgpu-react-bitmap-viewport/examples/index.html)をつくりました。もしも、あなたのブラウザが、WebGPU未対応の場合には、ブラウザの画面上には、次のような表示がなされているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193148/87f99622-d4c2-efee-5934-9ad7cb76e462.png)


この記事はこれで終わりです。