WebGPU로 앱 빌드

François Beaufort
François Beaufort

웹 개발자를 위한 WebGPU는 빠른 통합성 및 빠른 성능을 제공하는 웹 그래픽 API입니다. 액세스할 수 있습니다 최신 하드웨어 기능을 노출하고 렌더링을 허용하는 WebGPU GPU에서의 계산 작업을 지원합니다.

사실이지만 그 이야기는 불완전합니다. WebGPU는 협업을 통한 이러한 노력에 힘입어 Apple, Google, Intel, Mozilla, Microsoft를 선택합니다. 그중에서 몇 가지 실현 WebGPU는 JavaScript API 그 이상일 수 있지만 웹 외의 생태계 전반의 개발자를 위한 API입니다.

기본적인 사용 사례를 처리하기 위해 JavaScript API는 도입되었습니다. 하지만 또 다른 중요한 이 프로젝트는 이와 함께 개발되었습니다. webgpu.h C API에 액세스할 수 있습니다. 이 C 헤더 파일에는 사용 가능한 모든 절차와 데이터 구조가 나열되어 있습니다. 구현됩니다 플랫폼에 구애받지 않는 하드웨어 추상화 계층 역할을 하여 일관된 인터페이스를 제공하여 플랫폼별 애플리케이션을 구축할 수 있음 확인할 수 있습니다

이 문서에서는 WebGPU를 사용하여 웹과 특정 플랫폼에서 모두 실행되는 소형 C++ 앱을 작성하는 방법을 알아봅니다. 스포일러 경고, 코드베이스를 거의 조정하지 않은 채 브라우저 창과 데스크톱 창에 나타나는 것과 동일한 빨간색 삼각형이 표시됩니다.

<ph type="x-smartling-placeholder">
</ph> 브라우저 창의 WebGPU 기반 빨간색 삼각형과 macOS의 데스크톱 창의 스크린샷 <ph type="x-smartling-placeholder">
</ph> 브라우저 창과 데스크톱 창에서 WebGPU로 구동하는 동일한 삼각형

기본 원리

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

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

웹에서 앱은 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

앱은 실행되지만 아직 출력이 없습니다. 화면에 무언가를 그리는 방법이 필요합니다.

여명 시간

삼각형을 그리려면 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에 포함된 이 라이브러리를 사용하면 창 관리를 위해 플랫폼에 구애받지 않는 코드를 작성할 수 있습니다.

'WebGPU 창'이라는 창 열기 해상도가 512x512인 경우 다음과 같이 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();
}

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

<ph type="x-smartling-placeholder">
</ph> 빈 macOS 창의 스크린샷 <ph type="x-smartling-placeholder">
</ph> 빈 창

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++에서 GetAdapter()GetDevice()라는 도우미 함수 두 개를 만듭니다. 이 함수는 각각 wgpu::Adapterwgpu::Device가 있는 콜백 함수를 반환합니다.

#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::Adapterwgpu::Device를 선언합니다. main() 함수를 업데이트하여 GetAdapter()를 호출하고 결과 콜백을 adapter에 할당한 다음 GetDevice()를 호출하고 결과 콜백을 device에 할당한 후 Start()를 호출합니다.

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로 앱을 다시 빌드하고 이제 실행하면 창에서 오래 기다리던 빨간색 삼각형이 표시됩니다. 오늘은 충분히 휴식을 취하세요.

<ph type="x-smartling-placeholder">
</ph> macOS 창의 빨간색 삼각형 스크린샷 <ph type="x-smartling-placeholder">
</ph> 데스크톱 창의 빨간색 삼각형

WebAssembly로 컴파일

이제 브라우저 창에 이 빨간색 삼각형을 그리도록 기존 코드베이스를 조정하는 데 필요한 최소한의 변경사항을 살펴보겠습니다. 이 앱은 C/C++ 프로그램을 WebAssembly로 컴파일하는 도구인 Emscripten을 기반으로 빌드되었습니다. WebAssembly에는 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
드림 <ph type="x-smartling-placeholder">
</ph> 브라우저 창에 표시된 빨간색 삼각형 스크린샷
브라우저 창의 빨간색 삼각형

다음 단계

앞으로 변경되는 내용은 다음과 같습니다.

  • webgpu.h 및 webgpu_cpp.h API의 안정성이 개선되었습니다.
  • Android 및 iOS에 대한 Dawn 초기 지원

그동안 Emscripten의 WebGPU 문제Dawn 문제에 추천 및 질문을 제출해 주세요.

리소스

이 앱의 소스 코드를 자유롭게 살펴보세요.

WebGPU를 사용하여 C++에서 네이티브 3D 애플리케이션을 처음부터 만드는 방법을 자세히 알아보려면 C++용 WebGPU 알아보기 문서Dawn Native WebGPU 예시를 확인하세요.

Rust에 관심이 있다면 WebGPU 기반의 wgpu 그래픽 라이브러리를 살펴볼 수도 있습니다. hello-triangle 데모를 살펴보세요.

감사의 말

이 도움말은 코렌틴 월레즈, 카이 니노미야, 레이첼 앤드류가 검토했습니다.

사진: Marc-Olivier Jodoin, Unsplash