HTML 페이지의 WebGPU: GPU로 그리기

WebGPU를 사용하면 웹페이지에서 그래픽 칩을 사용하여 그림을 그리고 계산할 수 있습니다. 캔버스, 하나의 WGSL 셰이더 및 일부 비동기 JavaScript로 충분하며 라이브러리가 없습니다.

영문 원문 보기

HTML 페이지에서 WebGPU를 사용하려면 navigator.gpu를 확인하고 어댑터와 장치를 요청한 후 <canvas>를 해당 장치에 연결하세요. WGSL에 셰이더를 작성하고, 파이프라인을 구축하고, 렌더 패스를 기록하고 이를 GPU 대기열에 제출합니다.

라이브러리나 빌드 단계가 필요하지 않습니다. 다음은 각 모서리에 색상이 있는 삼각형을 그리는 파일입니다.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGPU triangle</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  canvas { display: block; width: 100%; height: 230px; border-radius: 10px; background: #111827; }
  .bar { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
  button { font: inherit; font-size: 14px; padding: 7px 12px; border-radius: 8px; border: 1px solid #c9cdd4; background: #fff; cursor: pointer; }
  pre { margin: 8px 0 0; font-size: 12px; white-space: pre-wrap; color: #374151; }
  .warn { color: #9a3412; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="bar"><button id="bad">Compile a broken shader</button></div>
<pre id="log"></pre>

<script>
  const canvas = document.getElementById('c');
  const log = document.getElementById('log');

  // One WGSL module holds both shaders
  const shaderCode = `
struct VOut {
  @builtin(position) pos : vec4f,
  @location(0) color : vec3f,
}

@vertex
fn vs(@location(0) pos : vec2f, @location(1) color : vec3f) -> VOut {
  var out : VOut;
  out.pos = vec4f(pos, 0.0, 1.0);
  out.color = color;
  return out;
}

@fragment
fn fs(v : VOut) -> @location(0) vec4f {
  return vec4f(v.color, 1.0);
}`;

  // x, y, r, g, b for each corner. x and y run from -1 to 1.
  const corners = new Float32Array([
     0.0,  0.8,   1.0, 0.3, 0.3,
    -0.8, -0.7,   0.3, 1.0, 0.4,
     0.8, -0.7,   0.3, 0.5, 1.0,
  ]);

  let device = null;

  async function start() {
    // 1. Is WebGPU here at all?
    if (!navigator.gpu) return fallback('navigator.gpu is undefined');
    let adapter = null;
    try { adapter = await navigator.gpu.requestAdapter(); } catch (err) { return fallback(err.message); }
    if (!adapter) return fallback('requestAdapter() returned null');
    device = await adapter.requestDevice();

    // 2. Connect the canvas to the device
    canvas.width = Math.round(canvas.clientWidth * devicePixelRatio);
    canvas.height = Math.round(canvas.clientHeight * devicePixelRatio);
    const context = canvas.getContext('webgpu');
    const format = navigator.gpu.getPreferredCanvasFormat();
    context.configure({ device, format });

    // 3. Copy the corners into a GPU buffer
    const buffer = device.createBuffer({
      size: corners.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(buffer, 0, corners);

    // 4. The pipeline: shaders + how to read the buffer
    const module = device.createShaderModule({ code: shaderCode });
    const pipeline = device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module, entryPoint: 'vs',
        buffers: [{
          arrayStride: 5 * 4,  // 5 floats of 4 bytes per corner
          attributes: [
            { shaderLocation: 0, offset: 0, format: 'float32x2' },
            { shaderLocation: 1, offset: 2 * 4, format: 'float32x3' },
          ],
        }],
      },
      fragment: { module, entryPoint: 'fs', targets: [{ format }] },
      primitive: { topology: 'triangle-list' },
    });

    // 5. Record the commands, then submit them
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({
      colorAttachments: [{
        view: context.getCurrentTexture().createView(),
        clearValue: { r: 0.07, g: 0.09, b: 0.15, a: 1 },
        loadOp: 'clear',
        storeOp: 'store',
      }],
    });
    pass.setPipeline(pipeline);
    pass.setVertexBuffer(0, buffer);
    pass.draw(3);
    pass.end();
    device.queue.submit([encoder.finish()]);
    log.textContent = 'Drawn with WebGPU. Canvas format: ' + format;
  }

  // No WebGPU: draw the same triangle with canvas 2D and say why
  function fallback(reason) {
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
    const ctx = canvas.getContext('2d');
    const w = canvas.width, h = canvas.height;
    const px = (x, y) => [(x + 1) / 2 * w, (1 - y) / 2 * h];
    ctx.beginPath();
    ctx.moveTo(...px(0, 0.8)); ctx.lineTo(...px(-0.8, -0.7)); ctx.lineTo(...px(0.8, -0.7));
    ctx.fillStyle = '#6b7280';
    ctx.fill();
    log.className = 'warn';
    log.textContent = 'WebGPU is not available here (' + reason + ').\nFallback: the grey triangle is drawn with canvas 2D.';
  }

  start();

  // A missing ")" in the vertex shader. createShaderModule does not throw.
  document.getElementById('bad').addEventListener('click', async () => {
    if (!device) return;
    const broken = device.createShaderModule({ code: shaderCode.replace('color : vec3f) ->', 'color : vec3f ->') });
    const info = await broken.getCompilationInfo();
    log.textContent = info.messages.map(m => m.type + ' at line ' + m.lineNum + ':' + m.linePos + '\n' + m.message).join('\n');
  });
</script>
</body>
</html>
하나의 HTML 파일, 라이브러리 없음. 브라우저에 WebGPU가 없으면 캔버스 2D로 회색 폴백을 그리고 그 이유를 알려줍니다.

전체 레시피는 5단계로 구성됩니다.

  1. WebGPU를 확인하세요. navigator.gpu가 undefined인 경우 대체를 표시합니다.
  2. 어댑터와 장치를 가져옵니다. requestAdapter()를 기다렸다가 결과가 null가 아닌지 확인한 다음 requestDevice()를 기다립니다.
  3. 캔버스를 구성합니다. canvas.getContext('webgpu')를 호출하고 장치 및 getPreferredCanvasFormat()를 configure에 전달합니다.
  4. 파이프라인을 구축합니다. createShaderModule에 WGSL 코드를 넣은 다음 createRenderPipeline에 셰이더와 버퍼 레이아웃을 설명합니다.
  5. 기록 및 제출. getCurrentTexture()에서 렌더 패스를 시작하고, 파이프라인을 설정하고, 패스를 그리고 종료하고 제출합니다.

WebGPU를 실행하는 브라우저

WebGPU 사양과 해당 셰이딩 언어인 WGSL은 모두 W3C 후보 권장 사항 초안입니다. 지원은 여전히 ​​브라우저와 운영 체제에 따라 다르므로 가정하기보다는 코드에서 확인하세요.

MDN의 호환성 데이터 목록은 다음 브라우저에서 지원됩니다.

브라우저 MDN의 데이터가 말하는 것
크롬, 엣지, 오페라 지원됩니다. Linux의 Chrome은 특정 Intel GPU로 제한됩니다.
안드로이드용 크롬, 삼성 인터넷 지원됨
Mac, iPhone, iPad의 Safari 지원됨
파이어폭스 부분: Linux가 아닌 Windows 및 Apple-silicon Mac
안드로이드용 파이어폭스 지원되지 않음

이 페이지에 대해 테스트된 내용: Windows의 Chrome 및 Edge는 데스크톱 및 휴대폰 너비에서 세 가지 예를 모두 실행했습니다. GPU 액세스가 없는 Chromium 빌드는 requestAdapter()에서 null를 반환했으며 모든 예에서는 대체 메시지를 표시했습니다.

WebGPU에는 보안 컨텍스트도 필요합니다. https, localhost의 페이지 및 파일에서 열린 페이지가 적합합니다. 네트워크 주소의 일반 http를 통해 제공되는 동일한 파일에는 Chrome에 navigator.gpu가 전혀 없습니다.

첫 번째 추첨 전 4번의 콜

WebGL은 컨텍스트를 즉시 제공합니다. WebGPU를 사용하면 하드웨어를 먼저 요청하고 호출 중 두 개가 약속을 반환합니다.

1단계와 2단계에서는 WebGPU와 폴백 중 하나를 결정합니다. 4단계는 그릴 때만 필요합니다.
1단계와 2단계에서는 WebGPU와 폴백 중 하나를 결정합니다. 4단계는 그릴 때만 필요합니다.
if (!navigator.gpu) return fallback('no navigator.gpu');
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return fallback('no adapter');
const device = await adapter.requestDevice();

const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
  • 어댑터는 물리적 GPU와 해당 드라이버를 나타냅니다. 적합한 항목을 사용할 수 없는 경우 requestAdapter()는 null로 확인됩니다.
  • 장치는 해당 GPU에 대한 페이지의 자체 논리적 보기입니다. 모든 버퍼, 셰이더 및 파이프라인이 여기에서 생성됩니다.
  • 어댑터는 하나의 장치를 전달합니다. requestDevice()를 두 번째로 호출하면 OperationError로 거부됩니다.

getPreferredCanvasFormat()는 화면에 적합한 형식(bgra8unorm 또는 rgba8unorm)을 반환합니다. 여기에 사용된 Windows 시스템에서는 bgra8unorm였습니다.

캔버스는 CSS 크기에 관계없이 300 x 150픽셀에서 시작합니다. clientWidth 및 clientHeight와 devicePixelRatio를 곱하여 canvas.width 및 canvas.height를 설정하지 않으면 도면이 부드러워 보입니다. HTML 캔버스 흐릿함은 크기 불일치를 다루고 있습니다.

WGSL 모듈 1개, 셰이더 2개

WebGPU 셰이더는 WebGPU 셰이딩 언어인 WGSL로 작성되었습니다. 하나의 모듈은 @vertex 및 @fragment로 표시된 정점 셰이더와 조각 셰이더를 모두 보유할 수 있습니다.

struct VOut {
  @builtin(position) pos : vec4f,
  @location(0) color : vec3f,
}

@vertex
fn vs(@location(0) pos : vec2f, @location(1) color : vec3f) -> VOut {
  var out : VOut;
  out.pos = vec4f(pos, 0.0, 1.0);
  out.color = color;
  return out;
}

@fragment
fn fs(v : VOut) -> @location(0) vec4f {
  return vec4f(v.color, 1.0);
}

정점 셰이더는 모서리마다 한 번씩 실행되고 해당 위치를 반환합니다. 프래그먼트 셰이더는 적용되는 픽셀당 한 번씩 실행되고 해당 색상을 반환합니다. 입력의 @location(0)는 아래 버퍼 레이아웃의 shaderLocation: 0와 일치합니다.

컴파일 오류는 조용합니다. createShaderModule는 오타를 일으키지 않습니다. 모듈에 메시지를 요청하세요.

const info = await module.getCompilationInfo();
for (const m of info.messages) {
  console.log(m.type, m.lineNum, m.linePos, m.message);
}

첫 번째 예의 버튼은 ) 하나를 제거하고 Chrome이 반환하는 내용을 인쇄합니다. )가 예상되었다는 내용의 8행 오류입니다.

명령을 기록한 후 제출

코너 데이터는 GPU 버퍼로 들어가고 파이프라인은 이를 읽는 방법을 설명합니다. 여기의 각 모서리는 5개의 4바이트 부동 소수점이므로 보폭은 20바이트이고 색상은 8바이트에서 시작됩니다.

const buffer = device.createBuffer({
  size: corners.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(buffer, 0, corners);

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: { module, entryPoint: 'vs', buffers: [{
    arrayStride: 20,
    attributes: [
      { shaderLocation: 0, offset: 0, format: 'float32x2' },
      { shaderLocation: 1, offset: 8, format: 'float32x3' },
    ],
  }] },
  fragment: { module, entryPoint: 'fs', targets: [{ format }] },
});

그리기는 두 단계로 이루어집니다. 먼저 명령을 인코더에 기록합니다. 완성된 명령 버퍼를 device.queue에 제출할 때까지는 아무 것도 GPU에 도달하지 않습니다.

인코더가 녹음합니다. 대기열이 실행됩니다. 모든 프레임에는 새로운 인코더와 새로운 텍스처가 적용됩니다.
인코더가 녹음합니다. 대기열이 실행됩니다. 모든 프레임에는 새로운 인코더와 새로운 텍스처가 적용됩니다.
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: { r: 0.07, g: 0.09, b: 0.15, a: 1 },
    loadOp: 'clear',
    storeOp: 'store',
  }],
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, buffer);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);

