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

François Beaufort
François Beaufort

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

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

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

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

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

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

איך זה עובד?

כדי לראות את האפליקציה המלאה, אפשר לעיין במאגר WebGPU cross-platform app.

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

באינטרנט, האפליקציה מבוססת על emdawnwebgpu (Emscripten Dawn WebGPU), שיש לה קשירות שמטמיעות את 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.22) # 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

האפליקציה פועלת אבל עדיין אין פלט, כי צריך דרך לצייר דברים על המסך.

Get Dawn

כדי לצייר את המשולש, אפשר להשתמש ב-Dawn, הטמעה של WebGPU חוצה פלטפורמות ב-Chromium. היא כוללת את ספריית C++‎‏ GLFW לשרטוט על המסך. אחת הדרכים להוריד את Dawn היא להוסיף אותו כמודול משנה של git למאגר. הפקודות הבאות מאחזרות אותו בתיקיית משנה dawn/.

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

לאחר מכן, מוסיפים לקובץ CMakeLists.txt את הטקסט הבא:

  • האפשרות DAWN_FETCH_DEPENDENCIES של CMake מאחזרת את כל התלות של Dawn.
  • האפשרות DAWN_BUILD_MONOLITHIC_LIBRARY ב-CMake מאגדת את כל רכיבי Dawn לספרייה אחת.
  • תיקיית המשנה dawn/ כלולה ביעד.
  • האפליקציה תהיה תלויה ביעדים webgpu_dawn, webgpu_glfw ו-glfw, כדי שתוכלו להשתמש בהם בקובץ main.cpp בהמשך.

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)

פתיחת חלון

עכשיו, כש-Dawn זמין, אפשר להשתמש ב-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();
}

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

צילום מסך של חלון ריק ב-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() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  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, מעדכנים את CMakeLists.txt קובץ ה-build באופן הבא. הקוד המודגש הוא הדבר היחיד שצריך לשנות.

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

עדכון הקוד

במקום להשתמש בלולאת 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 את סקריפט ה-Shell 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.
  • תמיכה ראשונית ב-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.