웹 개발자에게 WebGPU는 GPU에 통합되고 빠른 액세스를 제공하는 웹 그래픽 API입니다. WebGPU는 최신 하드웨어 기능을 노출하고 Direct3D 12, Metal, Vulkan과 마찬가지로 GPU에서 렌더링 및 계산 작업을 허용합니다.
사실이지만 불완전한 이야기입니다. WebGPU는 Apple, Google, Intel, Mozilla, Microsoft와 같은 주요 기업을 비롯한 여러 업체의 공동작업의 결과입니다. 그중 일부는 WebGPU가 JavaScript API 이상이 될 수 있으며 웹 이외의 생태계 전반의 개발자를 위한 크로스 플랫폼 그래픽 API가 될 수 있다는 점을 깨달았습니다.
기본 사용 사례를 충족하기 위해 Chrome 113에서 JavaScript API가 도입되었습니다. 하지만 그와 함께 또 다른 중요한 프로젝트인 webgpu.h C API가 개발되었습니다. 이 C 헤더 파일에는 WebGPU에서 사용할 수 있는 모든 프로시저 및 데이터 구조가 나열되어 있습니다. 이는 플랫폼에 관계없는 하드웨어 추상화 계층으로서, 여러 플랫폼에서 일관된 인터페이스를 제공하여 플랫폼별 애플리케이션을 빌드할 수 있도록 지원합니다.
이 문서에서는 웹과 특정 플랫폼에서 모두 실행되는 WebGPU를 사용하여 작은 C++ 앱을 작성하는 방법을 알아봅니다. 스포일러 경고: 코드베이스를 최소한으로 조정하면 브라우저 창과 데스크톱 창에 동일한 빨간색 삼각형이 표시됩니다.

기본 원리
완성된 애플리케이션을 보려면 WebGPU 크로스 플랫폼 앱 저장소를 확인하세요.
이 앱은 WebGPU를 사용하여 단일 코드베이스에서 데스크톱 및 웹 앱을 빌드하는 방법을 보여주는 최소한의 C++ 예시입니다. 내부적으로 WebGPU의 webgpu.h를 webgpu_cpp.h라는 C++ 래퍼를 통해 플랫폼에 관계없는 하드웨어 추상화 레이어로 사용합니다.
웹에서는 앱이 JavaScript API 위에 webgpu.h를 구현하는 바인딩이 있는 Emscripten을 기준으로 빌드됩니다. macOS 또는 Windows와 같은 특정 플랫폼에서는 이 프로젝트를 Chromium의 크로스 플랫폼 WebGPU 구현인 Dawn을 기준으로 빌드할 수 있습니다. webgpu.h의 Rust 구현인 wgpu-native도 있지만 이 문서에서는 사용되지 않습니다.
시작하기
시작하려면 크로스 플랫폼 빌드를 표준 방식으로 처리할 수 있는 C++ 컴파일러와 CMake가 필요합니다. 전용 폴더 내에 main.cpp
소스 파일과 CMakeLists.txt
빌드 파일을 만듭니다.
지금은 main.cpp
파일에 빈 main()
함수가 포함되어야 합니다.
int main() {}
CMakeLists.txt
파일에는 프로젝트에 관한 기본 정보가 포함됩니다. 마지막 줄은 실행 파일 이름이 'app'이고 소스 코드가 main.cpp
임을 지정합니다.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app) # Create project "app"
set(CMAKE_CXX_STANDARD 20) # Enable C++20 standard
add_executable(app "main.cpp")
cmake -B build
를 실행하여 'build/' 하위 폴더에 빌드 파일을 만들고 cmake --build build
를 실행하여 실제로 앱을 빌드하고 실행 파일을 생성합니다.
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
앱이 실행되지만 화면에 항목을 그리는 방법이 필요하므로 아직 출력이 없습니다.
Dawn 가져오기
삼각형을 그리려면 Chromium의 크로스 플랫폼 WebGPU 구현인 Dawn을 활용할 수 있습니다. 여기에는 화면에 그리기 위한 GLFW C++ 라이브러리가 포함됩니다. Dawn을 다운로드하는 한 가지 방법은 저장소에 git 하위 모듈로 추가하는 것입니다. 다음 명령어는 'dawn/' 하위 폴더에서 가져옵니다.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
그런 다음 다음과 같이 CMakeLists.txt
파일에 추가합니다.
- CMake
DAWN_FETCH_DEPENDENCIES
옵션은 모든 Dawn 종속 항목을 가져옵니다. dawn/
하위 폴더가 타겟에 포함됩니다.- 나중에
main.cpp
파일에서 사용할 수 있도록 앱은dawn::webgpu_dawn
,glfw
,webgpu_glfw
타겟에 종속됩니다.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
창 열기
이제 Dawn을 사용할 수 있으므로 GLFW를 사용하여 화면에 항목을 그립니다. 편의를 위해 webgpu_glfw
에 포함된 이 라이브러리를 사용하면 창 관리에 관한 플랫폼에 종속되지 않는 코드를 작성할 수 있습니다.
해상도가 512x512인 'WebGPU 창'이라는 창을 열려면 아래와 같이 main.cpp
파일을 업데이트합니다. 여기서 glfwWindowHint()
는 특정 그래픽 API 초기화를 요청하지 않기 위해 사용됩니다.
#include <GLFW/glfw3.h>
const uint32_t kWidth = 512;
const uint32_t kHeight = 512;
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// TODO: Render a triangle using WebGPU.
}
}
int main() {
Start();
}
앱을 다시 빌드하고 이전과 같이 실행하면 빈 창이 표시됩니다. 진전을 보이고 있습니다.

