Tạo ứng dụng bằng WebGPU

François Beaufort
François Beaufort

Xuất bản: ngày 20 tháng 7 năm 2023, Cập nhật lần gần đây nhất: ngày 27 tháng 8 năm 2025

Đối với nhà phát triển web, WebGPU là một API đồ hoạ web cung cấp quyền truy cập hợp nhất và nhanh chóng vào GPU. WebGPU khai thác các chức năng phần cứng hiện đại và cho phép các thao tác kết xuất và tính toán trên GPU, tương tự như Direct3D 12, Metal và Vulkan.

Mặc dù đúng, nhưng câu chuyện đó chưa hoàn chỉnh. WebGPU là kết quả của một nỗ lực hợp tác, bao gồm các công ty lớn như Apple, Google, Intel, Mozilla và Microsoft. Trong số đó, một số người nhận ra rằng WebGPU có thể không chỉ là một API Javascript mà còn là một API đồ hoạ đa nền tảng dành cho các nhà phát triển trên nhiều hệ sinh thái, ngoài web.

Để đáp ứng trường hợp sử dụng chính, một API JavaScript đã được ra mắt trong Chrome 113. Tuy nhiên, một dự án quan trọng khác cũng được phát triển song song với dự án này: API C webgpu.h. Tệp tiêu đề C này liệt kê tất cả các quy trình và cấu trúc dữ liệu có sẵn của WebGPU. Đây là một lớp trừu tượng phần cứng độc lập với nền tảng, cho phép bạn tạo các ứng dụng dành riêng cho từng nền tảng bằng cách cung cấp một giao diện nhất quán trên nhiều nền tảng.

Trong tài liệu này, bạn sẽ tìm hiểu cách viết một ứng dụng C++ nhỏ bằng WebGPU chạy trên cả web và các nền tảng cụ thể. Cảnh báo nội dung tiết lộ, bạn sẽ nhận được cùng một hình tam giác màu đỏ xuất hiện trong cửa sổ trình duyệt và cửa sổ máy tính với những điều chỉnh tối thiểu cho cơ sở mã của bạn.

Ảnh chụp màn hình một hình tam giác màu đỏ chạy bằng WebGPU trong cửa sổ trình duyệt và cửa sổ máy tính trên macOS.
Cùng một hình tam giác do WebGPU cung cấp trong cửa sổ trình duyệt và cửa sổ máy tính.

Cách thức hoạt động

Để xem ứng dụng hoàn chỉnh, hãy xem kho lưu trữ Ứng dụng đa nền tảng WebGPU.

Ứng dụng này là một ví dụ tối giản về C++ cho thấy cách sử dụng WebGPU để tạo ứng dụng dành cho máy tính và ứng dụng web chỉ từ một bộ mã cơ sở. Về cơ bản, thư viện này sử dụng webgpu.h của WebGPU làm lớp trừu tượng phần cứng độc lập với nền tảng thông qua một trình bao bọc C++ có tên là webgpu_cpp.h.

Trên web, ứng dụng này được xây dựng dựa trên emdawnwebgpu (Emscripten Dawn WebGPU), có các liên kết triển khai webgpu.h trên API JavaScript. Trên các nền tảng cụ thể như macOS hoặc Windows, dự án này có thể được tạo dựa trên Dawn, một cách triển khai WebGPU đa nền tảng của Chromium. Cũng cần đề cập đến wgpu-native, một cách triển khai Rust của webgpu.h, nhưng không được dùng trong tài liệu này.

Bắt đầu

Để bắt đầu, bạn cần có trình biên dịch C++ và CMake để xử lý các bản dựng trên nhiều nền tảng theo cách tiêu chuẩn. Trong một thư mục chuyên dụng, hãy tạo tệp nguồn main.cpp và tệp bản dựng CMakeLists.txt.

Tệp main.cpp hiện tại phải chứa một hàm main() trống.

int main() {}

Tệp CMakeLists.txt chứa thông tin cơ bản về dự án. Dòng cuối cùng chỉ định tên tệp thực thi là "app" và mã nguồn của tệp này là 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")

Chạy cmake -B build để tạo các tệp bản dựng trong thư mục con "build/" và cmake --build build để thực sự tạo bản dựng ứng dụng và tạo tệp thực thi.

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

