למפתחי אינטרנט, WebGPU הוא ממשק API של גרפיקה באינטרנט שמספק גישה מאוחדת ומהירה ל-GPU. WebGPU חושף יכולות חומרה מודרניות ומאפשר פעולות עיבוד ועיבוד גרפי ב-GPU, בדומה ל-Direct3D 12, ל-Metal ול-Vulkan.
זה נכון, אבל הסיפור הזה לא מלא. WebGPU הוא תוצאה של מאמץ משותף, כולל חברות גדולות כמו Apple, Google, Intel, Mozilla ו-Microsoft. חלק מהם הבינו ש-WebGPU יכול להיות יותר מ-API של JavaScript, אלא API גרפי בפלטפורמות שונות למפתחים בסביבות שונות, מלבד האינטרנט.
כדי לעמוד בדרישות של תרחיש לדוגמה הראשי, הוסף ממשק API של JavaScript ב-Chrome 113. עם זאת, במקביל לפרויקט הזה פותח פרויקט משמעותי נוסף: ה-API של C webgpu.h. קובץ הכותרת הזה ב-C כולל את כל התהליכים ומבני הנתונים הזמינים של WebGPU. הוא משמש כשכבת הפשטה של חומרה ללא תאימות לפלטפורמה, ומאפשר לכם לבנות אפליקציות ספציפיות לפלטפורמה באמצעות ממשק עקבי בפלטפורמות שונות.
במסמך הזה תלמדו איך לכתוב אפליקציית C++ קטנה באמצעות WebGPU שפועלת גם באינטרנט וגם בפלטפורמות ספציפיות. ספוילר: יופיע אותו משולש אדום שמופיע בחלון הדפדפן ובחלון במחשב, עם שינויים מינימליים בקוד.
איך זה עובד?
כדי לראות את האפליקציה המושלמת, אפשר להיכנס למאגר של אפליקציה מבוססת-WebGPU לפלטפורמות שונות.
האפליקציה היא דוגמה מינימלית ב-C++ שמראה איך להשתמש ב-WebGPU כדי ליצור אפליקציות למחשב ולאינטרנט מקוד בסיס יחיד. מתחת לפני השטח, הוא משתמש ב-webgpu.h של WebGPU כשכבת הפשטה של חומרה שאינה תלויה בפלטפורמה, באמצעות מעטפת C++ שנקראת webgpu_cpp.h.
באינטרנט, האפליקציה נוצרה באמצעות Emscripten, שיש לו קישורים שמטמיעים את webgpu.h מעל ל-JavaScript API. בפלטפורמות ספציפיות כמו macOS או Windows, אפשר לבנות את הפרויקט עם Dawn, ההטמעה של WebGPU בפלטפורמות שונות של Chromium. כדאי לציין גם את המונח wgpu-native, שהוא יישום Rust של webgpu.h, אבל הוא לא נמצא בשימוש במסמך הזה.
שנתחיל?
כדי להתחיל, צריך קומפילטור של C++ ו-CMake כדי לטפל ב-builds בפלטפורמות שונות באופן סטנדרטי. בתוך תיקייה ייעודית, יוצרים קובץ מקור main.cpp
וקובץ build מסוג 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 בתיקיית המשנה build/, ואת הפקודה cmake --build build
כדי לבנות את האפליקציה בפועל וליצור את קובץ ההפעלה.
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
האפליקציה פועלת, אבל עדיין אין פלט כי צריך דרך לצייר דברים במסך.
הורדת Dawn
כדי לצייר את המשולש, אפשר להשתמש ב-Dawn, ההטמעה של WebGPU בפלטפורמות שונות של Chromium. הספרייה כוללת את GLFW ב-C++ לציור במסך. אחת מהדרכים להורדת Dawn היא להוסיף אותו כמודול משנה של git למאגר. הפקודות הבאות מאחזרות אותו בתיקיית המשנה 'dawn/'.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
לאחר מכן, מוסיפים לקובץ CMakeLists.txt
את הטקסט הבא:
- האפשרות
DAWN_FETCH_DEPENDENCIES
ב-CMake מאחזרת את כל יחסי התלות של Dawn. - תיקיית המשנה
dawn/
כלולה ביעד. - האפליקציה תלויה ביעדים
dawn::webgpu_dawn
,glfw
ו-webgpu_glfw
, כדי שתוכלו להשתמש בהם בקובץmain.cpp
בהמשך.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
פתיחת חלון
עכשיו, כשזריחה, אפשר להשתמש ב-GLFW כדי לצייר דברים על המסך. הספרייה הזו נכללת ב-webgpu_glfw
לנוחות, ומאפשרת לכתוב קוד לניהול חלונות שלא תלוי בפלטפורמה.
כדי לפתוח חלון בשם 'WebGPU window' ברזולוציה של 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();
}
בניית האפליקציה מחדש והפעלתה כמו קודם גורמות עכשיו לחלון ריק. איזו התקדמות!
אחזור מכשיר GPU
ב-JavaScript, navigator.gpu
היא נקודת הכניסה לגישה ל-GPU. ב-C++, צריך ליצור ידנית משתנה wgpu::Instance
שמשמש לאותו מטרה. כדי להקל על העבודה, מגדירים את instance
בחלק העליון של הקובץ main.cpp
ומפעילים את wgpu::CreateInstance()
בתוך main()
.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
הגישה ל-GPU היא אסינכרונית בגלל הצורה של JavaScript API. ב-C++, יוצרים שתי פונקציות עזר שנקראות GetAdapter()
ו-GetDevice()
, שמחזירות פונקציית קריאה חוזרת עם wgpu::Adapter
ו-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));
}
כדי לגשת אליהם בקלות, מגדירים שני משתנים, wgpu::Adapter
ו-wgpu::Device
, בחלק העליון של הקובץ main.cpp
. צריך לעדכן את הפונקציה 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++ צריך ליצור אותו באופן ידני. שוב, למען הנוחות, מגדירים משתנה wgpu::Surface
בחלק העליון של הקובץ main.cpp
. מיד אחרי יצירת חלון GLFW ב-Start()
, קוראים לפונקציה wgpu::glfw::CreateSurfaceForWindow()
השימושית כדי ליצור wgpu::Surface
(בדומה ללוח HTML) ומגדירים אותו על ידי קריאה לפונקציית העזרה החדשה ConfigureSurface()
ב-InitGraphics()
. צריך גם להפעיל את 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();
}
}
עכשיו זה הזמן ליצור את צינור עיבוד התמונות באמצעות הקוד שבהמשך. כדי לגשת אליה בקלות, מגדירים משתנה wgpu::RenderPipeline
בחלק העליון של הקובץ main.cpp
ומפעילים את פונקציית העזר CreateRenderPipeline()
ב-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();
}
לבסוף, שולחים פקודות רינדור ל-GPU בפונקציה Render()
שנקראת בכל פריים.
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
עכשיו נראה מהם השינויים המינימליים הנדרשים כדי לשנות את קוד הבסיס הקיים כך שיצייר את המשולש האדום הזה בחלון הדפדפן. שוב, האפליקציה נוצרה באמצעות Emscripten, כלי לעיבוד תוכניות C/C++ ל-WebAssembly, עם קישורים שמטמיעים את webgpu.h מעל ל-JavaScript API.
עדכון ההגדרות של CMake
אחרי שמתקינים את Emscripten, מעדכנים את קובץ ה-build CMakeLists.txt
באופן הבא.
הקוד המודגש הוא הדבר היחיד שצריך לשנות.
- השדה
set_target_properties
משמש להוספה אוטומטית של סיומת הקובץ 'html' לקובץ היעד. במילים אחרות, ייווצר קובץ "app.html". - כדי להפעיל תמיכה ב-WebGPU ב-Emscripten, צריך להשתמש באפשרות קישור האפליקציה
USE_WEBGPU
. בלי זה, לקובץmain.cpp
לא תהיה גישה לקובץwebgpu/webgpu_cpp.h
. - צריך גם להוסיף את האפשרות של קישור לאפליקציה
USE_GLFW
כדי שתוכלו לעשות שימוש חוזר בקוד 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
כדי להתאים לרכיב הקנבס המתאים ב-HTML בדף ה-HTML שנוצר על ידי Emscripten.
במקום להשתמש בלול 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
המאמרים הבאים
זה מה שצפוי בעתיד:
- שיפורים ביציבות של ממשקי ה-API webgpu.h ו-webgpu_cpp.h.
- תמיכה ראשונית ב-Dawn ל-Android ול-iOS.
בינתיים, אפשר לשלוח הצעות ושאלות לגבי בעיות ב-WebGPU ל-Emscripten ובעיות ב-Dawn.
משאבים
אפשר להיעזר בקוד המקור של האפליקציה הזו.
כדי להבין לעומק איך יוצרים אפליקציות תלת-ממדיות מקוריות ב-C++ מהתחלה באמצעות WebGPU, כדאי לעיין במסמכי התיעוד של WebGPU ל-C++ ובדוגמאות ל-Dawn Native WebGPU.
אם אתם מתעניינים ב-Rust, אתם יכולים גם לבדוק את ספריית הגרפיקה wgpu שמבוססת על WebGPU. כדאי לעיין בהדגמה של hello-triangle.
אימות חתימות
המאמר הזה נבדקה על ידי קורנטין ואלז, קאי נינומיה ורייצ'ל אנדרו.
צילום: Marc-Olivier Jodoin ב-Unsplash.