GPU 기기 가져오기
JavaScript에서 navigator.gpu
는 GPU에 액세스하기 위한 진입점입니다. C++에서는 동일한 목적으로 사용되는 wgpu::Instance
변수를 수동으로 만들어야 합니다. 편의를 위해 main.cpp
파일 상단에 instance
를 선언하고 main()
내에서 wgpu::CreateInstance()
를 호출합니다.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
GPU에 대한 액세스는 JavaScript API의 형식 때문에 비동기식입니다. C++에서 wgpu::Adapter
및 wgpu::Device
가 있는 콜백 함수를 각각 반환하는 GetAdapter()
및 GetDevice()
라는 두 개의 도우미 함수를 만듭니다.
#include <iostream>
…
void GetAdapter(void (*callback)(wgpu::Adapter)) {
instance.RequestAdapter(
nullptr,
[](WGPURequestAdapterStatus status, WGPUAdapter cAdapter,
const char* message, void* userdata) {
if (status != WGPURequestAdapterStatus_Success) {
exit(0);
}
wgpu::Adapter adapter = wgpu::Adapter::Acquire(cAdapter);
reinterpret_cast<void (*)(wgpu::Adapter)>(userdata)(adapter);
}, reinterpret_cast<void*>(callback));
}
void GetDevice(void (*callback)(wgpu::Device)) {
adapter.RequestDevice(
nullptr,
[](WGPURequestDeviceStatus status, WGPUDevice cDevice,
const char* message, void* userdata) {
wgpu::Device device = wgpu::Device::Acquire(cDevice);
device.SetUncapturedErrorCallback(
[](WGPUErrorType type, const char* message, void* userdata) {
std::cout << "Error: " << type << " - message: " << message;
},
nullptr);
reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
}, reinterpret_cast<void*>(callback));
}
더 쉽게 액세스할 수 있도록 main.cpp
파일 상단에 두 변수 wgpu::Adapter
및 wgpu::Device
를 선언합니다. GetAdapter()
를 호출하고 결과 콜백을 adapter
에 할당하도록 main()
함수를 업데이트한 다음 Start()
를 호출하기 전에 GetDevice()
를 호출하고 결과 콜백을 device
에 할당합니다.
wgpu::Adapter adapter;
wgpu::Device device;
…
int main() {
instance = wgpu::CreateInstance();
GetAdapter([](wgpu::Adapter a) {
adapter = a;
GetDevice([](wgpu::Device d) {
device = d;
Start();
});
});
}
삼각형 그리기
브라우저에서 처리하므로 스왑 체인은 JavaScript API에 노출되지 않습니다. C++에서는 수동으로 만들어야 합니다. 편의를 위해 다시 한번 main.cpp
파일 상단에 wgpu::Surface
변수를 선언합니다. Start()
에서 GLFW 창을 만든 직후 편리한 wgpu::glfw::CreateSurfaceForWindow()
함수를 호출하여 wgpu::Surface
(HTML 캔버스와 유사)를 만들고 InitGraphics()
에서 새 도우미 ConfigureSurface()
함수를 호출하여 구성합니다. 또한 surface.Present()
를 호출하여 while 루프에서 다음 텍스처를 표시해야 합니다. 아직 렌더링이 이루어지지 않으므로 눈에 띄는 효과는 없습니다.
#include <webgpu/webgpu_glfw.h>
…
wgpu::Surface surface;
wgpu::TextureFormat format;
void ConfigureSurface() {
wgpu::SurfaceCapabilities capabilities;
surface.GetCapabilities(adapter, &capabilities);
format = capabilities.formats[0];
wgpu::SurfaceConfiguration config{
.device = device,
.format = format,
.width = kWidth,
.height = kHeight};
surface.Configure(&config);
}
void InitGraphics() {
ConfigureSurface();
}
void Render() {
// TODO: Render a triangle using WebGPU.
}
void Start() {
…
surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
InitGraphics();
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
Render();
surface.Present();
instance.ProcessEvents();
}
}
이제 아래 코드로 렌더링 파이프라인을 만드는 것이 좋습니다. 더 쉽게 액세스할 수 있도록 main.cpp
파일 상단에 wgpu::RenderPipeline
변수를 선언하고 InitGraphics()
에서 도우미 함수 CreateRenderPipeline()
를 호출합니다.
wgpu::RenderPipeline pipeline;
…
const char shaderCode[] = R"(
@vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
@builtin(position) vec4f {
const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
return vec4f(pos[i], 0, 1);
}
@fragment fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
)";
void CreateRenderPipeline() {
wgpu::ShaderModuleWGSLDescriptor wgslDesc{};
wgslDesc.code = shaderCode;
wgpu::ShaderModuleDescriptor shaderModuleDescriptor{
.nextInChain = &wgslDesc};
wgpu::ShaderModule shaderModule =
device.CreateShaderModule(&shaderModuleDescriptor);
wgpu::ColorTargetState colorTargetState{.format = format};
wgpu::FragmentState fragmentState{.module = shaderModule,
.targetCount = 1,
.targets = &colorTargetState};
wgpu::RenderPipelineDescriptor descriptor{
.vertex = {.module = shaderModule},
.fragment = &fragmentState};
pipeline = device.CreateRenderPipeline(&descriptor);
}
void InitGraphics() {
…
CreateRenderPipeline();
}
마지막으로 각 프레임에서 호출되는 Render()
함수에서 렌더링 명령어를 GPU로 전송합니다.
void Render() {
wgpu::SurfaceTexture surfaceTexture;
surface.GetCurrentTexture(&surfaceTexture);
wgpu::RenderPassColorAttachment attachment{
.view = surfaceTexture.texture.CreateView(),
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store};
wgpu::RenderPassDescriptor renderpass{.colorAttachmentCount = 1,
.colorAttachments = &attachment};
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
pass.SetPipeline(pipeline);
pass.Draw(3);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
device.GetQueue().Submit(1, &commands);
}
이제 CMake로 앱을 다시 빌드하고 실행하면 창에 오래 기다려온 빨간색 삼각형이 표시됩니다. 잠시 쉬세요.

