사전 WebGPU
표준

WebGPU

gabury1고친 사람 github-actions[bot]

WebGPU 는 웹페이지가 그래픽 카드에게 그림 그리기와 계산을 시킬 때 쓰는 약속입니다. 페이지가 보낸 명령을 브라우저가 한 번 검사한 뒤 그래픽 카드로 넘깁니다. 그리기 설정은 미리 한 덩이로 묶어 두었다가 그릴 때 그 덩이를 지정합니다. 그림과 상관없는 계산만 그래픽 카드에서 돌리는 길도 따로 열어 둡니다.

쉽고 빠른 이해

WebGPU 는 웹페이지의 자바스크립트가 그래픽 카드에게 일을 시키는 함수 목록입니다. 브라우저에서 도는 3D 지도나, 페이지 안에서 돌리는 기계 학습 계산이 이 함수를 씁니다.

이게 없으면 웹페이지가 쓸 수 있는 그래픽 카드 규격이 예전 설계 하나뿐입니다. 켜 둔 설정이 전역에 남아 한 곳에서 바꾼 것이 다른 그리기에 번지고, 그림이 아닌 계산을 시킬 길도 마땅치 않습니다.

어떻게 도나:

  1. 브라우저에서 그래픽 카드를 대표하는 객체를 하나 받습니다
  2. 그리기 설정과 셰이더를 미리 묶어 파이프라인 객체로 만들어 둡니다
  3. 그릴 명령을 인코더에 쌓아 두었다가 큐에 한 번에 밀어 넣습니다

대가도 있습니다. 삼각형 하나를 띄우는 데도 미리 만들어 둘 객체가 여럿이라 첫 코드가 깁니다. 브라우저가 명령을 검사하고 넘기므로 같은 일을 네이티브 앱에서 할 때보다 부담이 붙습니다.

상세

WebGPU 는 브라우저의 자바스크립트에서 그래픽 카드를 쓰는 API 규격입니다. API(Application Programming Interface, 응용 프로그램 인터페이스)는 프로그램이 다른 소프트웨어의 기능을 부를 때 쓰는 함수 목록입니다.

그래픽 카드 안에서 계산을 맡는 칩을 GPU(Graphics Processing Unit, 그래픽 처리 장치)라고 부릅니다. GPU 는 같은 계산을 아주 많은 데이터에 한꺼번에 겁니다. 점 수십만 개의 위치나 화면 픽셀의 색처럼 서로 독립인 계산이 여기에 맞습니다.

WebGPU 는 설치하는 프로그램이 아니라 문서입니다. 자바스크립트에서 부를 함수와 객체의 이름, 넘길 값, 부른 뒤에 일어날 일을 정해 둡니다. 이 문서는 W3C(World Wide Web Consortium, 월드 와이드 웹 컨소시엄)라는 웹 표준 단체가 관리하고, 그 함수를 실제로 돌리는 코드는 각 브라우저가 만들어 넣습니다.

아래에서는 앞선 규격의 한계, 객체를 얻는 순서, 셰이더 언어, 파이프라인, 자원 묶음, 명령을 보내는 방법, 계산 전용 경로, 실패하는 경우를 차례로 봅니다.

앞선 웹 그래픽스 규격이 남긴 숙제

브라우저에서 그래픽 카드를 쓰는 규격은 WebGL 이 먼저였습니다. WebGL 은 휴대폰까지 널리 퍼져 있던 옛 그래픽스 규격의 함수 목록을 거의 그대로 가져왔습니다. 덕분에 빨리 자리를 잡았지만 그 설계까지 같이 따라왔습니다.

따라온 설계 중 하나가 상태 기계 방식입니다. 상태 기계 방식에서는 켜 둔 설정이 전역으로 남아 있다가 다음 그리기에 그대로 적용됩니다. 한 곳에서 바꾼 설정이 엉뚱한 그림에 번지고, 그릴 때마다 지금 무엇이 켜져 있는지를 쫓아야 합니다.

다른 하나는 그림이 아닌 계산을 시킬 길이 사실상 없다는 점입니다. 숫자 배열을 GPU 로 계산하려면 그 값을 이미지인 척 꾸며 넣고 결과도 이미지에서 읽어 내야 했습니다.

WebGPU 는 옛 규격을 물려받지 않고 Vulkan · Direct3D · Metal 처럼 요즘 만들어진 그래픽스 규격의 설계를 따랐습니다. 설정을 전역에 켜 두는 대신 미리 묶어 두고, 계산만 시키는 경로를 따로 두었습니다.

어댑터를 거쳐 디바이스를 받는다

WebGPU 는 함수를 바로 부르는 대신 객체를 두 번 받아서 시작합니다. 먼저 어댑터를 받습니다. 어댑터는 이 기계에 붙은 그래픽 카드 하나를 대표하는 객체입니다.

