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

François Beaufort
François Beaufort

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

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

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

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

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

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

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

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

على الويب، يتم إنشاء التطبيق باستخدام Emscripten، الذي يتضمّن عمليات ربط تُنفِّذ webgpu.h على واجهة برمجة التطبيقات JavaScript API. على أنظمة تشغيل معيّنة، مثل 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

لرسم المثلّث، يمكنك الاستفادة من Dawn، وهي عملية تنفيذ WebGPU في Chromium من عدّة منصات. ويشمل ذلك مكتبة GLFW C++ للرسم على الشاشة. إحدى طرق تنزيل Dawn هي إضافته كـ وحدة فرعية في git إلى مستودعك. تجلب الأوامر التالية الملف في مجلد فرعي باسم "الفجر/".

$ 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)

فتح نافذة

الآن بعد أن أصبح الفجر متاحًا، استخدم 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 API. في 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() لعرض النسيج التالي في حلقة 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};
  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();
}

أخيرًا، أرسِل أوامر المعالجة إلى وحدة معالجة الرسومات في دالة 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 على واجهة برمجة التطبيقات JavaScript API.

تعديل إعدادات 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 dawn::webgpu_dawn glfw 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};
  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 هو إضافة نص برمجي شل emcmake السحري قبل أوامر cmake. هذه المرة، أنشِئ التطبيق في مجلد فرعي باسم "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
  • توفّر الإصدار الأول من Dawn لنظامَي التشغيل Android وiOS

في الوقت الحالي، يُرجى الإبلاغ عن مشاكل WebGPU for Emscripten وDawn مع تقديم اقتراحات وأسئلة.

الموارد

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

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

إذا كنت مهتمًا باستخدام Rust، يمكنك أيضًا استكشاف مكتبة الرسومات wgpu المستندة إلى WebGPU. ألقِ نظرة على العرض التوضيحي لميزة مثلث hello.

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

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

الصورة مقدمة من Marc-Olivier Jodoin على Unsplash.