MSAA アンチエイリアシングの記事 では、サンプル数ごとにレンダーパイプラインを事前作成して WebGPU ラインプロットに マルチサンプルアンチエイリアシングを追加する方法を示した。しかし実際に どのサンプル数が使えるのか?WebGPU 仕様が保証するのは 1× と 4× のみ——2×、8×、16× はアダプタ依存。サポートされないサンプル数で パイプラインを作成すると、エラーなくコンパイルされても 表示出力が出ないことがある。
この記事では、各候補サンプル数をテストして結果を HTML リストとして
描画する、最小限の依存なしプローブを示す。下のライブデモは
/scripts/msaa-levels-webgpu.js から
まったく同じスクリプトを読み込んでいる。
上のリストは各サンプル数を createRenderPipelineAsync で
プローブして生成される。WebGPU 対応ブラウザ(Chrome / Edge 113+)が必要。
動作の仕組み
プローブは単純:各候補サンプル数(1、2、4、8、16)について、
その multisample.count で createRenderPipelineAsync を使い
レンダーパイプラインの作成を試みる。Promise が解決すれば
サンプル数はサポート、reject されればサポートなし。
for (const sc of SAMPLE_COUNTS) {
try {
await device.createRenderPipelineAsync({
layout: 'auto',
vertex: { module, entryPoint: 'vs_main' },
fragment: { module, entryPoint: 'fs_main', targets: [{ format }] },
primitive: { topology: 'triangle-list' },
multisample: { count: sc },
});
supported.push(sc);
} catch {
// サポートされないサンプル数 — スキップ。
}
}プローブにシェーダの内容は関係ない——サンプル数が受け入れられるかを テストする有効なレンダーパイプラインだけが必要。最も単純な パイプライン(ソリッドカラーのフラグメントシェーダを持つ 全画面三角形1枚)で十分。
なぜ createRenderPipelineAsync?
同期版の createRenderPipeline はバリデーションエラーでthrow しない——
無効なパイプラインオブジェクトを返しコンソールにログするだけ。
これではプローブに頼れない:コンソールを調べるかエラースコープ API を
使う必要がある。一方 createRenderPipelineAsync はバリデーション
エラーで reject する Promise を返すため、プローブの最も
ポータブルな方法。WebGPU ローンチ時から利用可能で、
pushErrorFilter/popErrorFilter のようなまだ広く出荷されていない
新しい API と違う。
JavaScript から結果にアクセス
スクリプトはサポートされるサンプル数を3つの方法で公開:
- HTML リスト — サポート数ごとに
<li>を持つ<ul>、 各<li>はdata-sample-count="N"でタグ付け。CSS セレクタやquerySelectorAllで個別エントリをクエリできる。 - カスタムイベント — div に
msaalevelsイベントが ディスパッチされ、event.detail.levelsにサポート数の 配列が入る。グローバルコールバック不要で動作:example.jsdocument.querySelector('.msaa-levels') .addEventListener('msaalevels', (e) => { console.log('Supported:', e.detail.levels); }); - コールバック — div に
data-onlevels="functionName"属性がある場合、window[functionName](supported)が 配列で呼び出される。direction-cubes の記事 で 使われているのと同じコールバックパターン。
自分のページで使う
/scripts/msaa-levels-webgpu.js を静的フォルダに入れ、msaa-levels クラスの div を追加:
<div class="msaa-levels" data-onlevels="onMsaaLevels"></div>
<script>
function onMsaaLevels(levels) {
console.log('Supported MSAA sample counts:', levels);
}
</script>
<script src="/scripts/msaa-levels-webgpu.js" defer></script>data-onlevels 属性は任意——あれば指定された関数がサポート
サンプル数の配列で呼ばれる。なくても msaalevels カスタム
イベントをリッスンするか、描画された HTML リストを読めばよい。
完全なソース
// SPDX-FileCopyrightText: 2026 Uli Köhler <gitlab@techoverflow.net>
// SPDX-License-Identifier: CC0-1.0
//
// 最小限の WebGPU MSAA サポートサンプル数プローブ。
//
// 現在の GPU アダプタがサポートする MSAA サンプル数(1、2、4、8、16)を
// プローブし、結果をページ上の各 <div class="msaa-levels"> 内に
// HTML <ul> リストとして描画する。
//
// リストは JavaScript から以下でアクセス可能:
// 1. data-onlevels="functionName" で指定したコールバック —
// サポート数の配列(例:[1, 2, 4, 8])で呼び出される。
// 2. div にディスパッチされるカスタム 'msaalevels' イベント。
// event.detail.levels に配列が入る。
//
// これは interactive-msaa-antialiasing-for-webgpu-line-plots の
// 記事で使われているのと同じプローブ技術:各候補サンプル数で
// createRenderPipelineAsync によるレンダーパイプラインを作成。
// Promise が reject されれば、そのサンプル数はこのアダプタで
// サポートされていない。
//
// なぜ IIFE? すべてを即時実行関数式でラップし、ヘルパーや定数、
// WGSL ソースがグローバルスコープに漏れないようにする。スクリプトは
// <script src=... defer> でそのままインクルードされる想定で、
// 同じページの他のスクリプトと共存する可能性があるため、
// 分離が重要。
(function () {
'use strict';
// ─────────────────────────────────────────────────────────────────────
// Configuration
// ─────────────────────────────────────────────────────────────────────
// WebGPU 仕様が保証するのは 1× と 4× のみ;2×、8×、16× は
// アダプタ依存。5つすべてをプローブし成功したものを報告する。
// 昇順なので描画されるリストが自然に読める。
const SAMPLE_COUNTS = [1, 2, 4, 8, 16];
// ─────────────────────────────────────────────────────────────────────
// プローブ用の最小限 WGSL シェーダ
// ─────────────────────────────────────────────────────────────────────
// プローブにシェーダの内容は関係ない——サンプル数が受け入れられるかを
// テストする有効なレンダーパイプラインだけが必要。これは最も単純な
// レンダーパイプライン:ソリッドカラーのフラグメントシェーダを持つ
// 三角形1枚。実際には描画されない;createRenderPipelineAsync が
// 成功するか reject するかだけが重要。
const SHADER = /* wgsl */ `
@vertex
fn vs_main(@builtin(vertex_index) vi : u32) -> @builtin(position) vec4<f32> {
// 全画面を覆う三角形1枚 — 描画はされない、
// 有効な頂点シェーダであることだけが必要。
let pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
return vec4<f32>(pos[vi], 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}
`;
// ─────────────────────────────────────────────────────────────────────
// サポートされるサンプル数をプローブ
// ─────────────────────────────────────────────────────────────────────
// 各候補サンプル数で、その sampleCount を持つレンダーパイプラインの
// 作成を試みる。createRenderPipelineAsync はバリデーションエラー
// (サポートされないサンプル数を含む)で reject する Promise を返す。
// これはプローブの最もポータブルな方法——WebGPU ローンチ時から
// 利用可能で、pushErrorFilter/popErrorFilter のようなまだ広く
// 出荷されていない新しい API と違う。
//
// プローブパイプラインは layout: 'auto' と自明なシェーダで作成——
// 受け入れテスト以外には必要ない。GPU メモリを解放するため
// プローブ直後に破棄される。
//
// 戻り値:サポートされるサンプル数の配列(昇順)に解決する
// Promise。例:[1, 4] や [1, 2, 4, 8]。
async function probeSampleCounts(device, format) {
const module = device.createShaderModule({ code: SHADER });
const supported = [];
for (const sc of SAMPLE_COUNTS) {
let pipeline = null;
try {
pipeline = await device.createRenderPipelineAsync({
layout: 'auto',
vertex: { module, entryPoint: 'vs_main' },
fragment: { module, entryPoint: 'fs_main', targets: [{ format }] },
primitive: { topology: 'triangle-list' },
multisample: { count: sc },
});
supported.push(sc);
} catch {
// サポートされないサンプル数 — 静かにスキップ。
} finally {
// パイプラインオブジェクトはプローブ専用;GPU メモリを
// 解放するため破棄する。createRenderPipelineAsync は
// destroy() メソッドを持たない GPURenderPipeline を返すが、
// 参照が落ちれば GPU リソースはガベージコレクトされる。
// GC を助けるため参照を null にする。
pipeline = null;
}
}
return supported;
}
// ─────────────────────────────────────────────────────────────────────
// サポートレベルリストを div に描画
// ─────────────────────────────────────────────────────────────────────
// サポートサンプル数ごとに <li> を持つ HTML <ul> を構築。
// 各 <li> は数を "N×" で表示し、JS が個別エントリをクエリできるよう
// data-sample-count="N" でタグ付け。
//
// div にはさらに以下が送られる:
// - detail.levels = サポート配列を持つカスタム 'msaalevels' イベント
// - data-onlevels="functionName" 経由のコールバック(指定時)
function renderList(div, supported) {
// <ul> リストを構築。
const ul = document.createElement('ul');
ul.className = 'msaa-levels-list';
for (const sc of supported) {
const li = document.createElement('li');
li.textContent = sc + '\u00d7'; // × 文字を使った "N×"
li.setAttribute('data-sample-count', sc);
ul.appendChild(li);
}
// div の内容をリストで置き換え。
div.innerHTML = '';
div.appendChild(ul);
// 外部の JS がグローバルコールバックなしでプローブ結果に
// 反応できるようカスタムイベントをディスパッチ。
div.dispatchEvent(new CustomEvent('msaalevels', {
bubbles: true,
detail: { levels: supported },
}));
// 任意の data-onlevels コールバックを呼び出す。
const cbName = div.getAttribute('data-onlevels');
if (cbName && typeof window[cbName] === 'function') {
window[cbName](supported);
}
}
// ─────────────────────────────────────────────────────────────────────
// div ごとの初期化
// ─────────────────────────────────────────────────────────────────────
// ページ上の各 <div class="msaa-levels"> に対し1回呼ばれる。
// WebGPU アダプタ+デバイスを要求し、全候補サンプル数をプローブし、
// 結果を HTML リストとして描画。
async function initDiv(div) {
// プローブ中にロード中プレースホルダを表示。
div.textContent = 'Probing MSAA support\u2026';
if (!navigator.gpu) {
div.innerHTML = '<p>WebGPU is not supported in this browser.</p>';
div.dispatchEvent(new CustomEvent('msaalevels', {
bubbles: true,
detail: { levels: [], error: 'no-webgpu' },
}));
return;
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
div.innerHTML = '<p>No WebGPU adapter available.</p>';
div.dispatchEvent(new CustomEvent('msaalevels', {
bubbles: true,
detail: { levels: [], error: 'no-adapter' },
}));
return;
}
const device = await adapter.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
// 全候補サンプル数をプローブ。
const supported = await probeSampleCounts(device, format);
// デバイスは用済み——解放。プローブパイプラインはすでに
// ガベージコレクト可能(参照は保持しない)。
device.destroy();
// リストを描画しリスナに通知。
renderList(div, supported);
}
// ─────────────────────────────────────────────────────────────────────
// 自動初期化
// ─────────────────────────────────────────────────────────────────────
// ページ上のすべての <div class="msaa-levels"> を見つけて初期化。
// ドキュメントがまだ読み込み中(スクリプトが <head> からまたは
// defer で実行)なら、div が存在するよう DOMContentLoaded を待つ。
// ドキュメントがすでに準備完了(スクリプトが後から注入)なら
// 即座に初期化。
function initAll() {
document.querySelectorAll('div.msaa-levels').forEach(initDiv);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();スクリプトと上のインラインリスティングは同一で、両方とも
CC0-1.0 で公開。プローブ技術は、サポートされるサンプル数の
レンダーパイプラインを事前作成する MSAA アンチエイリアシングの記事
や、MSAA と非 MSAA レンダリングパスを選択する
WebGCodeViewer プロジェクトの
ToolpathRenderer と同じもの。