triangle.wgsl

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) color: vec3f,
};
@vertex fn vertexMain(@builtin(vertex_index) index: u32) -> VertexOutput {
  let positions = array<vec2f, 3>(vec2f(0.0, 0.65), vec2f(-0.6, -0.55), vec2f(0.6, -0.55));
  let colors = array<vec3f, 3>(vec3f(0.66, 0.71, 0.81), vec3f(0.25, 0.55, 0.57), vec3f(0.40, 0.51, 0.69));
  var output: VertexOutput;
  output.position = vec4f(positions[index], 0.0, 1.0);
  output.color = colors[index];
  return output;
}
@fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
  return vec4f(input.color, 1.0);
}

main.js

const button = document.querySelector('#gpu-start');
const status = document.querySelector('#gpu-status');
const canvas = document.querySelector('#gpu-canvas');
const info = document.querySelector('#gpu-info');
let device;
let context;
let failed = false;
function detail(label, value) {
  const dt = document.createElement('dt');
  const dd = document.createElement('dd');
  dt.textContent = label;
  dd.textContent = value;
  info.append(dt, dd);
}
function fail(message) {
  failed = true;
  status.textContent = message;
  context?.unconfigure();
  button.disabled = false;
}
button.addEventListener('click', async () => {
  button.disabled = true;
  failed = false;
  info.replaceChildren();
  status.textContent = 'WebGPUを初期化しています';
  try {
    context?.unconfigure();
    device?.destroy();
    device = undefined;
    if (!window.isSecureContext) throw new Error('HTTPSまたはlocalhostで開いてください');
    if (!navigator.gpu) throw new Error('このブラウザ環境ではWebGPUを利用できません');
    const adapter = await navigator.gpu.requestAdapter();
    if (!adapter) throw new Error('利用可能なGPUアダプターを取得できませんでした');
    const currentDevice = await adapter.requestDevice();
    device = currentDevice;
    currentDevice.lost.then(({ reason, message }) => {
      if (device === currentDevice && reason !== 'destroyed') fail(`GPUデバイスを失いました: ${message || reason}`);
    });
    currentDevice.addEventListener('uncapturederror', event => fail(`GPUエラー: ${event.error.message}`));
    context = canvas.getContext('webgpu');
    if (!context) throw new Error('WebGPUのCanvasを取得できませんでした');
    const format = navigator.gpu.getPreferredCanvasFormat();
    context.configure({ device, format, alphaMode: 'opaque' });
    const shaderResponse = await fetch(new URL('./triangle.wgsl', import.meta.url));
    if (!shaderResponse.ok) throw new Error(`WGSLの取得に失敗しました: ${shaderResponse.status}`);
    const shader = device.createShaderModule({ code: await shaderResponse.text() });
    const compilation = await shader.getCompilationInfo();
    const errors = compilation.messages.filter(message => message.type === 'error');
    if (errors.length) throw new Error(errors.map(message => message.message).join('; '));
    const pipeline = await device.createRenderPipelineAsync({
      layout: 'auto',
      vertex: { module: shader, entryPoint: 'vertexMain' },
      fragment: { module: shader, entryPoint: 'fragmentMain', targets: [{ format }] },
      primitive: { topology: 'triangle-list' },
    });
    device.pushErrorScope('validation');
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({ colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      clearValue: { r: 0.09, g: 0.12, b: 0.14, a: 1 },
      loadOp: 'clear', storeOp: 'store',
    }] });
    pass.setPipeline(pipeline);
    pass.draw(3);
    pass.end();
    device.queue.submit([encoder.finish()]);
    const validation = await device.popErrorScope();
    if (validation) throw new Error(validation.message);
    await device.queue.onSubmittedWorkDone();
    if (failed) return;
    detail('描画形式', format);
    detail('描画サイズ', `${canvas.width} × ${canvas.height}`);
    detail('GPU情報', adapter.info?.description || '機種名を取得できませんでした');
    status.textContent = '描画完了';
    button.textContent = '再実行';
  } catch (error) {
    fail(`実行できませんでした: ${error.message}`);
  } finally {
    button.disabled = false;
  }
});
window.addEventListener('pagehide', () => { context?.unconfigure(); device?.destroy(); });