この記事の目次
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
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。