WebGPU로 앱 빌드

François Beaufort
François Beaufort

게시일: 2023년 7월 20일, 최종 업데이트: 2025년 8월 27일

웹 개발자에게 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++ 앱을 작성하는 방법을 알아봅니다. 스포일러를 하자면, 코드베이스를 최소한으로 조정하여 브라우저 창과 데스크톱 창에 표시되는 것과 동일한 빨간색 삼각형을 얻을 수 있습니다.

macOS의 브라우저 창과 데스크톱 창에 WebGPU로 구동되는 빨간색 삼각형의 스크린샷
브라우저 창과 데스크톱 창에서 WebGPU로 구동되는 동일한 삼각형

기본 원리

완성된 애플리케이션을 보려면 WebGPU 교차 플랫폼 앱 저장소를 확인하세요.

이 앱은 WebGPU를 사용하여 단일 코드베이스에서 데스크톱 및 웹 앱을 빌드하는 방법을 보여주는 최소한의 C++ 예시입니다. 내부적으로는 WebGPU의 webgpu.hwebgpu_cpp.h라는 C++ 래퍼를 통해 플랫폼에 구애받지 않는 하드웨어 추상화 레이어로 사용합니다.

웹에서 앱은 JavaScript API 위에 webgpu.h를 구현하는 바인딩이 있는 emdawnwebgpu (Emscripten Dawn WebGPU)에 대해 빌드됩니다. 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.22) # 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

앱이 실행되지만 화면에 항목을 그릴 방법이 필요하므로 아직 출력은 없습니다.

Get 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 종속 항목을 가져옵니다.
  • CMake DAWN_BUILD_MONOLITHIC_LIBRARY 옵션은 모든 Dawn 구성요소를 단일 라이브러리로 번들링합니다.
  • dawn/ 하위 폴더가 타겟에 포함됩니다.
  • 나중에 main.cpp 파일에서 사용할 수 있도록 앱이 webgpu_dawn, webgpu_glfw, glfw 타겟에 종속됩니다.

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

창 열기

이제 Dawn을 사용할 수 있으므로 GLFW를 사용하여 화면에 항목을 그립니다. 편의를 위해 webgpu_glfw에 포함된 이 라이브러리를 사용하면 창 관리를 위해 플랫폼에 구애받지 않는 코드를 작성할 수 있습니다.

해상도가 512x512인 'WebGPU window'라는 창을 열려면 main.cpp 파일을 아래와 같이 업데이트합니다. 여기서는 특정 그래픽 API 초기화를 요청하지 않기 위해 glfwWindowHint()가 사용됩니다.

#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();
}

이제 앱을 다시 빌드하고 이전과 같이 실행하면 빈 창이 표시됩니다. 진전을 보이고 있습니다.

빈 macOS 창의 스크린샷
빈 창입니다.

GPU 기기 가져오기

JavaScript에서 navigator.gpu는 GPU에 액세스하기 위한 진입점입니다. C++에서는 동일한 용도로 사용되는 wgpu::Instance 변수를 수동으로 만들어야 합니다. 편의를 위해 main.cpp 파일 상단에서 instance을 선언하고 Init() 내에서 wgpu::CreateInstance()을 호출합니다.

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

main.cpp 파일 상단에서 두 변수 wgpu::Adapterwgpu::Device를 선언합니다. instance.RequestAdapter()를 호출하고 결과 콜백을 adapter에 할당한 다음 adapter.RequestDevice()를 호출하고 결과 콜백을 device에 할당하도록 Init() 함수를 업데이트합니다.

#include <iostream>

#include <dawn/webgpu_cpp_print.h>


wgpu::Adapter adapter;
wgpu::Device device;


