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

François Beaufort
François Beaufort

تاريخ النشر: 20 يوليو 2023، تاريخ آخر تعديل: 27 أغسطس 2025

بالنسبة إلى مطوّري الويب، فإنّ WebGPU هي واجهة برمجة تطبيقات لرسومات الويب توفّر وصولاً موحّدًا وسريعًا إلى وحدات معالجة الرسومات. تتيح WebGPU إمكانات الأجهزة الحديثة وتسمح بتنفيذ عمليات العرض والحوسبة على وحدة معالجة الرسومات (GPU)، على غرار 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.

على الويب، تم إنشاء التطبيق باستخدام emdawnwebgpu (Emscripten Dawn WebGPU)، الذي يتضمّن روابط تنفّذ 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.22) # 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

يتم تشغيل التطبيق ولكن لا يوجد أي ناتج حتى الآن، لأنّك تحتاج إلى طريقة لرسم العناصر على الشاشة.

Get 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.
  • يجمع الخيار CMake DAWN_BUILD_MONOLITHIC_LIBRARY جميع مكونات Dawn في مكتبة واحدة.
  • يتم تضمين المجلد الفرعي dawn/ في الهدف.
  • سيعتمد تطبيقك على استهدافات webgpu_dawn وwebgpu_glfw وglfw لتتمكّن من استخدامها في ملف main.cpp لاحقًا.

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw 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() داخل Init().

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

عرِّف المتغيرَين wgpu::Adapter وwgpu::Device في أعلى ملف main.cpp. عدِّل الدالة Init() لاستدعاء instance.RequestAdapter() وتعيين دالة ردّ الاتصال الخاصة بالنتيجة إلى adapter، ثم استدعِ adapter.RequestDevice() وعيِّن دالة ردّ الاتصال الخاصة بالنتيجة إلى device.

#include <iostream>

#include <dawn/webgpu_cpp_print.h>


wgpu::Adapter adapter;
wgpu::Device device;


void Init() {
  

  wgpu::Future f1 = instance.RequestAdapter(
      nullptr, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
         wgpu::StringView message) {
        if (status != wgpu::RequestAdapterStatus::Success) {
          std::cout << "RequestAdapter: " << message << "\n";
          exit(0);
        }
        adapter = std::move(a);
      });
  instance.WaitAny(f1, UINT64_MAX);

  wgpu::DeviceDescriptor desc{};
  desc.SetUncapturedErrorCallback([](const wgpu::Device&,
                                     wgpu::ErrorType errorType,
                                     wgpu::StringView message) {
    std::cout << "Error: " << errorType << " - message: " << message << "\n";
  });

  wgpu::Future f2 = adapter.RequestDevice(
      &desc, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestDeviceStatus status, wgpu::Device d,
         wgpu::StringView message) {
        if (status != wgpu::RequestDeviceStatus::Success) {
          std::cout << "RequestDevice: " << message << "\n";
          exit(0);
        }
        device = std::move(d);
      });
  instance.WaitAny(f2, UINT64_MAX);
}

رسم مثلث

لا يتم عرض سلسلة التبديل في 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,
                                    .presentMode = wgpu::PresentMode::Fifo};
  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::ShaderSourceWGSL wgsl{{.code = shaderCode}};

  wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl};
  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

لنلقِ نظرة الآن على الحد الأدنى من التغييرات المطلوبة لتعديل قاعدة الرموز الحالية لرسم هذا المثلث الأحمر في نافذة المتصفح. مرة أخرى، تم إنشاء التطبيق باستخدام emdawnwebgpu (Emscripten Dawn WebGPU)، الذي يتضمّن روابط تنفّذ webgpu.h فوق واجهة JavaScript API. يستخدم هذا الإصدار Emscripten، وهي أداة لتجميع برامج C/C++ في WebAssembly.

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

بعد تثبيت Emscripten، عدِّل ملف CMakeLists.txt على النحو التالي. الرمز المميّز هو الشيء الوحيد الذي عليك تغييره.

  • يُستخدَم set_target_properties لإضافة امتداد الملف "html" تلقائيًا إلى الملف المستهدف. بعبارة أخرى، سيتم إنشاء ملف "app.html".
  • تتيح مكتبة روابط الاستهداف emdawnwebgpu_cpp إمكانية استخدام WebGPU في Emscripten. وبدونها، لا يمكن لملف main.cpp الوصول إلى ملف webgpu/webgpu_cpp.h.
  • يتيح ASYNCIFY=1 خيار رابط التطبيق تفاعُل رمز C++ المتزامن مع JavaScript غير المتزامن.
  • يطلب خيار رابط التطبيق USE_GLFW=3 من Emscripten استخدام تنفيذ JavaScript المضمّن لواجهة برمجة التطبيقات GLFW 3.
cmake_minimum_required(VERSION 3.22) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
  target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
  target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)
endif()

تعديل الرمز

بدلاً من استخدام حلقة while، استدعِ الدالة emscripten_set_main_loop(Render) للتأكّد من استدعاء الدالة Render() بمعدّل سلس مناسب يتوافق بشكل صحيح مع المتصفّح والشاشة.

#include <iostream>

#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
  
#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
  • توفير الدعم الأولي لـ Dawn على أجهزة Android وiOS

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

الموارد

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

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

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

كلمة شكر

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

صورة من مارك-أوليفر جودوان على Unsplash