2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-11

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

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

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 や、Google codelabsの「初めてのWebGPUアプリ 3. WebGPU を初期化する」などで、こうした内容でのコードが示されています。要するに、navigator.gpuプロパティが存在するかどうかをチェックし、WebGPUが未サポートであることを検出し、consoleに(こっそりと)エラーを出力する、という動作です。

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

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

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

useWebGPUDevice.tsx

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

pnpm add react-webgpu-context

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

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が使えない環境であることを表示するコンポーネント

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

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のインスタンスを参照することができます。

WebGPUApp.tsx
import { useWebGPUDevice } from './useWebGPUDevice';

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

利用例

この記事で紹介したWebGPUDeviceContextProviderの中に配置した形で、WebGPUコンピュートシェーダとWebGPUによる画面描画を用いたアプリをつくりました。もしも、あなたのブラウザが、WebGPU未対応の場合には、ブラウザの画面上には、次のような表示がなされているはずです。

image.png

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?