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

François Beaufort
François Beaufort

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

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

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

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

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

איך זה עובד?

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

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

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

שנתחיל?

כדי להתחיל, אתם צריכים מהדר (compiler) C++ ו-CMake כדי לטפל בשיטות build בפלטפורמות שונות באופן סטנדרטי. בתוך תיקייה ייעודית, יוצרים קובץ מקור main.cpp וקובץ build מסוג CMakeLists.txt.

בשלב הזה, הקובץ main.cpp צריך להכיל פונקציית main() ריקה.

int main() {}

הקובץ CMakeLists.txt מכיל מידע בסיסי על הפרויקט. בשורה האחרונה מצוין ששם ההפעלה הוא 'אפליקציה' וקוד המקור שלו הוא 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. כולל ספריית GLFW ל-C++ לציור במסך. אחת מהדרכים להוריד את Dawn היא להוסיף אותו כמודול משנה של git למאגר שלכם. הפקודות הבאות מאחזרות אותו ב-'dawn/' .

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

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

  • האפשרות CMake DAWN_FETCH_DEPENDENCIES מאחזרת את כל יחסי התלות של שחר.
  • תיקיית המשנה 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 ברזולוציה של 512x512, מעדכנים את הקובץ main.cpp כמו שמתואר בהמשך. חשוב לשים לב ש-glfwWindowHint() משמש כאן כדי לבקש לא אתחול מסוים של Graphics 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() בתוך 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() כדי להציג את המרקם הבא בלולאת ה-time. אין לכך השפעה גלויה כי עדיין לא מתבצע רינדור.

#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 והפעלה שלה תציג כעת את המשולש האדום שכולם חיכו לו בחלון! הגיע הזמן לצאת להפסקה

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

הידור ל-WebAssembly

בואו נבחן עכשיו את השינויים המינימליים שנדרשים כדי להתאים את בסיס הקוד הקיים כדי לשרטט את המשולש האדום הזה בחלון דפדפן. כמו כן, האפליקציה מבוססת על 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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()

עדכון הקוד

ב-Emscripten, כדי ליצור wgpu::surface נדרש רכיב בד ציור של HTML. לשם כך, צריך לקרוא לפונקציה instance.CreateSurface() ולציין את הבורר #canvas כדי שיתאים לרכיב ה-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.
  • תמיכה ראשונית בין זריחה ב-Android וב-iOS.

בינתיים, אפשר לשלוח בעיות ב-WebGPU עבור Emscripten ובעיות ב-Dawn עם הצעות ושאלות.

משאבים

אפשר להיעזר בקוד המקור של האפליקציה הזו.

אם אתם רוצים להתעמק יותר ביצירה של אפליקציות תלת-ממדיות מקוריות ב-C++ באמצעות WebGPU, תוכלו לקרוא את המסמכים בנושא WebGPU ל-C++ ואת דוגמאות של Native WebGPU במקור (Dawn Native WebGPU).

אם אתם מתעניינים ב-Rust, תוכלו גם לעיין בספריית הגרפיקה של wgpu שמבוססת על WebGPU. כדאי לצפות בהדגמה של משולש הלוך-שלום.

אימות חתימות

המאמר הזה נבדק על ידי קורנטין ואלז, קאי נינומיה ורייצ'ל אנדרו.

תמונה מאת Marc-Olivier Jodoin ב-Un משתתפים.