Ứng dụng chạy nhưng chưa có đầu ra vì bạn cần một cách để vẽ các đối tượng trên màn hình.

Get Dawn

Để vẽ tam giác, bạn có thể tận dụng Dawn, một cách triển khai WebGPU đa nền tảng của Chromium. Trong đó có thư viện C++ GLFW để vẽ lên màn hình. Một cách để tải Dawn xuống là thêm Dawn làm git submodule vào kho lưu trữ của bạn. Các lệnh sau đây sẽ tìm nạp tệp này trong thư mục con "dawn/".

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

Sau đó, hãy thêm vào tệp CMakeLists.txt như sau:

  • Lựa chọn DAWN_FETCH_DEPENDENCIES của CMake sẽ tìm nạp tất cả các phần phụ thuộc của Dawn.
  • Lựa chọn DAWN_BUILD_MONOLITHIC_LIBRARY của CMake sẽ kết hợp tất cả các thành phần Dawn vào một thư viện duy nhất.
  • Thư mục con dawn/ có trong mục tiêu.
  • Ứng dụng của bạn sẽ phụ thuộc vào các mục tiêu webgpu_dawn, webgpu_glfwglfw để bạn có thể sử dụng chúng trong tệp main.cpp sau này.

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)

Mở cửa sổ

Giờ đây, khi Dawn đã có sẵn, hãy dùng GLFW để vẽ các đối tượng trên màn hình. Thư viện này có trong webgpu_glfw để thuận tiện cho bạn, cho phép bạn viết mã không phụ thuộc vào nền tảng để quản lý cửa sổ.

Để mở một cửa sổ có tên "WebGPU window" với độ phân giải 512x512, hãy cập nhật tệp main.cpp như bên dưới. Xin lưu ý rằng glfwWindowHint() được dùng ở đây để yêu cầu không khởi chạy API đồ hoạ cụ thể.

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

Việc tạo lại ứng dụng và chạy ứng dụng như trước đây sẽ dẫn đến một cửa sổ trống. Bạn đang tiến bộ!

Ảnh chụp màn hình một cửa sổ macOS trống.
Một cửa sổ trống.

Nhận thiết bị GPU

Trong JavaScript, navigator.gpu là điểm truy cập để bạn truy cập vào GPU. Trong C++, bạn cần tạo một biến wgpu::Instance theo cách thủ công cho cùng một mục đích. Để thuận tiện, hãy khai báo instance ở đầu tệp main.cpp và gọi wgpu::CreateInstance() bên trong Init().

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

Khai báo hai biến wgpu::Adapterwgpu::Device ở đầu tệp main.cpp. Cập nhật hàm Init() để gọi instance.RequestAdapter() và chỉ định lệnh gọi lại kết quả của hàm này cho adapter, sau đó gọi adapter.RequestDevice() và chỉ định lệnh gọi lại kết quả của hàm này cho device.

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

Vẽ một hình tam giác

Chuỗi hoán đổi không được hiển thị trong API JavaScript vì trình duyệt sẽ xử lý chuỗi này. Trong C++, bạn cần tạo đối tượng này theo cách thủ công. Một lần nữa, để thuận tiện, hãy khai báo một biến wgpu::Surface ở đầu tệp main.cpp. Ngay sau khi tạo cửa sổ GLFW trong Start(), hãy gọi hàm wgpu::glfw::CreateSurfaceForWindow() tiện dụng để tạo wgpu::Surface (tương tự như canvas HTML) và định cấu hình bằng cách gọi hàm trợ giúp ConfigureSurface() mới trong InitGraphics(). Bạn cũng cần gọi surface.Present() để trình bày hoạ tiết tiếp theo trong vòng lặp while. Thao tác này không tạo ra hiệu ứng nào vì chưa có quá trình kết xuất nào diễn ra.

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

Bây giờ là thời điểm thích hợp để tạo quy trình kết xuất bằng mã bên dưới. Để dễ dàng truy cập, hãy khai báo một biến wgpu::RenderPipeline ở đầu tệp main.cpp và gọi hàm trợ giúp CreateRenderPipeline() trong InitGraphics().

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

