בניית אפליקציה עם WebGPU

François Beaufort
François Beaufort

תאריך פרסום: 20 ביולי 2023, תאריך עדכון אחרון: 17 ביוני 2025

למפתחי אינטרנט, WebGPU הוא ממשק API של גרפיקה באינטרנט שמספק גישה מאוחדת ומהירה ל-GPU. WebGPU חושף יכולות חומרה מודרניות ומאפשר פעולות עיבוד ועיבוד גרפי ב-GPU, בדומה ל-Direct3D 12, ל-Metal ול-Vulkan.

זה נכון, אבל הסיפור הזה לא מלא. WebGPU הוא תוצאה של מאמץ משותף של חברות גדולות כמו Apple,‏ Google,‏ Intel,‏ Mozilla ו-Microsoft. חלק מהם הבינו ש-WebGPU יכול להיות יותר מ-API של JavaScript, אלא API גרפי בפלטפורמות שונות למפתחים בסביבות חיים שונות, מלבד האינטרנט.

כדי לעמוד בדרישות של תרחיש לדוגמה הראשי, הוסף ממשק API של JavaScript בגרסה 113 של Chrome. עם זאת, במקביל לפרויקט הזה פותח פרויקט משמעותי נוסף: ה-API של C‏ webgpu.h. קובץ הכותרת הזה ב-C מפרט את כל התהליכים ומבני הנתונים הזמינים של WebGPU. הוא משמש כשכבת הפשטה של חומרה ללא תלות בפלטפורמה, ומאפשר ליצור אפליקציות ספציפיות לפלטפורמה על ידי מתן ממשק עקבי בפלטפורמות שונות.

במסמך הזה תלמדו איך לכתוב אפליקציה קטנה ב-C++ באמצעות WebGPU שפועלת גם באינטרנט וגם בפלטפורמות ספציפיות. ספוילר: יופיע אותו משולש אדום שמופיע בחלון הדפדפן ובחלון במחשב, עם שינויים מינימליים בקוד.

צילום מסך של משולש אדום שמופעל על ידי WebGPU בחלון דפדפן ובחלון במחשב ב-MacOS.
אותו משולש שמופעל על ידי WebGPU בחלון דפדפן ובחלון במחשב.

איך זה עובד?

כדי לראות את האפליקציה המושלמת, אפשר להיכנס למאגר של אפליקציה מבוססת-WebGPU לפלטפורמות שונות.

האפליקציה היא דוגמה מינימליסטית ב-C++ שמראה איך להשתמש ב-WebGPU כדי ליצור אפליקציות למחשב ולאינטרנט מקוד בסיס יחיד. מתחת לפני השטח, הוא משתמש ב-webgpu.h של WebGPU כשכבת הפשטה של חומרה שאינה תלויה בפלטפורמה, באמצעות מעטפת C++‏ שנקראת webgpu_cpp.h.

באינטרנט, האפליקציה מבוססת על emdawnwebgpu (Emscripten Dawn WebGPU), עם קישורים שמטמיעים את webgpu.h מעל JavaScript API. בפלטפורמות ספציפיות כמו macOS או Windows, אפשר לבנות את הפרויקט עם Dawn, ההטמעה של WebGPU בפלטפורמות שונות של Chromium. כדאי לציין שקיימת גם wgpu-native, הטמעה של webgpu.h ב-Rust, אבל לא נעשה בה שימוש במסמך הזה.

שנתחיל?

כדי להתחיל, צריך קומפילטור של 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)

פתיחת חלון

עכשיו, כש-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();
}

בניית האפליקציה מחדש והפעלתה כמו קודם גורמות עכשיו לחלון ריק. איזו התקדמות!

צילום מסך של חלון ריק ב-MacOS.
חלון ריק.

אחזור מכשיר GPU

ב-JavaScript, navigator.gpu היא נקודת הכניסה לגישה ל-GPU. ב-C++‎, צריך ליצור באופן ידני משתנה wgpu::Instance שמשמש לאותו מטרה. כדי להקל על העבודה, מגדירים את instance בחלק העליון של הקובץ main.cpp ומפעילים את wgpu::CreateInstance() בתוך Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  wgpu::InstanceDescriptor instanceDesc{
      .capabilities = {.timedWaitAnyEnable = true}};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

מגדירים שני משתנים, wgpu::Adapter ו-wgpu::Device, בחלק העליון של הקובץ main.cpp. מעדכנים את הפונקציה Init() כך שתפעיל את instance.RequestAdapter() ותקצה את פונקציית הקריאה החוזרת עם התוצאה שלה ל-adapter, ואז קוראים ל-adapter.RequestDevice() ותוקצים את פונקציית הקריאה החוזרת עם התוצאה שלה ל-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);
}

ציור משולש

שרשרת ההחלפה לא מוצגת ב-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,
                                    .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();
  }
}

עכשיו זה הזמן ליצור את צינור עיבוד התמונות באמצעות הקוד שבהמשך. כדי לגשת אליה בקלות, מגדירים משתנה 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::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();
}

לבסוף, שולחים פקודות רינדור ל-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, מופיע בחלון המשולש האדום הצפוי. הגיע הזמן לצאת להפסקה.

צילום מסך של משולש אדום בחלון של macOS.
משולש אדום בחלון במחשב.

הידור ל-WebAssembly

עכשיו נבחן את השינויים המינימליים הנדרשים כדי לשנות את קוד הבסיס הקיים כך שיצייר את המשולש האדום הזה בחלון הדפדפן. שוב, האפליקציה נוצרה באמצעות emdawnwebgpu (Emscripten Dawn WebGPU), שיש לו קישורים שמטמיעים את webgpu.h מעל JavaScript API. הוא משתמש ב-Emscripten, כלי לעיבוד תוכניות C/C++ ל-WebAssembly.

עדכון ההגדרות של CMake

אחרי שמתקינים את Emscripten, מעדכנים את קובץ ה-build CMakeLists.txt באופן הבא. הקוד המודגש הוא היחיד שצריך לשנות.

  • השדה set_target_properties משמש להוספה אוטומטית של סיומת הקובץ 'html' לקובץ היעד. במילים אחרות, ייווצר קובץ app.html.
  • ספריית הקישור של היעד emdawnwebgpu_cpp מאפשרת תמיכה ב-WebGPU ב-Emscripten. בלי זה, לא תהיה לקובץ main.cpp גישה לקובץ webgpu/webgpu_cpp.h.
  • אפשרות הקישור לאפליקציה ASYNCIFY=1 מאפשרת לקוד C++‎ סינכרוני לקיים אינטראקציה עם קוד JavaScript לא סנכרוני.
  • האפשרות USE_GLFW=3 של קישור האפליקציה מורה ל-Emscripten להשתמש בהטמעה המובנית של JavaScript לממשק GLFW 3 API.
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")

set(DAWN_FETCH_DEPENDENCIES ON)
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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()

עדכון הקוד

במקום להשתמש בלול while, צריך לבצע קריאה ל-emscripten_set_main_loop(Render) כדי לוודא שפונקציית Render() נקראת בקצב חלק ומתואמת עם הדפדפן והצג.

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

פיתוח האפליקציה באמצעות 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.

תודות

הבדיקה של המאמר בוצעה על ידי קורנטין וואלז (Corentin Wallez), קאי ניינומיה (Kai Ninomiya) ורייצ'ל אנדרו (Rachel Andrew).

צילום: Marc-Olivier Jodoin ב-Unsplash.