この記事の目次
表示を切り替えるたびにWebGLのbufferやtextureを作るなら、前の部品をいつ終わらせるかも必要です。新しい値を同じ変数へ代入するだけでは、前に作った資源を明示的に片付ける場所がなくなります。作成と解放を、一つの部品の仕事としてまとめてみましょう。
JavaScriptの関数、クロージャー、tryとcatchが分かる人向けです。WebGL 1でbufferとtextureを一つずつ作り、入れ替えと解放を文字で観察します。画像の描画やシェーダーは使いません。GPUのメモリー量を測る教材でもなく、作ったハンドルの有効性と削除の呼び出しを確かめます。
変数を空にする前に、所有者へ片付けを頼む
JavaScriptからWebGLの資源を操作するためのオブジェクトを、ここではハンドルと呼びます。bufferを入れていた変数をnullにしても、その行がdeleteBufferを呼ぶわけではありません。不要になった時点で削除を明示すると、後始末をブラウザーの回収任せにせず書けます。
MDNのWebGLの実装指針でも、不要になったハンドルを早めに削除する方針が紹介されています。ただし削除を要求した瞬間に、使用中の内部資源や物理的なGPUメモリーまで必ず解放済みになる、という意味ではありません。
今回の所有者はcreatePairで作る一組です。その組だけが自分のbufferとtextureを削除します。ページ全体のcanvasや、別の部品が所有する資源まで消しません。イベント登録の後始末と寿命をそろえる考えは似ていますが、リスナーを解除してもWebGLの資源は削除されません。
| 操作 | 呼び出す順番 | 最後に残すもの |
|---|---|---|
| 最初の作成 | 一組を作成 | 新しい一組 |
| 作り直し | 前の組を解放 → 新しい組を作成 | 新しい一組 |
| 解放 | 現在の組を解放 → 参照を外す | なし |
| 作成途中で失敗 | 途中まで作った分を解放 | なし |
失敗する場所も選べる、小さな実験
次をresources.htmlとして保存して開きます。canvasは1×1で非表示です。画面へ描画しないため、DPRやリサイズで描画領域を増やす処理はありません。WebGLが利用できなければ、操作ボタンを無効のままにして文章を残します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGLの作成と解放</title>
<style>
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 640px; margin: auto; }
button { font: inherit; margin: 4px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style>
</head>
<body>
<main>
<h1>資源を一組ずつ作り直す</h1>
<p>描画を行わず、bufferとtextureの寿命を確かめます。</p>
<button id="create" type="button" disabled>作成・作り直し</button>
<button id="release" type="button" disabled>解放</button>
<button id="fail" type="button" disabled>texture作成の失敗を再現</button>
<pre id="result" role="status">JavaScriptとWebGLを確認しています。</pre>
<canvas id="gpu" width="1" height="1" hidden></canvas>
</main>
<script>
const canvas = document.querySelector('#gpu');
const result = document.querySelector('#result');
const buttons = [...document.querySelectorAll('button')];
const gl = canvas.getContext('webgl');
let active = null;
function createPair(failTexture = false) {
let buffer = null;
let texture = null;
let disposed = false;
function dispose() {
if (disposed) return;
disposed = true;
if (texture) gl.deleteTexture(texture);
if (buffer) gl.deleteBuffer(buffer);
}
function describe() {
return `buffer: ${gl.isBuffer(buffer)}, texture: ${gl.isTexture(texture)}`;
}
try {
buffer = gl.createBuffer();
if (!buffer) throw new Error('bufferを作成できません');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
texture = failTexture ? null : gl.createTexture();
if (!texture) throw new Error('textureを作成できません');
gl.bindTexture(gl.TEXTURE_2D, texture);
if (gl.isContextLost()) throw new Error('WebGLを失いました');
return { dispose, describe };
} catch (error) {
dispose();
throw new Error(error.message + '\n後始末後 ' + describe());
} finally {
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
}
}
function replace(failTexture) {
active?.dispose();
active = null;
try {
active = createPair(failTexture);
result.textContent = '作成後 ' + active.describe();
} catch (error) {
result.textContent = error.message;
}
}
if (gl) {
buttons.forEach(button => { button.disabled = false; });
result.textContent = '現在の組:なし';
document.querySelector('#create').addEventListener('click', () => replace(false));
document.querySelector('#fail').addEventListener('click', () => replace(true));
document.querySelector('#release').addEventListener('click', () => {
if (!active) {
result.textContent = '現在の組:なし';
return;
}
active.dispose();
active.dispose(); // 二重に呼んでも片付けは一度だけ
result.textContent = '解放後 ' + active.describe();
active = null;
});
} else {
result.textContent = 'この環境ではWebGLを利用できません。';
}
canvas.addEventListener('webglcontextlost', () => {
active?.dispose();
active = null;
buttons.forEach(button => { button.disabled = true; });
result.textContent = 'WebGLを失いました。再読み込みしてください。';
});
</script>
</body>
</html>
bufferとtextureをそれぞれ一度バインドしてから、有効性を調べています。作成しただけの状態と混同しないためです。KhronosのWebGL 1仕様で、これらの操作とisBufferなどの判定を確認できます。参照先は編集草案です。今回の例はデータを転送せず、画像や頂点の領域を確保する処理まで進めません。
describeが返すtrue・falseは、APIから見たハンドルの有効性です。メモリー使用量や解放したバイト数ではありません。また、このcanvasではほかの部品が同時にバインド状態を使わないため、finallyでnullへ戻しています。共有の描画エンジンへ組み込むなら、そのエンジンの状態管理に合わせます。
作り直す前の一行を抜くと、前の組を見失う
一つ目の失敗は、replaceの冒頭にあるactive?.dispose()を省くことです。新しい組を代入すると、前の組のdisposeへ届く参照がなくなります。最後の組を解放してfalseが出ても、以前の組に対して削除を呼んだ証明にはなりません。
修正は、参照を外す前に前の組を解放することです。調べるときはdisposeのdisposedを調べた後へ一時的にconsole.count('pair disposed')を入れ、再読み込みしてから「作成・作り直し」を三回、解放を一回押します。カウントは順に、初回なし、1、2、3となるのが期待結果です。
同じdisposeを二度呼ぶと、二度目は先頭で戻ります。deleteBufferとdeleteTextureも、削除済みの対象への再度の削除に効果はありません。それでも関数側で一度にそろえると、後で追加する別の後始末も重複させずに済みます。
完成した組だけを解放すると、途中のbufferが残る
二つ目の失敗は、解放をreplace側にだけ書き、createPair内のcatchからdisposeを外すことです。textureを作る前に失敗すると、完成した組はreturnされません。呼び出し側のactiveはnullのままなので、先に作ったbufferをそこから片付けられません。
修正は、作成を担当する関数内で途中の資源も把握し、失敗時に同じdisposeを呼ぶことです。「texture作成の失敗を再現」を押し、後始末後のbufferとtextureがどちらもfalseになるか見ます。catch内のdisposeを抜いた練習用の版ではbufferがtrueになるため、差を確かめたら元へ戻し、ページを再読み込みしてください。
このボタンはtextureの作成がnullを返す状況を意図的に作っています。実機のメモリー不足を起こしているわけではありません。保存失敗を代役で再現する記事と同様、失敗後の分岐を確かめる手段です。WebGLのすべての異常がJavaScriptの例外になるわけでもないため、catchだけであらゆるGPUエラーを検知したとは扱いません。
最後に解放してから、もう一度作れるか
正常作成では両方true、解放直後では両方false、途中失敗の後も両方falseになるのが三つの判定です。さらに失敗の後に正常作成を押して両方trueへ戻ること、解放を連続して押すと「現在の組:なし」となることを確認します。操作の順序も記録してください。
この例は、部品を作り直す前に古い表示を捨てる方針です。新しいものが完成するまで前の表示を残したいなら、新規作成が成功してから入れ替える設計へ変えます。また、全体で共有するtextureを複数部品が使うなら、一部品の終了だけで削除せず、共有資源の所有者へ返す必要があります。
コンテキスト喪失後の復元や、描画中のアニメーション停止は別の課題です。この教材は再読み込みの案内で止めます。canvasのサイズ調整へ組み込む場合も、表示領域を変える処理と資源の寿命を分けてください。JavaScriptの無料レッスンから部品を増やす際には、作成に対応する後始末を誰が呼ぶかまで一組で説明できれば、同じ考え方を使えます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。