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

الحصول على جهاز وحدة معالجة الرسومات
في JavaScript، navigator.gpu
هي نقطة الدخول للوصول إلى وحدة معالجة الرسومات. في C++، عليك إنشاء متغيّر wgpu::Instance
يدويًا يُستخدَم للغرض نفسه. للتيسير، يمكنك الإفصاح عن instance
في أعلى ملف main.cpp
والاتصال بـ wgpu::CreateInstance()
داخل Init()
.
#include <webgpu/webgpu_cpp.h>
…
wgpu::Instance instance;
…
void Init() {
wgpu::InstanceDescriptor instanceDesc{};
instanceDesc.capabilities.timedWaitAnyEnable = true;
instance = wgpu::CreateInstance(&instanceDesc);
}
int main() {
Init();
Start();
}
أدخِل المتغيّرين wgpu::Adapter
وwgpu::Device
في أعلى ملف main.cpp
. عدِّل الدالة Init()
لاستدعاء instance.RequestAdapter()
وتخصيص دالة ردّ الاتصال بالنتيجة لها على adapter
، ثم استخدِم adapter.RequestDevice()
وتخصيص دالة ردّ الاتصال بالنتيجة لها على device
.
#include <dawn/webgpu_cpp_print.h>
#include <iostream>
…
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{};
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 وتشغيله إلى ظهور المثلث الأحمر الذي طال انتظاره في نافذة. حان وقت الاستراحة، أنت تستحق ذلك.

التحويل إلى 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 غير المتزامن.
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")
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES SUFFIX ".html")
target_link_libraries(app PRIVATE emdawnwebgpu_cpp)
target_link_options(app PRIVATE "-sASYNCIFY=1")
else()
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 <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <GLFW/glfw3.h>
#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::EmscriptenSurfaceSourceCanvasHTMLSelector source{};
source.selector = "#canvas";
wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &source};
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.