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

François Beaufort
François Beaufort

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

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

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

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

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

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

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

التطبيق هو مثال بسيط على لغة C++ يعرض كيفية استخدام WebGPU لإنشاء تطبيقات سطح المكتب والويب من قاعدة رموز برمجية واحدة. في الخلفية، يستخدم WebGPU webgpu.h كطبقة تجريدية للأجهزة لا تعتمد على النظام الأساسي من خلال حزمة 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 إلى مستودعك. تُسترجع الأوامر التالية هذا الملف في مجلد فرعي باسم "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() هنا لطلب عدم بدء أي واجهة برمجة تطبيقات رسومات معيّنة.

#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 في Emscripten ومشاكل Dawn مع تقديم اقتراحات والأسئلة.

الموارد

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

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

إذا كنت مهتمًا باستخدام Rust، يمكنك أيضًا استكشاف مكتبة الرسومات wgpu المستندة إلى WebGPU. يمكنك الاطّلاع على العرض التوضيحي hello-triangle.

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

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

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