بالنسبة إلى مطوّري الويب، 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.
على الويب، يتم إنشاء التطبيق باستخدام 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();
}
يؤدي الآن إعادة إنشاء التطبيق وتشغيله كما كان من قبل إلى ظهور نافذة فارغة. لقد أحرزت تقدمًا جيدًا.
الحصول على جهاز وحدة معالجة الرسومات
في 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 وتشغيله إلى ظهور المثلث الأحمر الذي طال انتظاره في نافذة. حان وقت الاستراحة، أنت تستحق ذلك.
التحويل إلى 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.