Đối với nhà phát triển web, WebGPU là API đồ hoạ trên web cung cấp tính năng hợp nhất và nhanh chóng quyền truy cập vào GPU. WebGPU tiếp cận các tính năng phần cứng hiện đại và cho phép kết xuất hình ảnh và các hoạt động tính toán trên GPU, tương tự như Direct3D 12, Metal và Vulkan.
Tuy nhiên, câu chuyện đó chưa đầy đủ. WebGPU là kết quả của quá trình cộng tác bao gồm cả các công ty lớn như Apple, Google, Intel, Mozilla và Microsoft. Trong số đó, một số người nhận ra WebGPU có thể không chỉ là một API JavaScript, mà còn là một hình ảnh đồ hoạ trên nhiều nền tảng API dành cho nhà phát triển trên các hệ sinh thái, không phải web.
Để đáp ứng trường hợp sử dụng chính, API JavaScript cần được giới thiệu trong Chrome 113. Tuy nhiên, một vấn đề quan trọng khác đã được phát triển cùng với nó: webgpu.h C API. 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. Đóng vai trò là một tầng trừu tượng phần cứng không phụ thuộc vào nền tảng, cho phép bạn tạo các ứng dụng dành riêng cho nền tảng bằng cách cung cấp giao diện nhất quán trên các nền tảng khác nhau.
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 cả trên web và các nền tảng cụ thể. Cảnh báo tiết lộ nội dung, 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àn hình với những điều chỉnh tối thiểu đối với cơ sở mã của bạn.
Cách thức hoạt động
Để xem ứng dụng đã hoàn tất, hãy xem kho lưu trữ ứng dụng nhiều nền tảng WebGPU.
Ứng dụng này là một ví dụ C++ tối giản cho thấy cách sử dụng WebGPU để tạo ứng dụng web và ứng dụng máy tính từ một cơ sở mã duy nhất. Về sau, lớp này sử dụng webgpu.h của WebGPU làm lớp trừu tượng phần cứng, không phụ thuộc vào 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 được xây dựng dựa trên Emscripten, có mối liên kết triển khai webgpu.h bên cạnh API JavaScript. Trên các nền tảng cụ thể như macOS hoặc Windows, dự án này có thể được xây dựng dựa trên Dawn, phương thức triển khai WebGPU trên nhiều nền tảng của Chromium. Điều đáng chú ý là wgpu-native, một cách triển khai Rust của webgpu.h, cũng tồn tại nhưng không được sử dụng trong tài liệu này.
Bắt đầu
Để bắt đầu, bạn cần có một trình biên dịch C++ và CMake để xử lý các bản dựng đa nền tảng theo cách chuẩn. Bên trong một thư mục chuyên dụng, hãy tạo một
Tệp nguồn main.cpp
và một tệp bản dựng CMakeLists.txt
.
Hiện tại, tệp main.cpp
cần 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 thực thi là "app" và mã nguồn là 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")
Chạy cmake -B build
để tạo tệp bản dựng trong một "build/" thư mục con và cmake --build build
để tạo ứ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ó kết quả nào, vì bạn cần có cách để vẽ các nội dung trên màn hình.
Bình minh
Để vẽ hình tam giác, bạn có thể tận dụng Dawn, mô hình triển khai WebGPU trên nhiều 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 mô-đun này dưới dạng mô-đun con git vào kho lưu trữ của bạn. Các lệnh sau đây tìm nạp lệnh gọi lúc "bình minh/" thư mục con.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Sau đó, hãy thêm vào tệp CMakeLists.txt
như sau:
- Tuỳ chọn
DAWN_FETCH_DEPENDENCIES
của CMake tìm nạp tất cả phần phụ thuộc Dawn. - Thư mục con
dawn/
được đưa vào trong đích. - Ứng dụng của bạn sẽ phụ thuộc vào các mục tiêu
dawn::webgpu_dawn
,glfw
vàwebgpu_glfw
để bạn có thể sử dụng chúng trong tệpmain.cpp
sau này.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
Mở một cửa sổ
Bây giờ Dawn đã có sẵn, hãy sử dụng GLFW để vẽ các nội dung trên màn hình. Để thuận tiện, thư viện này có trong webgpu_glfw
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ở cửa sổ có tên "Cửa sổ WebGPU" có độ phân giải 512x512, hãy cập nhật tệp main.cpp
như bên dưới. Lưu ý rằng glfwWindowHint()
được dùng ở đây để không yêu cầu 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();
}
Bây giờ, việc tạo lại 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ộ!
Tải thiết bị GPU
Trong JavaScript, navigator.gpu
là điểm truy cập để truy cập vào GPU. Trong C++, bạn cần tạo thủ công một biến wgpu::Instance
dù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 main()
.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
Việc truy cập vào GPU không đồng bộ do hình dạng của API JavaScript. Trong C++, hãy tạo 2 hàm trợ giúp có tên là GetAdapter()
và GetDevice()
. Hàm này lần lượt trả về một hàm callback có wgpu::Adapter
và wgpu::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));
}
Để truy cập dễ dàng hơn, hãy khai báo hai biến wgpu::Adapter
và wgpu::Device
ở đầu tệp main.cpp
. Cập nhật hàm main()
để gọi GetAdapter()
và chỉ định lệnh gọi lại kết quả cho adapter
, sau đó gọi GetDevice()
và chỉ định lệnh gọi lại kết quả cho device
trước khi gọi 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();
});
});
}
Vẽ hình tam giác
Chuỗi hoán đổi không hiển thị trong API JavaScript vì trình duyệt sẽ xử lý. Trong C++, bạn cần tạo mã theo cách thủ công. Một lần nữa, để thuận tiện, hãy khai báo 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 mới ConfigureSurface()
trong InitGraphics()
. Bạn cũng cần gọi surface.Present()
để hiển thị 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 rõ ràng 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};
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();
}
}
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. Để truy cập dễ dàng hơn, hãy khai báo 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::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();
}
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 là từng khung.
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);
}
Tạo lại ứng dụng bằng CMake và chạy ứng dụng này giờ đây sẽ tạo ra hình tam giác màu đỏ mà bạn đã chờ đợi từ lâu trong một cửa sổ! Hãy nghỉ giải lao một chút nhé.
Biên dịch lên 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ó của bạn nhằm vẽ hình tam giác màu đỏ này trong cửa sổ trình duyệt. Xin nhắc lại, ứng dụng này được xây dựng dựa trên Emscripten, một công cụ biên dịch các chương trình C/C++ sang WebAssembly, công cụ này có các mối liên kết triển khai webgpu.h bên trên API JavaScript.
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.
Mã được đánh dấu là phần duy nhất bạn cần thay đổi.
set_target_properties
dùng để tự động thêm "html" vào tệp đích. Nói cách khác, bạn sẽ tạo một "app.html" .- Bạn phải chọn tuỳ chọn đường liên kết ứng dụng
USE_WEBGPU
để bật tính năng hỗ trợ WebGPU trong Emscripten. Nếu không có phương thức này, tệpmain.cpp
của bạn sẽ không thể truy cập vào tệpwebgpu/webgpu_cpp.h
. - Bạn cũng phải chọn đường liên kết đến ứng dụng
USE_GLFW
tại đây để có thể sử dụng lại mã 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()
Cập nhật đoạn mã
Trong Emscripten, việc tạo wgpu::surface
cần phải có phần tử canvas HTML. Để thực hiện việc này, hãy gọi instance.CreateSurface()
và chỉ định bộ chọn #canvas
để khớp với phần tử canvas HTML thích hợp trên trang HTML do Emscripten tạo.
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 độ trơn tru thích hợp và khớp với trình duyệt và màn hình.
#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
}
Xây dựng ứng dụng bằng Emscripten
Thay đổi duy nhất cần thiết để xây dựng ứng dụng bằng Emscripten là thêm tập lệnh cmake
bằng tập lệnh magical emcmake
shell. Lần này, hãy tạo ứng dụng trong thư mục con build-web
và khởi động máy chủ HTTP. Cuối cùng, hãy mở trình duyệt rồi truy cập 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
Các bước tiếp theo
Dưới đây là những gì bạn có thể mong đợi trong tương lai:
- Những điểm cải tiến về tính ổn định của các API webgpu.h và webgpu_cpp.h.
- Hỗ trợ ban đầu Bình minh cho Android và iOS.
Trong thời gian chờ đợi, vui lòng gửi nội dung đề xuất và câu hỏi cho các vấn đề về WebGPU đối với Emscripten và Dawn.
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 các ứng dụng 3D gốc trong C++ từ đầu bằng WebGPU, hãy tham khảo Tìm hiểu tài liệu về WebGPU dành cho C++ và 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ọ.
Thư cảm ơn
Bài viết này do Corentin Wallez, Kai Ninomiya và Rachel Andrew đánh giá.
Ảnh của Marc-Olivier Jodoin trên Unsplash.