어댑터에서 다시 디바이스를 받습니다. 디바이스는 그 카드에 명령을 보내는 통로이자, 앞으로 쓸 객체를 전부 만들어 주는 공장입니다. 버퍼도 셰이더도 파이프라인도 디바이스가 만듭니다.

JavaScript
const adapter =        // 없으면 null
  await navigator.gpu.requestAdapter();
const device =         // 명령을 보내는 통로
  await adapter.requestDevice();

requestAdapter 는 쓸 수 있는 그래픽 카드를 못 찾으면 오류를 던지지 않고 null 을 돌려줍니다. 실제 코드는 다음 줄로 넘어가기 전에 한 번 확인합니다.

둘로 나눈 데는 까닭이 있습니다. 한 기계에 그래픽 카드가 둘 이상 붙어 있을 수 있습니다. 어댑터를 먼저 받으면 페이지가 어느 카드를 쓸지, 그 카드가 무엇을 할 수 있는지 보고 나서 디바이스를 요청할 수 있습니다.

flowchart TD
    A["페이지의 자바스크립트"] --> B["어댑터 · 그래픽 카드 하나를 대표한다"]
    B --> C["디바이스 · 그 카드에 명령을 보내는 통로"]
    subgraph D["디바이스가 만들어 주는 객체"]
        E["버퍼 · 텍스처 · 샘플러"]
        F["셰이더 모듈"]
        G["파이프라인 · 바인드 그룹"]
        H["커맨드 인코더"]
    end
    C --> D

그림에서 페이지가 만드는 객체는 전부 디바이스 아래에 달립니다. 그래서 디바이스를 잃으면 그 아래 객체도 같이 못 쓰게 됩니다. 뒤의 「디바이스를 잃는 경우」가 이 이야기입니다.

셰이더는 WGSL 로 쓴다

셰이더는 GPU 에서 도는 작은 프로그램입니다. 점 하나, 화면 조각 하나마다 같은 프로그램이 따로 한 번씩 돕니다. 무엇을 어떻게 계산할지는 그림을 그리는 쪽이 직접 적어 넣습니다.

WebGPU 의 셰이더는 WGSL(WebGPU Shading Language, WebGPU 셰이딩 언어)로 씁니다. 자바스크립트는 이 소스를 문자열로 넘기고, 브라우저가 그것을 컴파일해 셰이더 모듈 객체로 만듭니다.

WGSL 에서 GPU 가 실제로 부를 함수 앞에는 어느 단계의 셰이더인지를 적습니다. 그리기에 쓰는 단계는 둘입니다.

wgsl
@vertex
fn vs(@location(0) pos: vec2f)
    -> @builtin(position) vec4f {
  return vec4f(pos, 0, 1); // 화면 위 좌표
}

@fragment
fn fs() -> @location(0) vec4f {
  return vec4f(1, 0, 0, 1); // 빨강 한 색
}

위의 @vertex 가 붙은 함수가 정점 셰이더입니다. 정점은 좌표와 색 같은 값을 가진 점 하나이고, 삼각형 하나는 정점 셋으로 그립니다. 정점 셰이더는 정점마다 한 번 돌아서 그 점을 화면 어디에 둘지 정합니다.

@fragment 가 붙은 함수는 프래그먼트 셰이더입니다. 프래그먼트는 삼각형이 덮은 화면 조각마다 생기는 픽셀 후보입니다. 프래그먼트 셰이더는 후보마다 한 번 돌아서 무슨 색으로 칠할지 정합니다. 위 예시는 조건 없이 빨강을 돌려주므로 삼각형 전체가 빨갛게 칠해집니다.

설정을 미리 묶어 파이프라인으로 만든다

렌더 파이프라인은 한 번 그릴 때 필요한 설정을 통째로 담은 객체입니다. 어느 셰이더를 쓸지, 정점 값이 메모리에 어떤 차례로 놓였는지, 깊이 테스트와 블렌딩을 어떻게 할지가 한 덩이로 들어갑니다.

이 객체는 그리기 전에 미리 만듭니다. 만드는 시점에 브라우저가 설정이 서로 맞는지 검사하고, 그래픽 드라이버가 셰이더를 그 카드용 코드로 번역해 둡니다. 그리는 동안 할 일을 앞으로 당겨 놓는 것입니다.

그릴 때는 만들어 둔 파이프라인을 지정하는 한 줄이면 됩니다. 앞에서 본 상태 기계 방식과 갈리는 대목이 여기입니다. 지금 무엇이 켜져 있는지 쫓을 필요가 없고, 다른 그리기가 켜 둔 설정이 이쪽으로 번지지도 않습니다.

자원은 바인드 그룹으로 묶어 건넨다