Cuối cùng, hãy gửi các lệnh kết xuất đến GPU trong hàm Render() được gọi cho mỗi khung hình.

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

Việc tạo lại ứng dụng bằng CMake và chạy ứng dụng hiện sẽ cho ra kết quả là hình tam giác màu đỏ mà bạn hằng mong đợi trong một cửa sổ! Hãy nghỉ ngơi một chút vì bạn xứng đáng được như vậy.

Ảnh chụp màn hình cho thấy một hình tam giác màu đỏ trong cửa sổ macOS.
Hình tam giác màu đỏ trong cửa sổ trên máy tính.

Biên dịch sang WebAssembly

Bây giờ, hãy xem những thay đổi tối thiểu cần thiết để điều chỉnh cơ sở mã hiện có nhằm vẽ hình tam giác màu đỏ này trong cửa sổ trình duyệt. Một lần nữa, ứng dụng này được xây dựng dựa trên emdawnwebgpu (Emscripten Dawn WebGPU), có các liên kết triển khai webgpu.h trên API JavaScript. Công cụ này sử dụng Emscripten, một công cụ để biên dịch các chương trình C/C++ sang WebAssembly.

Cập nhật chế độ cài đặt CMake

Sau khi cài đặt Emscripten, hãy cập nhật tệp bản dựng CMakeLists.txt như sau. Đoạn mã được đánh dấu là đoạn mã duy nhất bạn cần thay đổi.

  • set_target_properties được dùng để tự động thêm đuôi "html" vào tệp đích. Nói cách khác, bạn sẽ tạo một tệp "app.html".
  • Thư viện đường liên kết mục tiêu emdawnwebgpu_cpp cho phép hỗ trợ WebGPU trong Emscripten. Nếu không có khoá này, tệp main.cpp của bạn sẽ không thể truy cập vào tệp webgpu/webgpu_cpp.h.
  • Lựa chọn đường liên kết đến ứng dụng ASYNCIFY=1 cho phép mã C++ đồng bộ tương tác với JavaScript không đồng bộ.
  • Lựa chọn liên kết ứng dụng USE_GLFW=3 cho Emscripten biết rằng bạn muốn sử dụng chế độ triển khai JavaScript tích hợp của API GLFW 3.
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()

Cập nhật mã

Thay vì sử dụng vòng lặp while, hãy gọi emscripten_set_main_loop(Render) để đảm bảo hàm Render() được gọi ở tốc độ mượt mà phù hợp, khớp với trình duyệt và màn hình.

#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
}

Tạo ứng dụng bằng Emscripten

Thay đổi duy nhất cần thiết để tạo ứng dụng bằng Emscripten là thêm các lệnh cmake vào trước bằng tập lệnh shell emcmake kỳ diệu. Lần này, hãy tạo ứng dụng trong thư mục con build-web và khởi động một máy chủ HTTP. Cuối cùng, hãy mở trình duyệt và truy cập vào 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
Ảnh chụp màn hình cho thấy một hình tam giác màu đỏ trong cửa sổ trình duyệt.
Một hình tam giác màu đỏ trong cửa sổ trình duyệt.

Bước tiếp theo

Dưới đây là những điểm cải tiến trong tương lai:

  • Cải thiện độ ổn định của các API webgpu.h và webgpu_cpp.h.
  • Dawn hỗ trợ ban đầu cho Android và iOS.

Trong thời gian chờ đợi, vui lòng gửi các vấn đề về WebGPU cho Emscriptencác vấn đề về Dawn kèm theo đề xuất và câu hỏi.

Tài nguyên

Bạn có thể khám phá mã nguồn của ứng dụng này.

Nếu bạn muốn tìm hiểu thêm về cách tạo ứng dụng 3D gốc bằng C++ từ đầu bằng WebGPU, hãy xem Tài liệu Tìm hiểu WebGPU cho C++Các ví dụ về WebGPU gốc của Dawn.

Nếu quan tâm đến Rust, bạn cũng có thể khám phá thư viện đồ hoạ wgpu dựa trên WebGPU. Hãy xem bản minh hoạ hello-triangle của họ.

Lời cảm ơn

Bài viết này được Corentin Wallez, Kai NinomiyaRachel Andrew xem xét.

Ảnh của Marc-Olivier Jodoin trên Unsplash.