WebGPU הוא ממשק API של גרפיקה באינטרנט שמספק גישה מאוחדת ומהירה למעבדי GPU עבור מפתחי אינטרנט. WebGPU חושף את יכולות החומרה המודרניות ומאפשר פעולות של רינדור וחישוב ב-GPU, בדומה ל-Direct3D 12, Meta ו-Vulkan.
למרות שהסיפור הוא נכון, הסיפור חלקי. WebGPU הוא תוצאה של מאמץ משותף, כולל חברות גדולות כמו Apple, Google, Intel, Mozilla ו-Microsoft. חלק מהם הבינו ש-WebGPU יכול להיות יותר מ-JavaScript API, אלא ממשק API של גרפיקה בפלטפורמות שונות, שמיועד למפתחים בסביבות שונות, מלבד האינטרנט.
כדי למלא את התרחיש העיקרי לדוגמה, הושק JavaScript API ב-Chrome 113. עם זאת, פותח לצדו פרויקט משמעותי נוסף: ה-C API של webgpu.h. קובץ הכותרת C מפרט את כל הנהלים ומבני הנתונים הזמינים של WebGPU. היא משמשת כשכבה להפשטת חומרה שאינה קשורה לפלטפורמה, ומאפשרת ליצור אפליקציות ספציפיות לפלטפורמה באמצעות ממשק עקבי בפלטפורמות שונות.
במסמך הזה תלמדו איך לכתוב אפליקציית C++ קטנה באמצעות WebGPU שפועלת גם באינטרנט וגם בפלטפורמות ספציפיות. התראת ספוילר, תראו את אותו משולש אדום שמופיע בחלון הדפדפן ובחלון במחשב, עם שינויים קלים ב-codebase.
איך זה עובד?
כדי לראות את האפליקציה המלאה, ניתן לעיין במאגר של WebGPU app בפלטפורמות שונות.
האפליקציה היא דוגמה מינימליסטית ל-C++ שמראה איך להשתמש ב-WebGPU כדי לבנות אפליקציות למחשב ואינטרנט על בסיס קוד אחד. מאחורי הקלעים, נעשה שימוש ב-webgpu.h של WebGPU כשכבה להפשטת חומרה שאינה קשורה לפלטפורמה, דרך wrapper של C++ שנקרא webgpu_cpp.h.
באינטרנט, האפליקציה מבוססת על Emscripten, שכולל קישורים שמטמיעים את webgpu.h מעל ה-API של JavaScript. בפלטפורמות ספציפיות כמו macOS או Windows, אפשר לפתח את הפרויקט הזה מול Dawn, הטמעת WebGPU בפלטפורמות שונות של Chromium. כדאי לציין את wgpu-native, יישום Rust של webgpu.h, שגם הוא קיים, אבל לא נמצא בשימוש במסמך זה.
אני רוצה לנסות
כדי להתחיל, יש צורך במהדר C++ וב-CMake כדי לטפל בגרסאות build בפלטפורמות שונות בדרך רגילה. בתיקייה ייעודית, יוצרים קובץ מקור 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, ההטמעה של WebGPU בפלטפורמות שונות של Chromium. התוכן כולל את ספריית C++ של GLFW לשרטוט במסך. אחת מהדרכים להוריד את שחר היא להוסיף אותו כמודול משנה של Git למאגר. הפקודות הבאות מאחזרות אותו בתיקיית המשנה "dawn/".
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
לאחר מכן, מוסיפים לקובץ CMakeLists.txt
באופן הבא:
- האפשרות CMake
DAWN_FETCH_DEPENDENCIES
מאחזרת את כל יחסי התלות של שחר. - תיקיית המשנה
dawn/
כלולה ביעד. - האפליקציה שלך תלויה ביעדים של
webgpu_cpp
,webgpu_dawn
,glfw
ו-webgpu_glfw
כדי שניתן יהיה להשתמש בהם בקובץmain.cpp
מאוחר יותר.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
פתיחת חלון
עכשיו כשעלות השחר זמינה, אפשר להשתמש ב-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();
}
אם בונים מחדש את האפליקציה ומפעילים אותה כמו קודם, ייפתח חלון ריק. התקדמות!
אחזור מכשיר 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 היא אסינכרונית בגלל הצורה של ממשק ה-API של JavaScript. ב-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()
כדי להציג את המרקם הבא בלולאת הזמן. אין לזה השפעה גלויה כי עדיין לא מתבצע רינדור.
#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
עכשיו נבחן את השינויים המינימליים שנדרשים להתאמת ה-codebase הקיים כדי לשרטט את המשולש האדום הזה בחלון דפדפן. שוב, האפליקציה מבוססת על Emscripten, כלי להידור של תוכניות C/C++ ל-WebAssembly. הכלי הזה כולל קישורים שמטמיעים את webgpu.h מעל ה-API ל-JavaScript.
עדכון הגדרות CMake
אחרי התקנת Emscripten, יש לעדכן את קובץ ה-build של CMakeLists.txt
באופן הבא.
הקוד המודגש הוא הדבר היחיד שעליך לשנות.
- נעשה שימוש ב-
set_target_properties
כדי להוסיף באופן אוטומטי את סיומת הקובץ "html" לקובץ היעד. במילים אחרות, תפיק את הקובץ "app.html". - האפשרות של הקישור לאפליקציה
USE_WEBGPU
נדרשת כדי להפעיל תמיכה ב-WebGPU ב-Emscripten. בלי גישה, הקובץ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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
עדכון הקוד
ב-Emscripten, יצירת wgpu::surface
דורשת רכיב של בד ציור של HTML. לשם כך, צריך לקרוא ל-instance.CreateSurface()
ולציין את הסלקטור #canvas
כך שיתאים לאלמנט ה-HTML המתאים בבד הציור בדף ה-HTML שנוצר על ידי Emscripten.
במקום להשתמש בלולאת זמן, צריך לקרוא לפונקציה 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.
- תמיכה ראשונית ב-D0I ב-Android וב-iOS.
בינתיים, יש לדווח על בעיות ב-WebGPU עבור Emscripten ובעיות ב-Dawn עם הצעות ושאלות.
מקורות מידע
אפשר לעיין בקוד המקור של האפליקציה הזו.
אם אתם רוצים להתעמק ביצירת אפליקציות תלת-ממדיות מקוריות ב-C++ עם WebGPU, תוכלו לקרוא את המאמרים לימודי WebGPU למסמכי C++ ודוגמאות ל-Dawn Native WebGPU.
אם אתם מתעניינים ב-Rust, תוכלו גם לעיין בספריית הגרפיקה של wgpu שמבוססת על WebGPU. כדאי לצפות בהדגמה של משולש שלום.
אימות חתימות
המאמר הזה נבדק על ידי קורנטין וולז, קאי נינומיה וריצ'ל אנדרו.
תמונה מאת Marc-Olivier Jodoin ב-UnFlood.