إنشاء تطبيق باستخدام WebGPU

François Beaufort
François Beaufort

بالنسبة إلى مطوّري البرامج على الويب، WebGPU هي واجهة برمجة تطبيقات لرسومات الويب توفّر وصولاً موحدًا وسريعًا إلى وحدات معالجة الرسومات. تعرض WebGPU إمكانيات الأجهزة الحديثة وتتيح عمليات العرض والحوسبة على وحدة معالجة الرسومات، تمامًا مثل Direct3D 12 وMetal وVulkan.

رغم الحقيقة، هذه القصة غير مكتملة. وWebGPU هو نتيجة لجهود تعاونية تشمل الشركات الكبرى، مثل Apple وGoogle وIntel وMozilla وMicrosoft. من بينها، أدرك البعض أنّ WebGPU يمكن أن تكون أكثر من مجرد واجهة برمجة تطبيقات JavaScript، ولكنها واجهة برمجة تطبيقات للرسومات من عدة أنظمة أساسية مصمَّمة للمطوّرين في جميع الأنظمة الأساسية وغير الويب.

لتنفيذ حالة الاستخدام الأساسية، تم تقديم واجهة برمجة تطبيقات JavaScript في Chrome 113. ومع ذلك، تم تطوير مشروع آخر مهم إلى جانبه: واجهة برمجة التطبيقات webgpu.h. يسرد ملف العنوان C هذا جميع الإجراءات وهياكل البيانات المتاحة لوحدة معالجة الرسومات WebGPU. وهي تعمل كطبقة تجريد غير متوافقة مع النظام الأساسي للأجهزة، ما يسمح لك بإنشاء تطبيقات خاصة بالنظام الأساسي من خلال توفير واجهة متسقة على الأنظمة الأساسية المختلفة.

في هذا المستند، ستتعلم كيفية كتابة تطبيق C++ صغير باستخدام WebGPU يعمل على الويب وعلى أنظمة أساسية محدَّدة. تنبيه بحرق الأحداث، سيظهر لك المثلث الأحمر نفسه الذي يظهر في نافذة متصفّح ونافذة على سطح المكتب مع إجراء تعديلات بسيطة على قاعدة الرموز.

لقطة شاشة لمثلث أحمر مدعوم من خلال WebGPU في نافذة متصفِّح ونافذة كمبيوتر مكتبي على نظام التشغيل macOS
المثلث نفسه الذي يتم تشغيله بواسطة WebGPU في نافذة متصفح ونافذة على الكمبيوتر المكتبي.

كيف تعمل هذه الميزة؟

للاطّلاع على التطبيق المكتمل، راجِع مستودع تطبيقات WebGPU من عدّة أنظمة أساسية.

التطبيق هو مثال مبسَّط C++ يوضح كيفية استخدام WebGPU لإنشاء تطبيقات سطح المكتب وتطبيقات الويب من قاعدة رموز واحدة. وتستخدم هذه الأنظمة webgpu.h في WebGPU كطبقة مجرّدة للأجهزة غير متوافقة مع النظام الأساسي، وذلك من خلال برنامج تضمين C++ يُسمى webgpu_cpp.h.

على الويب، تم تصميم التطبيق وفقًا لـ Emscripten، الذي يضم عمليات ربط تنفّذ webgpu.h أعلى واجهة برمجة تطبيقات JavaScript. على أنظمة أساسية معيَّنة، مثل macOS أو Windows، يمكن إنشاء هذا المشروع وفقًا لتنفيذ Dawn، وهو تطبيق WebGPU من Chromium على عدّة منصات. من الجدير الإشارة إلى wgpu-Native، وهو تنفيذ Rust لـ webgpu.h، وهو متوفّر أيضًا ولكن لم يتمّ استخدامه في هذا المستند.

بدء

للبدء، تحتاج إلى برنامج تجميع C++ وبرنامج CMake للتعامل مع الإصدارات من عدّة أنظمة أساسية بطريقة عادية. داخل مجلد مخصّص، أنشِئ ملف مصدر main.cpp وملف إصدار 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/" و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_cpp وwebgpu_dawn وwebgpu_glfw لتتمكّن من استخدامها في ملف main.cpp لاحقًا.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)

فتح نافذة

الآن وبعد أن أصبحت Dawn متاحة، يمكنك استخدام GLFW لرسم الأشياء على الشاشة. تتيح لك هذه المكتبة المضمّنة في webgpu_glfw إمكانية كتابة رمز لا يتوافق مع النظام الأساسي لإدارة النوافذ.

لفتح نافذة باسم "نافذة WebGPU" بدرجة دقة 512x512، عليك تعديل ملف main.cpp كما هو موضَّح أدناه. تجدُر الإشارة إلى أنّ واجهة برمجة التطبيقات glfwWindowHint() تُستخدم هنا لطلب عدم إعداد واجهة برمجة تطبيقات معيّنة للرسومات.

#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
نافذة فارغة

الحصول على جهاز وحدة معالجة الرسومات

في JavaScript، تكون navigator.gpu هي نقطة دخولك للوصول إلى وحدة معالجة الرسومات. في C++ ، يجب إنشاء متغيّر wgpu::Instance يدويًا يتم استخدامه للغرض نفسه. لتسهيل العملية، يُرجى الإشارة إلى الرمز instance في أعلى ملف main.cpp والاتصال بـ wgpu::CreateInstance() في main().

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