마지막 줄을 생략하면 모든 호출이 계속 성공하지만 캔버스는 비어 있습니다. 테스트에서 빨간색으로 지워지고 제출되지 않은 패스는 캔버스를 완전히 투명하게 유지했습니다.

균일한 버퍼로 애니메이션화

유니폼은 그리기의 모든 모서리와 픽셀이 읽을 수 있는 값으로, JavaScript에서 설정됩니다. WebGPU에서는 버퍼에 위치하며 바인드 그룹은 사용할 버퍼를 파이프라인에 알려줍니다.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGPU uniform buffer</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  canvas { display: block; width: 100%; height: 250px; border-radius: 10px; background: #111827; }
  .bar { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-top: 10px; font-size: 14px; }
  label { display: flex; gap: 6px; align-items: center; }
  input[type=range] { width: 120px; }
  p { margin: 8px 0 0; font-size: 12px; color: #9a3412; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="bar">
  <label>Color <input type="color" id="color" value="#f59e0b"></label>
  <label>Speed <input type="range" id="speed" min="0" max="4" step="0.1" value="1"></label>
</div>
<p id="msg"></p>

<script>
  const canvas = document.getElementById('c');
  const colorInput = document.getElementById('color');
  const speedInput = document.getElementById('speed');

  const shaderCode = `
struct Params {
  angleAspect : vec4f,  // x = angle, y = width / height
  color : vec4f,
}
@group(0) @binding(0) var<uniform> u : Params;

@vertex
fn vs(@builtin(vertex_index) i : u32) -> @builtin(position) vec4f {
  // Three corners written in the shader, no vertex buffer needed
  var p = array<vec2f, 3>(vec2f(0.0, 0.7), vec2f(-0.6, -0.45), vec2f(0.6, -0.45));
  let a = u.angleAspect.x;
  let r = vec2f(p[i].x * cos(a) - p[i].y * sin(a), p[i].x * sin(a) + p[i].y * cos(a));
  return vec4f(r.x / u.angleAspect.y, r.y, 0.0, 1.0);
}

@fragment
fn fs() -> @location(0) vec4f {
  return u.color;
}`;

  async function start() {
    const adapter = navigator.gpu && await navigator.gpu.requestAdapter().catch(() => null);
    if (!adapter) {
      document.getElementById('msg').textContent = 'WebGPU is not available in this browser, so nothing is drawn. Try a browser listed in the article.';
      return;
    }
    const device = await adapter.requestDevice();
    const context = canvas.getContext('webgpu');
    const format = navigator.gpu.getPreferredCanvasFormat();
    context.configure({ device, format });

    // 8 floats = 32 bytes: two vec4f
    const params = new Float32Array(8);
    const uniformBuffer = device.createBuffer({
      size: params.byteLength,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    const module = device.createShaderModule({ code: shaderCode });
    const pipeline = device.createRenderPipeline({
      layout: 'auto',
      vertex: { module, entryPoint: 'vs' },
      fragment: { module, entryPoint: 'fs', targets: [{ format }] },
    });

    // Tells the pipeline which buffer is @group(0) @binding(0)
    const bindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
    });

    let angle = 0, last = performance.now();

    function frame(now) {
      // Keep the drawing buffer the same size as the element
      const w = Math.round(canvas.clientWidth * devicePixelRatio);
      const h = Math.round(canvas.clientHeight * devicePixelRatio);
      if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }

      angle += (now - last) / 1000 * Number(speedInput.value);
      last = now;
      const hex = colorInput.value;
      params.set([angle, w / h, 0, 0,
        parseInt(hex.slice(1, 3), 16) / 255, parseInt(hex.slice(3, 5), 16) / 255, parseInt(hex.slice(5, 7), 16) / 255, 1]);
      device.queue.writeBuffer(uniformBuffer, 0, params);  // new values every frame

      const encoder = device.createCommandEncoder();
      const pass = encoder.beginRenderPass({
        colorAttachments: [{
          view: context.getCurrentTexture().createView(),  // a fresh texture each frame
          clearValue: { r: 0.07, g: 0.09, b: 0.15, a: 1 },
          loadOp: 'clear', storeOp: 'store',
        }],
      });
      pass.setPipeline(pipeline);
      pass.setBindGroup(0, bindGroup);
      pass.draw(3);
      pass.end();
      device.queue.submit([encoder.finish()]);
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  }

  start();
</script>
</body>
</html>
각도, 종횡비 및 색상은 매 프레임마다 균일한 버퍼에 기록됩니다. 속도를 0으로 설정하면 정지합니다.
struct Params {
  angleAspect : vec4f,  // x = angle, y = width / height
  color : vec4f,
}
@group(0) @binding(0) var<uniform> u : Params;
const uniformBuffer = device.createBuffer({
  size: 32,  // two vec4f
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});

function frame(now) {
  device.queue.writeBuffer(uniformBuffer, 0, params);
  // ...begin a pass on a fresh getCurrentTexture()...
  pass.setBindGroup(0, bindGroup);
  requestAnimationFrame(frame);
}

layout: 'auto'를 사용하면 파이프라인은 셰이더의 @group 및 @binding 라인에서 바인드 그룹 레이아웃을 구축하고 getBindGroupLayout(0)는 이를 다시 전달합니다.

getCurrentTexture()는 캔버스에 표시될 다음 텍스처를 반환하므로 처음에 한 번 호출하는 것이 아니라 프레임 함수 내에서 호출합니다. 루프 자체는 일반적인 requestAnimationFrame입니다.

이 예제에는 정점 버퍼가 전혀 없습니다. 세 모서리가 셰이더에 기록되고 @builtin(vertex_index)는 어느 모서리가 처리되고 있는지 알려줍니다.

컴퓨팅: GPU를 계산기로 사용

WebGPU에는 아무것도 그리지 않는 두 번째 종류의 파이프라인이 있습니다. 컴퓨팅 셰이더는 버퍼를 읽고 쓰며, 많은 복사본이 병렬로 실행됩니다.

GPU는 스토리지 버퍼에 씁니다. JavaScript는 MAP_READ 버퍼만 읽을 수 있으므로 결과가 먼저 복사됩니다.
GPU는 스토리지 버퍼에 씁니다. JavaScript는 MAP_READ 버퍼만 읽을 수 있으므로 결과가 먼저 복사됩니다.
@group(0) @binding(0) var<storage, read_write> data : array<f32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3u) {
  if (id.x >= arrayLength(&data)) { return; }
  data[id.x] = data[id.x] * data[id.x];
}

각 작업 그룹은 64개 번호를 다루므로 페이지는 Math.ceil(n / 64) 작업 그룹을 발송합니다. 마지막 그룹은 배열의 끝을 지나 도달할 수 있으므로 셰이더는 인덱스를 먼저 확인합니다.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGPU compute shader</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; font-size: 14px; }
  select, button { font: inherit; font-size: 14px; padding: 7px 10px; border-radius: 8px; border: 1px solid #c9cdd4; background: #fff; }
  button { cursor: pointer; }
  .out { margin-top: 12px; padding: 12px; border-radius: 10px; background: #fff; border: 1px solid #e1e4ea; min-height: 150px; }
  .out b { display: block; font-size: 13px; margin-bottom: 6px; }
  .nums { font: 13px/1.6 ui-monospace, Consolas, monospace; word-break: break-all; color: #374151; }
  .ok { color: #0f5132; font-size: 13px; margin-top: 8px; }
  .warn { color: #9a3412; font-size: 13px; }
</style>
</head>
<body>
<div class="bar">
  <label>Numbers
    <select id="count">
      <option value="1000">1,000</option>
      <option value="100000" selected>100,000</option>
      <option value="1000000">1,000,000</option>
    </select>
  </label>
  <button id="run">Square them on the GPU</button>
</div>
<div class="out" id="out"><span class="nums">Press the button.</span></div>

<script>
  const out = document.getElementById('out');

  // One compute shader: each invocation squares one number
  const shaderCode = `
@group(0) @binding(0) var<storage, read_write> data : array<f32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3u) {
  if (id.x >= arrayLength(&data)) { return; }
  data[id.x] = data[id.x] * data[id.x];
}`;

  let device = null, pipeline = null;

  async function setup() {
    const adapter = navigator.gpu && await navigator.gpu.requestAdapter().catch(() => null);
    if (!adapter) return false;
    device = await adapter.requestDevice();
    pipeline = device.createComputePipeline({
      layout: 'auto',
      compute: { module: device.createShaderModule({ code: shaderCode }), entryPoint: 'main' },
    });
    return true;
  }

  async function run() {
    if (!device && !(await setup())) {
      out.innerHTML = '<span class="warn">WebGPU is not available in this browser, so the shader cannot run here.</span>';
      return;
    }
    const n = Number(document.getElementById('count').value);
    const input = new Float32Array(n).map((_, i) => i);

    // The GPU works on a storage buffer...
    const work = device.createBuffer({
      size: input.byteLength,
      usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(work, 0, input);
    // ...and JavaScript can only read a MAP_READ buffer
    const readback = device.createBuffer({
      size: input.byteLength,
      usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
    });

    const bindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [{ binding: 0, resource: { buffer: work } }],
    });

    const encoder = device.createCommandEncoder();
    const pass = encoder.beginComputePass();
    pass.setPipeline(pipeline);
    pass.setBindGroup(0, bindGroup);
    pass.dispatchWorkgroups(Math.ceil(n / 64));  // 64 numbers per workgroup
    pass.end();
    encoder.copyBufferToBuffer(work, 0, readback, 0, input.byteLength);
    device.queue.submit([encoder.finish()]);

    // Wait until the GPU is done, then copy the result out
    await readback.mapAsync(GPUMapMode.READ);
    const result = new Float32Array(readback.getMappedRange().slice(0));
    readback.unmap();
    work.destroy(); readback.destroy();

    // Check every value against plain JavaScript
    let wrong = 0;
    for (let i = 0; i < n; i++) if (result[i] !== Math.fround(i * i)) wrong++;
    out.innerHTML = '<b>First 10 results</b><div class="nums">' + Array.from(result.slice(0, 10)).join(', ') +
      '</div><b style="margin-top:8px">Last result</b><div class="nums">' + (n - 1) + '² = ' + result[n - 1] +
      '</div><div class="ok">' + (wrong ? wrong + ' values differ from JavaScript' : 'All ' + n.toLocaleString('en') + ' values match JavaScript.') + '</div>';
  }

  document.getElementById('run').addEventListener('click', run);
</script>
</body>
</html>
GPU에서 최대 1,000,000개의 숫자를 제곱하고 다시 읽어서 JavaScript와 비교하여 모든 숫자를 확인합니다.

결과를 다시 읽으려면 다음 세 단계를 거쳐야 합니다.

  1. MAP_READ | COPY_DST로 만든 버퍼에 스토리지 버퍼를 복사합니다. MAP_READ가 있는 버퍼는 COPY_DST 외에는 다른 용도로 사용할 수 없으므로 스토리지 버퍼 자체가 될 수 없습니다.
  2. mapAsync(GPUMapMode.READ)를 기다리세요.
  3. unmap()를 호출하기 전에 getMappedRange()에서 데이터를 복사하세요. unmap() 이후에는 해당 ArrayBuffer가 분리되고 길이는 0입니다.

긴 컴퓨팅 작업은 navigator.gpu도 사용할 수 있는 웹 작업자에서 실행할 수도 있습니다.

WebGPU 또는 WebGL

둘 다 GPU를 사용하여 <canvas>를 사용하며 라이브러리가 필요하지 않습니다. HTML의 WebGL은 WebGL의 동일한 삼각형을 통과합니다.

WebGL 웹GPU
시작하기 getContext('webgl2')는 컨텍스트를 한 번에 반환합니다. 어댑터와 장치를 기다린 후 캔버스를 구성하세요.
셰이더 언어 GLSL WGSL
셰이더 프로그램에 연결된 별도의 정점 및 조각 소스 하나의 모듈이 두 진입점을 모두 보유할 수 있습니다.
일반 계산 그것을 위해 설계되지 않았습니다 컴퓨팅 파이프라인
오류 GPU를 기다리는 getError() 약속을 해결하는 오류 범위

특정 호출에서 유효성 검사 오류를 포착하려면 이를 device.pushErrorScope('validation')로 래핑하고 device.popErrorScope()를 기다립니다. Promise는 범위의 첫 번째 오류 또는 null를 해결합니다.

작동하지 않을 때

당신이 보는 것 원인 수정
navigator.gpu는 undefined입니다. 이 브라우저에 WebGPU가 없거나 페이지가 보안 컨텍스트가 아닙니다. https 또는 localhost를 통해 서비스하고 폴백을 표시합니다.
requestAdapter()는 null를 제공합니다. 페이지에 사용 가능한 GPU 어댑터가 없습니다. 대체 표시
캔버스가 비어 있고 오류가 없습니다. 명령 버퍼가 제출되지 않았습니다. encoder.finish()를 queue.submit()에 전달하세요.
getCurrentTexture()의 InvalidStateError 컨텍스트가 아직 구성되지 않았습니다. 먼저 context.configure에 전화하세요
셰이더 편집 후 아무 것도 그려지지 않음 WGSL 컴파일 오류 및 createShaderModule가 발생하지 않습니다. getCompilationInfo()를 기다리고 메시지를 인쇄하세요.
requestDevice()의 OperationError 동일한 어댑터의 두 번째 장치 먼저 새 어댑터를 요청하세요.
getContext('2d')는 null를 반환합니다. 캔버스에는 이미 webgpu 컨텍스트가 있습니다. 캔버스당 하나의 컨텍스트 유형 사용
createBuffer의 유효성 검사 오류 MAP_READ와 STORAGE 결합 별도의 다시 읽기 버퍼 사용
결과 배열의 길이는 0입니다. unmap() 이후에 읽혔습니다. 매핑을 해제하기 전에 slice()로 복사하세요.
부드럽거나 늘어난 드로잉 캔버스는 여전히 300 x 150입니다. 요소 크기에서 너비와 높이 설정

링크로 공유하세요

WebGPU 페이지는 스크린샷에 표시하기 어렵습니다. 삼각형이 회전을 멈추고 뷰어는 속도 슬라이더를 끌거나 자체 GPU에서 컴퓨팅 셰이더를 실행할 수 없습니다.

.html 첨부 파일은 전화기에서 일반 코드로 열릴 수 있습니다. 휴대전화에서 HTML 파일 열기가 그 이유를 설명합니다.

작업 버전을 보내려면 페이지를 NOS 문서에 붙여넣고 공유 링크 만들기를 선택하세요. 링크할 HTML이 이를 안내합니다.

페이지는 작성된 대로 렌더링되고 해당 스크립트가 실행되므로 페이지를 보내는 사람들은 모양이 움직이는 것을 보고 버튼을 직접 누를 수 있습니다.

WebGPU가 없는 브라우저를 사용하는 사람은 대신 대체 메시지를 보게 됩니다. 나중에 코드를 변경하면 동일한 링크에 새 버전이 표시됩니다.

NOS 내부에서는 페이지가 다른 사이트의 이미지, 프레임 또는 데이터를 로드할 수 없으므로 모든 것을 하나의 파일에 보관합니다. 여기의 셰이더는 스크립트의 일반 문자열이며 모든 숫자는 페이지에 생성됩니다.

자주 묻는 질문

WebGPU를 사용하려면 라이브러리가 필요합니까?

아니요. 이를 지원하는 브라우저에는 WebGPU가 내장되어 있으며 navigator.gpu에서 시작됩니다. 이 페이지의 첫 번째 예에서는 라이브러리와 빌드 단계가 없는 삼각형을 그립니다.

navigator.gpu가 정의되지 않은 이유는 무엇입니까?

브라우저에 WebGPU가 없거나 페이지가 보안 컨텍스트가 아닙니다. WebGPU는 보안 페이지(https, localhost 및 파일 URL 수)에만 노출됩니다. 네트워크 주소에서 일반 http를 통해 열린 페이지는 navigator.gpu를 전혀 얻지 못합니다.

requestAdapter()가 null을 반환하는 이유는 무엇입니까?

브라우저에서 나눠줄 GPU 어댑터를 찾지 못했습니다. GPU 액세스 없이 실행되는 Chromium 빌드에서 이 문제가 발생했지만 동일한 시스템의 Chrome과 Edge는 어댑터를 반환했습니다. null을 확인하고 대체를 표시합니다.

WebGPU가 WebGL을 대체합니까?

MDN은 WebGPU를 WebGL의 후속 제품으로 설명하며 범용 GPU 계산을 지원합니다. WebGL은 MDN의 호환성 데이터에서 더 이상 사용되지 않는 것으로 표시되지 않았으며 캔버스에서 GPU에 그림을 그리는 방법으로 남아 있습니다.

웹 워커에서 WebGPU를 사용할 수 있나요?

그렇습니다. 워커 내에서 WorkerNavigator.gpu는 navigator.gpu가 페이지에 제공하는 것과 동일한 GPU 개체를 제공합니다. 그러면 페이지의 기본 스레드를 건드리지 않고도 컴퓨팅 작업을 실행할 수 있습니다.