إنشاء تطبيق باستخدام 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 هذا جميع الإجراءات وهياكل البيانات المتاحة 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، وهو تنفيذ صدئ لموقع 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" لإنشاء ملفات إصدار في "بناء/" و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 إلى مستودعك. تعمل الأوامر التالية على استرجاعها في علامة "الفجر/" الفرعي.

$ 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" بدقة 512×512، يمكنك تعديل ملف 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++ ، أنشئ دالتَين مساعدتَين باسم 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 حيث يعتني بها المتصفّح. في 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.

تعديل إعدادات 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 بلغة 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 هو إضافة أوامر 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
لقطة شاشة لمثلث أحمر في نافذة متصفّح
مثلث أحمر في نافذة متصفح.

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

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

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

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

الموارد

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

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

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

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

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

تصوير مارك أوليفييه جودوين على موقع Unسباش