WebGPU でサポートされる MSAA サンプル数を検出する

MSAA アンチエイリアシングの記事 では、サンプル数ごとにレンダーパイプラインを事前作成して WebGPU ラインプロットに マルチサンプルアンチエイリアシングを追加する方法を示した。しかし実際に どのサンプル数が使えるのか?WebGPU 仕様が保証するのは 1× と 4× のみ——2×、8×、16× はアダプタ依存。サポートされないサンプル数で パイプラインを作成すると、エラーなくコンパイルされても 表示出力が出ないことがある。

この記事では、各候補サンプル数をテストして結果を HTML リストとして 描画する、最小限の依存なしプローブを示す。下のライブデモは /scripts/msaa-levels-webgpu.js から まったく同じスクリプトを読み込んでいる。

MSAA サポートをプローブ中…

上のリストは各サンプル数を createRenderPipelineAsync で プローブして生成される。WebGPU 対応ブラウザ(Chrome / Edge 113+)が必要。

動作の仕組み

プローブは単純:各候補サンプル数(1、2、4、8、16)について、 その multisample.count で createRenderPipelineAsync を使い レンダーパイプラインの作成を試みる。Promise が解決すれば サンプル数はサポート、reject されればサポートなし。

probe.js
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つの方法で公開:

  1. HTML リスト — サポート数ごとに <li> を持つ <ul>、 各 <li> は data-sample-count="N" でタグ付け。CSS セレクタや querySelectorAll で個別エントリをクエリできる。
  2. カスタムイベント — div に msaalevels イベントが ディスパッチされ、event.detail.levels にサポート数の 配列が入る。グローバルコールバック不要で動作:
    example.js
    document.querySelector('.msaa-levels')
      .addEventListener('msaalevels', (e) => {
        console.log('Supported:', e.detail.levels);
      });
  3. コールバック — div に data-onlevels="functionName" 属性がある場合、window[functionName](supported) が 配列で呼び出される。direction-cubes の記事 で 使われているのと同じコールバックパターン。

自分のページで使う

/scripts/msaa-levels-webgpu.js を静的フォルダに入れ、msaa-levels クラスの div を追加:

example.html
<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 リストを読めばよい。

完全なソース

msaa-levels-webgpu.js
// 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 と同じもの。


同じカテゴリの類似記事をチェック: JavaScript WebGPU Web