WebAssembly로 컴파일
이제 브라우저 창에 이 빨간색 삼각형을 그리도록 기존 코드베이스를 조정하는 데 필요한 최소한의 변경사항을 살펴보겠습니다. 다시 말해, 앱은 C/C++ 프로그램을 WebAssembly로 컴파일하는 도구인 Emscripten을 기준으로 빌드되며, 이 도구에는 JavaScript API 위에 webgpu.h를 구현하는 바인딩이 있습니다.
CMake 설정 업데이트
Emscripten이 설치되면 다음과 같이 CMakeLists.txt
빌드 파일을 업데이트합니다.
강조 표시된 코드만 변경하면 됩니다.
set_target_properties
는 타겟 파일에 'html' 파일 확장자를 자동으로 추가하는 데 사용됩니다. 즉, 'app.html' 파일이 생성됩니다.- Emscripten에서 WebGPU 지원을 사용 설정하려면
USE_WEBGPU
앱 링크 옵션이 필요합니다. 이 섹션이 없으면main.cpp
파일이webgpu/webgpu_cpp.h
파일에 액세스할 수 없습니다. - GLFW 코드를 재사용할 수 있도록
USE_GLFW
앱 링크 옵션도 여기에 필요합니다.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app) # Create project "app"
set(CMAKE_CXX_STANDARD 20) # Enable C++20 standard
add_executable(app "main.cpp")
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES SUFFIX ".html")
target_link_options(app PRIVATE "-sUSE_WEBGPU=1" "-sUSE_GLFW=3")
else()
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()
코드 업데이트
Emscripten에서 wgpu::surface
를 만들려면 HTML 캔버스 요소가 필요합니다. 이렇게 하려면 instance.CreateSurface()
를 호출하고 #canvas
선택기를 지정하여 Emscripten에서 생성된 HTML 페이지의 적절한 HTML 캔버스 요소와 일치시킵니다.
while 루프를 사용하는 대신 emscripten_set_main_loop(Render)
를 호출하여 Render()
함수가 브라우저와 모니터에 적절하게 정렬되는 적절한 부드러운 속도로 호출되도록 합니다.
#include <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <webgpu/webgpu_glfw.h>
#endif
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
#if defined(__EMSCRIPTEN__)
wgpu::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
canvasDesc.selector = "#canvas";
wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &canvasDesc};
surface = instance.CreateSurface(&surfaceDesc);
#else
surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif
InitGraphics();
#if defined(__EMSCRIPTEN__)
emscripten_set_main_loop(Render, 0, false);
#else
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
Render();
surface.Present();
instance.ProcessEvents();
}
#endif
}
Emscripten으로 앱 빌드
Emscripten으로 앱을 빌드하는 데 필요한 유일한 변경사항은 cmake
명령어 앞에 마법 같은 emcmake
셸 스크립트를 추가하는 것입니다. 이번에는 build-web
하위 폴더에 앱을 생성하고 HTTP 서버를 시작합니다. 마지막으로 브라우저를 열고 build-web/app.html
로 이동합니다.
# Build the app with Emscripten.
$ emcmake cmake -B build-web && cmake --build build-web
# Start a HTTP server.
$ npx http-server

다음 단계
향후 기대할 수 있는 사항은 다음과 같습니다.
- webgpu.h 및 webgpu_cpp.h API의 안정화 개선
- Android 및 iOS에 대한 Dawn 초기 지원
그동안 Emscripten의 WebGPU 문제 및 Dawn 문제와 함께 제안 및 질문을 제출해 주세요.
리소스
이 앱의 소스 코드를 자유롭게 살펴보세요.
WebGPU를 사용하여 C++로 네이티브 3D 애플리케이션을 처음부터 만드는 방법을 자세히 알아보려면 C++용 WebGPU 학습 문서 및 Dawn 네이티브 WebGPU 예시를 확인하세요.
Rust에 관심이 있다면 WebGPU를 기반으로 하는 wgpu 그래픽 라이브러리도 살펴볼 수 있습니다. hello-triangle 데모를 살펴보세요.
감사의 말
이 도움말은 코렌틴 월레즈, 카이 니노미야, 레이첼 앤드류가 검토했습니다.
사진: Unsplash의 마크-올리비에 조두앵