WebGL / WebGPU公開 約16分

WebGPU 対応確認の先へ:初期化失敗とdevice喪失で代替表示を残す

navigator.gpuがあっても描画を続けられるとは限りません。静止図を最初に表示し、adapterなし・device取得失敗・使用中の喪失で戻る経路を作ります。待機中の中止も含め、表示と資源の終了を確かめます。

この記事の目次
  1. 代替図は失敗してから作らず、最初から置く
  2. 失敗条件を選べる、一枚の色見本
  3. 待機中に戻した後、古い結果を表示させない
  4. 描画と資源の終了を、一つの出口へ集める
  5. 表示成功だけでなく、戻った後を記録する

WebGPUのAPIが見つかったので代替画像を消したら、何も見えないままになった。APIの存在は、使えるadapterやdeviceを取得できる保証ではありません。さらに初期化できても、その後にdeviceを失う場合があります。

async関数とDOM操作が分かる人向けに、架空の色見本を作ります。WebGPUで一色をクリア描画し、使えない間は同じ色の静止図を残します。WebGPU APIを使いますがシェーダーは作らず、WGSL、バッファ配置、性能比較は対象外です。成功する環境だけを前提にしない表示の順序を練習します。

代替図は失敗してから作らず、最初から置く

初期状態は静止図です。API確認、adapter取得、device取得、canvas設定、初回の描画命令を送る段階を通ってからcanvasへ切り替えます。初期化に時間がかかっても、利用者には見本と説明が残ります。

requestAdapterの資料では、adapterを取得できずnullになる場合があります。requestDeviceの資料のPromiseも失敗を考慮します。存在確認だけでなく、それぞれの出口を表示へ接続します。

条件 表示 次の操作
初期状態・初期化中 静止図 初期化中なら中止できる
初回の描画まで進んだ canvas 静止図へ戻せる
APIなし・取得失敗 静止図と理由 条件を変えて再試行
device喪失 静止図と案内 自動で繰り返さず再試行

device.lostの資料に沿って、取得後すぐに監視を登録します。catchは取得途中の失敗を、lostは取得後の寿命の終了を扱います。同じ「使えない」でも検出する場所が異なります。

失敗条件を選べる、一枚の色見本

次をgpu.htmlへ保存します。同じフォルダでpython3 -m http.server 8766 --bind 127.0.0.1を実行し、http://127.0.0.1:8766/gpu.htmlを開きます。公開先ではHTTPSなど安全なコンテキストが必要です。通常のHTTPサイトでAPIがない結果と、対応するブラウザーでない結果を混同しません。