셰이더가 읽을 값은 대개 GPU 메모리에 따로 올려 둡니다. 숫자 배열을 담는 그릇이 GPU 버퍼이고, 표면에 입힐 이미지를 담는 그릇이 텍스처입니다. 텍스처에서 색을 어떻게 읽을지 정하는 작은 설정 객체는 샘플러입니다.

이 그릇들을 셰이더에 하나씩 꽂지 않고 몇 개씩 묶어서 건넵니다. 그 묶음이 바인드 그룹입니다. 바인드 그룹은 미리 만들어 두었다가 그릴 때 번호를 붙여 지정합니다.

묶어 두면 바꿀 때가 편합니다. 물체 열 개를 서로 다른 이미지로 그린다면 파이프라인은 그대로 두고 바인드 그룹만 바꿔 가며 열 번 그리면 됩니다.

명령을 모았다가 큐에 한 번에 보낸다

자바스크립트에서 부른 그리기 명령은 그 자리에서 GPU 로 가지 않습니다. 먼저 커맨드 인코더에 쌓입니다. 인코더는 보낼 명령을 적어 두는 공책이라고 보면 됩니다.

쌓기를 마치면 인코더에서 커맨드 버퍼를 하나 뽑아 커맨드 큐에 제출합니다. 제출된 덩이가 GPU 로 건너가 순서대로 처리됩니다.

JavaScript
const enc = device.createCommandEncoder();
const pass = enc.beginRenderPass(desc);
pass.setPipeline(pipeline);
pass.setBindGroup(0, group);
pass.draw(3);          // 삼각형 하나
pass.end();
const cb = enc.finish();   // 커맨드 버퍼
device.queue.submit([cb]); // 이제 보낸다

가운데의 beginRenderPass 부터 end 까지가 렌더 패스입니다. 렌더 패스는 어디에 그릴지와 시작할 때 무슨 색으로 지울지를 정해 두고, 그 안의 그리기 명령을 한 묶음으로 받습니다.

그린 결과가 놓이는 곳은 대개 페이지 안의 canvas 요소입니다. canvas 요소는 픽셀로 된 빈 그림판을 페이지에 하나 만드는 태그입니다. 화면에 안 내보내고 계산에만 쓸 이미지라면 텍스처에 그려 두었다가 나중에 씁니다.

이렇게 모아 보내는 데는 까닭이 있습니다. 자바스크립트에서 GPU 쪽으로 한 번 건너가는 일은 비쌉니다. 명령마다 건너가면 그 비용이 명령 수만큼 붙습니다. 한 덩이로 모아 한 번만 건너가면 그 비용을 한 번만 냅니다.

sequenceDiagram
    participant 페이지
    participant 인코더
    participant 큐
    participant GPU
    페이지->>인코더: 파이프라인과 바인드 그룹 지정
    페이지->>인코더: 그리라는 명령을 쌓는다
    Note over 페이지,인코더: 여기까지는 GPU 로 안 건너간다
    페이지->>큐: 커맨드 버퍼 제출
    큐->>GPU: 한 덩이로 전달
    GPU-->>페이지: 그린 결과가 그림판에 남는다

그림이 아닌 계산만 시키는 길

WebGPU 에는 그리기와 나란히 계산 전용 경로가 있습니다. 여기서 도는 셰이더가 컴퓨트 셰이더이고, 앞의 두 단계와 달리 화면에 아무것도 그리지 않습니다. 버퍼에서 값을 읽어 계산하고 버퍼에 값을 씁니다.

wgsl
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id)
        gid: vec3u) {
  data[gid.x] = data[gid.x] * 2; // 두 배로
}

@workgroup_size 는 이 셰이더를 몇 개씩 한 조로 묶어 돌릴지를 적은 값입니다. global_invocation_id 는 지금 도는 것이 전체에서 몇 번째인지를 알려 주는 번호입니다. 셰이더는 그 번호로 자기가 맡을 데이터를 고릅니다. 위 코드는 배열의 제 번호 칸 하나만 두 배로 만들고 끝납니다.

계산 결과를 자바스크립트에서 읽으려면 한 단계가 더 붙습니다. 셰이더가 쓴 버퍼는 GPU 쪽에 있어서 페이지가 바로 못 읽습니다. 읽기용으로 만든 버퍼에 그 내용을 복사하는 명령을 큐에 같이 넣고, 처리가 끝난 뒤에 그 버퍼를 페이지 쪽 메모리에 연결해 읽습니다.

이 경로 덕분에 그림과 무관한 일도 GPU 에 맡길 수 있습니다. 브라우저 안에서 도는 기계 학습 계산이나 물리 계산이 이 길을 씁니다.

검사를 거치고, 실패는 따로 보고된다