int main() {
  instance = wgpu::CreateInstance();
  Start();
}

ويكون الوصول إلى وحدة معالجة الرسومات غير متزامن بسبب شكل واجهة برمجة تطبيقات JavaScript. في C++ ، أنشِئ دالة GetDevice() مساعِدة تأخذ وسيطة دالة رد الاتصال وتستدعيها باستخدام wgpu::Device الناتجة.

#include <iostream>
…

void GetDevice(void (*callback)(wgpu::Device)) {
  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);
        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);
            },
            userdata);
      },
      reinterpret_cast<void*>(callback));
}

لتسهيل الوصول إليه، حدِّد متغيّر wgpu::Device في أعلى ملف main.cpp وعدِّل الدالة main() لاستدعاء GetDevice() وعيِّن النتيجة الخاصة بها معاودة الاتصال إلى device قبل طلب Start().

wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetDevice([](wgpu::Device dev) {
    device = dev;
    Start();
  });
}

رسم مثلث

لا تظهر سلسلة التبديل في واجهة برمجة تطبيقات JavaScript لأنّ المتصفّح يأخذها في الاعتبار. في C++ ، يجب عليك إنشاؤه يدويًا. ولتسهيل الأمر عليك مرة أخرى، يمكنك الإشارة إلى متغيّر wgpu::SwapChain في أعلى ملف main.cpp. بعد إنشاء نافذة GLFW في Start() مباشرةً، يمكنك استدعاء دالة wgpu::glfw::CreateSurfaceForWindow() المفيدة لإنشاء wgpu::Surface (مشابهة للوحة HTML) واستخدمها لإعداد سلسلة التبديل من خلال استدعاء دالة المساعد الجديدة SetupSwapChain() في InitGraphics(). عليك أيضًا طلب الرمز swapChain.Present() لتقديم الزخرفة التالية في حلقة التكرار while. ليس لهذا الإجراء أي تأثير مرئي لأنّه لم يتم عرض أيّ عرض حتى الآن.

#include <webgpu/webgpu_glfw.h>
…

wgpu::SwapChain swapChain;

void SetupSwapChain(wgpu::Surface surface) {
  wgpu::SwapChainDescriptor scDesc{
      .usage = wgpu::TextureUsage::RenderAttachment,
      .format = wgpu::TextureFormat::BGRA8Unorm,
      .width = kWidth,
      .height = kHeight,
      .presentMode = wgpu::PresentMode::Fifo};
  swapChain = device.CreateSwapChain(surface, &scDesc);
}

void InitGraphics(wgpu::Surface surface) {
  SetupSwapChain(surface);
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  …
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics(surface);

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.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 = wgpu::TextureFormat::BGRA8Unorm};

  wgpu::FragmentState fragmentState{.module = shaderModule,
                                    .targetCount = 1,
                                    .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{
      .vertex = {.module = shaderModule},
      .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics(wgpu::Surface surface) {
  …
  CreateRenderPipeline();
}

وأخيرًا، أرسِل أوامر العرض إلى وحدة معالجة الرسومات في الدالة Render() التي تسمى كل إطار.

void Render() {
  wgpu::RenderPassColorAttachment attachment{
      .view = swapChain.GetCurrentTextureView(),
      .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 أعلى واجهة برمجة تطبيقات JavaScript.

تعديل إعدادات CMake

بعد تثبيت Emscripten، يمكنك تعديل ملف إصدار 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 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};
  wgpu::Surface surface = instance.CreateSurface(&surfaceDesc);
#else
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics(surface);

#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.Present();
    instance.ProcessEvents();
  }
#endif
}

إنشاء التطبيق باستخدام Emscripten

لا يحتاج التغيير الوحيد لإنشاء التطبيق باستخدام Emscripten إلى إضافة أوامر cmake إلى نص واجهة magical 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
لقطة شاشة لمثلث أحمر في نافذة متصفح
مثلث أحمر في نافذة متصفح.

الخطوات التالية

في ما يلي ما يمكن أن تتوقعه في المستقبل:

  • تحسينات في تثبيت واجهات برمجة التطبيقات webgpu.h وwebgpu_cpp.h.
  • الدعم الأوّلي لنظامي التشغيل Android وiOS

إلى ذلك الحين، يُرجى إرسال اقتراحات واستفسارات بشأن مشاكل WebGPU التي تخص Emscripten وDawn.

المراجع

يمكنك استكشاف رمز المصدر لهذا التطبيق.

إذا كنت ترغب في التعمق أكثر في إنشاء تطبيقات ثلاثية الأبعاد أصلية في C++ من الصفر باستخدام WebGPU، يمكنك الاطلاع على التعرُّف على وثائق WebGPU لوثائق C++ وأمثلة Dawn Native WebGPU.

إذا كنت مهتمًا ببرامج Rust، يمكنك أيضًا استكشاف مكتبة رسومات wgpu بناءً على WebGPU. ألقِ نظرة على العرض التوضيحي مرحبًا-triangle.

خدمات الإقرار

راجعت هذه المقالة كورنتين واليز وكاي نينوميا وراشيل أندرو.

تصوير مارك أوليفير جودين على UnLaunch.