<!doctype html>
<html lang="ja"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>色見本の代替表示</title>
<style>
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 560px; margin: auto; }
.frame { width: 100%; aspect-ratio: 2; position: relative; }
canvas, svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
[hidden] { display: none; }
button, select { font: inherit; max-width: 100%; margin: 4px 0; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style></head><body><main>
<h1>色見本(架空の練習)</h1>
<p>橙色の面を表示します。静止図でも同じ見本を確認できます。</p>
<div class="frame">
<svg id="fallback" role="img" aria-label="橙色の静止見本" viewBox="0 0 2 1">
  <path fill="#ff8000" d="M0 0h2v1H0z" />
</svg>
<canvas id="canvas" aria-label="WebGPUによる橙色の見本" hidden></canvas>
</div>
<p id="status" role="status">静止図を表示しています。</p>
<label for="mode">検証する条件</label>
<select id="mode">
<option value="normal">通常</option>
<option value="absent">APIなしを模擬</option>
<option value="null">adapterなしを模擬</option>
<option value="reject">device取得失敗を模擬</option>
<option value="delay">取得後に2秒待つ</option>
</select>
<button id="start" type="button">WebGPUを試す</button>
<button id="stop" type="button">静止図へ戻す</button>
<button id="lose" type="button" disabled>deviceを破棄して喪失を試す</button>
</main><script type="module">
const canvas = document.querySelector('#canvas');
const fallback = document.querySelector('#fallback');
const status = document.querySelector('#status');
const mode = document.querySelector('#mode');
const start = document.querySelector('#start');
const stop = document.querySelector('#stop');
const lose = document.querySelector('#lose');
let generation = 0;
let active = null;
function showStatic(message) {
  canvas.hidden = true;
  fallback.hidden = false;
  status.textContent = message;
}
function finish(message) {
  generation++;
  const previous = active;
  active = null;
  previous?.dispose();
  start.disabled = false;
  lose.disabled = true;
  showStatic(message);
}
async function run(condition) {
  finish('静止図を残して初期化しています。');
  const own = generation;
  start.disabled = true;
  let device;
  let context;
  let observer;
  let disposed = false;
  const dispose = () => {
    if (disposed) return;
    disposed = true;
    observer?.disconnect();
    context?.unconfigure();
    device?.destroy();
  };
  try {
    const gpu = condition === 'absent' ? null : navigator.gpu;
    if (!gpu) throw new Error('WebGPU APIを利用できません。');
    const adapter = condition === 'null' ? null : await gpu.requestAdapter();
    if (own !== generation) return;
    if (!adapter) throw new Error('adapterを取得できません。');
    if (condition === 'reject') throw new Error('device取得失敗の模擬です。');
    device = await adapter.requestDevice();
    if (condition === 'delay') await new Promise(resolve => setTimeout(resolve, 2000));
    if (own !== generation) return;
    active = { device, dispose };
    device.lost.then(() => {
      if (own === generation) finish('deviceを失ったため静止図へ戻しました。');
    });
    context = canvas.getContext('webgpu');
    if (!context) throw new Error('canvasを設定できません。');
    context.configure({ device, format: gpu.getPreferredCanvasFormat(), alphaMode: 'opaque' });
    function draw() {
      const rect = canvas.parentElement.getBoundingClientRect();
      const dpr = Math.min(devicePixelRatio, 2);
      const limit = device.limits.maxTextureDimension2D;
      canvas.width = Math.max(1, Math.min(limit, Math.round(rect.width * dpr)));
      canvas.height = Math.max(1, Math.min(limit, Math.round(rect.height * dpr)));
      const encoder = device.createCommandEncoder();
      const pass = encoder.beginRenderPass({ colorAttachments: [{
        view: context.getCurrentTexture().createView(),
        clearValue: { r: 1, g: 0.5, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store'
      }] });
      pass.end();
      device.queue.submit([encoder.finish()]);
    }
    device.pushErrorScope('validation');
    draw();
    const error = await device.popErrorScope();
    if (own !== generation) return;
    if (error) throw new Error('初回の描画を検証できません。');
    device.addEventListener('uncapturederror', () => {
      if (own === generation) finish('描画エラーのため静止図へ戻しました。');
    });
    observer = new ResizeObserver(() => {
      if (own !== generation) return;
      try { draw(); } catch { finish('再描画できず静止図へ戻しました。'); }
    });
    observer.observe(canvas.parentElement);
    canvas.hidden = false;
    fallback.hidden = true;
    status.textContent = 'WebGPUで色見本を表示しています。';
    lose.disabled = false;
  } catch (error) {
    if (own === generation) finish(error.message);
  } finally {
    if (own !== generation || active?.device !== device) dispose();
    if (own === generation) start.disabled = false;
  }
}
start.addEventListener('click', () => run(mode.value));
stop.addEventListener('click', () => finish('静止図へ戻しました。'));
lose.addEventListener('click', () => active?.device.destroy());
window.addEventListener('pagehide', () => finish('静止図を表示しています。'));
</script></body></html>

「APIなし」「adapterなし」「device取得失敗」は分岐を確かめる模擬条件で、実機の障害を発生させるものではありません。一方、喪失ボタンは取得したdeviceのdestroyを呼びます。実機の突然の故障とは原因が異なりますが、lostから代替表示へ戻る経路を確認できます。

待機中に戻した後、古い結果を表示させない

generationは一回の初期化を識別する番号です。静止図へ戻すたび番号を進め、awaitの後に自分の番号と比較します。取得中のPromise自体を中止する処理ではありません。後から返ったdeviceを採用せず、finallyで破棄します。

第一の失敗はこの比較を外すことです。「取得後に2秒待つ」で開始し、待機中に静止図へ戻しても、遅れて終わった処理がcanvasを表示してしまいます。比較を戻し、二秒より長く待っても静止図が残ることを再確認します。失敗だけでなく、不要になった成功も捨てる必要があります。

第二の失敗は、device取得をtryとcatchで囲むだけにしてlostを監視しないことです。初期化後の喪失では取得時のcatchへ戻りません。監視を戻し、通常で表示した後に喪失ボタンを押して、静止図と喪失の案内が出るか確かめます。

描画と資源の終了を、一つの出口へ集める

finishは先に古い処理を無効にし、ResizeObserverを解除し、canvasの設定と専用deviceを終了します。WebGPU仕様の資源の扱いに合わせ、失敗経路でも参照を捨てるだけで終えません。正常な終了によるlostが後から来ても、古い番号なので表示を上書きしません。

この例は専用deviceを一つ持ちます。複数部品で共有するdeviceを一部品の終了で破棄する構成にはしないでください。所有者を決める考え方はWebGLの資源解放の記事と共通ですが、APIは異なります。

canvasの表示寸法から描画寸法を計算し、DPRを2で制限します。幅の変更は監視しますが、モニター移動などDPRだけの変化を常時監視する実装ではありません。その場合は試すボタンで作り直します。動く絵ではないためアニメーションや動きを減らす設定への分岐はありません。

実際の3D作品では、同じ色の面だけでは情報を代替できません。形や説明が分かる静止画と文章を用意し、失われる操作も示します。この練習の静止図は、表示切替の確認に必要な最小の題材です。

表示成功だけでなく、戻った後を記録する

15〜20分の目安で、APIなし、adapterなし、取得失敗では静止図が残ることを確認します。通常で動く環境なら、喪失ボタンで静止図へ戻る、待機中に戻したら遅れてcanvasが出ない、もう一度通常で試せる、という三条件も記録します。

今回の動作確認ではソフトウェアGPUによる実行と模擬条件を区別します。ブラウザー・OS・描画環境を記録し、実機GPUの性能やすべての喪失原因を確認したとは扱いません。通常が使えない環境でも、静止図と説明が読めれば代替表示の役割は残ります。カリキュラムの描画課題へ進む際も、成功時の画面と同じように、表示できなかった後の画面を完成条件に含められます。

TRY A LESSON

記事の内容を、課題で確かめる

前提になる表示サイズの練習 →

WebGL・3Dの発展課題 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る