void Init() {
  

  wgpu::Future f1 = instance.RequestAdapter(
      nullptr, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
         wgpu::StringView message) {
        if (status != wgpu::RequestAdapterStatus::Success) {
          std::cout << "RequestAdapter: " << message << "\n";
          exit(0);
        }
        adapter = std::move(a);
      });
  instance.WaitAny(f1, UINT64_MAX);

  wgpu::DeviceDescriptor desc{};
  desc.SetUncapturedErrorCallback([](const wgpu::Device&,
                                     wgpu::ErrorType errorType,
                                     wgpu::StringView message) {
    std::cout << "Error: " << errorType << " - message: " << message << "\n";
  });

  wgpu::Future f2 = adapter.RequestDevice(
      &desc, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestDeviceStatus status, wgpu::Device d,
         wgpu::StringView message) {
        if (status != wgpu::RequestDeviceStatus::Success) {
          std::cout << "RequestDevice: " << message << "\n";
          exit(0);
        }
        device = std::move(d);
      });
  instance.WaitAny(f2, UINT64_MAX);
}

삼각형 그리기

브라우저에서 처리하므로 스왑 체인은 JavaScript API에 노출되지 않습니다. C++에서는 수동으로 만들어야 합니다. 편의를 위해 main.cpp 파일 상단에 wgpu::Surface 변수를 다시 선언합니다. Start()에서 GLFW 창을 만든 직후 편리한 wgpu::glfw::CreateSurfaceForWindow() 함수를 호출하여 wgpu::Surface (HTML 캔버스와 유사)를 만들고 InitGraphics()에서 새 도우미 ConfigureSurface() 함수를 호출하여 구성합니다. 또한 while 루프에서 다음 텍스처를 표시하려면 surface.Present()를 호출해야 합니다. 아직 렌더링이 발생하지 않으므로 눈에 띄는 효과는 없습니다.

#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,
                                    .presentMode = wgpu::PresentMode::Fifo};
  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::ShaderSourceWGSL wgsl{{.code = shaderCode}};

  wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl};
  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로 앱을 다시 빌드하고 실행하면 오랫동안 기다려 온 빨간색 삼각형이 창에 표시됩니다. 잠시 쉬세요.

macOS 창에 있는 빨간색 삼각형의 스크린샷
데스크톱 창의 빨간색 삼각형

WebAssembly로 컴파일

이제 기존 코드베이스를 조정하여 브라우저 창에 이 빨간색 삼각형을 그리는 데 필요한 최소한의 변경사항을 살펴보겠습니다. 여기서 앱은 JavaScript API 위에 webgpu.h를 구현하는 바인딩이 있는 emdawnwebgpu (Emscripten Dawn WebGPU)에 대해 빌드됩니다. C/C++ 프로그램을 WebAssembly로 컴파일하는 도구인 Emscripten을 사용합니다.

CMake 설정 업데이트

Emscripten이 설치되면 다음과 같이 CMakeLists.txt 빌드 파일을 업데이트합니다. 강조 표시된 코드만 변경하면 됩니다.

  • set_target_properties는 타겟 파일에 'html' 파일 확장자를 자동으로 추가하는 데 사용됩니다. 즉, 'app.html' 파일을 생성합니다.
  • emdawnwebgpu_cpp 타겟 링크 라이브러리는 Emscripten에서 WebGPU 지원을 사용 설정합니다. 이 권한이 없으면 main.cpp 파일이 webgpu/webgpu_cpp.h 파일에 액세스할 수 없습니다.
  • ASYNCIFY=1 앱 링크 옵션을 사용하면 동기 C++ 코드가 비동기 JavaScript와 상호작용할 수 있습니다.
  • USE_GLFW=3 앱 링크 옵션은 Emscripten에 GLFW 3 API의 기본 제공 JavaScript 구현을 사용하도록 지시합니다.
cmake_minimum_required(VERSION 3.22) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
  target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
  target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)
endif()

코드 업데이트

while 루프를 사용하는 대신 emscripten_set_main_loop(Render)를 호출하여 Render() 함수가 브라우저 및 모니터와 적절하게 일치하는 적절한 부드러운 속도로 호출되도록 합니다.

#include <iostream>

#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
  
#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 제공)