웹페이지 코드는 누가 썼는지 믿을 수 없습니다. 그래서 브라우저는 페이지가 보낸 값과 셰이더를 검사한 뒤에야 그래픽 드라이버로 넘깁니다. 드라이버의 허점을 페이지가 건드려 기계를 멈추거나 남의 메모리를 읽는 일을 막으려는 것입니다.

검사에 걸린 호출은 대개 예외를 던지지 않습니다. 만들려던 객체를 쓸 수 없는 상태로 돌려주고 오류를 따로 보고합니다. 명령이 모여서 나중에 처리되는 구조라 부르는 시점에는 결과를 알 수 없기 때문입니다.

그래서 오류를 보려면 페이지가 받아 갈 준비를 해 두어야 합니다. 의심스러운 구간의 앞뒤를 오류 범위로 감싸면 그 안에서 난 검증 오류를 하나 돌려받습니다. 아무 데도 안 걸린 오류는 개발자 도구 콘솔에 남습니다. 화면이 비어 있는데 코드는 끝까지 돌았다면 대개 이 보고를 안 받아 본 경우입니다.

디바이스를 잃는 경우

GPU 는 여러 탭과 프로그램이 나눠 쓰는 자원이라, 브라우저나 운영체제가 필요할 때 거둬 갑니다. 그래픽 드라이버가 다시 시작되거나 GPU 메모리가 모자랄 때 일어납니다. 이것이 디바이스 손실입니다.

디바이스를 잃으면 그 아래에 달려 있던 버퍼와 텍스처와 파이프라인이 전부 못 쓰게 됩니다. 페이지는 어댑터부터 다시 요청해서 객체를 처음부터 새로 만들어야 합니다. 이 처리를 안 해 두면 화면이 한 번 멈춘 뒤 다시 살아나지 않습니다.

디바이스에는 잃었을 때 그 사실을 알려 주는 통지가 하나 딸려 있습니다. 페이지는 그 통지에 다시 만드는 절차를 걸어 둡니다.

대가

WebGPU 의 함수는 낮은 수준입니다. 삼각형 하나를 띄우려 해도 어댑터와 디바이스를 받고, 셰이더를 컴파일하고, 파이프라인과 바인드 그룹을 만들고, 인코더에 명령을 쌓아 제출하는 일을 전부 손으로 써야 합니다.

그래서 실무에서는 이 과정을 감싼 3D 라이브러리를 얹어 쓰는 경우가 많습니다. 그리기 단계를 하나하나 직접 다뤄야 할 때만 WebGPU 함수를 바로 부릅니다. 차트나 단순한 2D 그림이라면 여기까지 갈 일이 없습니다.

검사도 대가입니다. 브라우저가 값과 셰이더를 검사하고 넘기므로, 같은 그림을 같은 카드에서 그려도 네이티브 앱보다 한 겹이 더 붙습니다. 이 겹은 웹페이지를 믿을 수 없어서 두는 것이라 없앨 수 있는 성질이 아닙니다.

관련 항목

WebGPU 가 속하는 상위 분류

그래픽스 · 렌더링 · 그래픽스 API · 웹 API · API

WebGPU 와 같은 역할을 두고 겨루는 규격

WebGL · WebGL 2 · OpenGL · OpenGL ES · Vulkan · Direct3D · Metal

WebGPU 를 정하고 구현하는 주체

W3C · 브라우저 · 그래픽 드라이버 · Dawn · wgpu

WebGPU 코드를 쓰는 셰이더 언어

WGSL · GLSL · HLSL · SPIR-V

WebGPU 가 브라우저 쪽에서 붙는 장치

자바스크립트 · canvas 요소 · OffscreenCanvas · 웹 워커 · DOM

WebGPU 가 만들어 다루는 GPU 쪽 객체

어댑터 · 디바이스 · GPU 버퍼 · 텍스처 · 샘플러 · 바인드 그룹 · 셰이더 모듈 · 렌더 파이프라인 · 컴퓨트 파이프라인

WebGPU 로 그리기가 거치는 처리 단계

그래픽스 파이프라인 · 정점 셰이더 · 래스터화 · 프래그먼트 셰이더 · 깊이 테스트 · 블렌딩 · 프레임버퍼

WebGPU 가 명령을 모아 보내는 장치

커맨드 인코더 · 커맨드 버퍼 · 커맨드 큐 · 렌더 패스 · 컴퓨트 패스

WebGPU 가 실패하는 경우와 막는 장치

디바이스 손실 · 검증 오류 · CORS · 동일 출처 정책 · 상태 기계

WebGPU 를 그림 밖에서 쓰는 분야

GPGPU · 컴퓨트 셰이더 · 기계 학습 · WebNN · 병렬 계산

WebGPU 위에 얹어 쓰는 라이브러리

3D 라이브러리 · Three.js · Babylon.js · glTF

다른 이름: Web GPU · WebGPU API