بالنسبة إلى مطوّري الويب، WebGPU هي واجهة برمجة تطبيقات لرسومات الويب توفّر إمكانية موحّدة وسريعة للوصول إلى وحدات معالجة الرسومات. وتعرض WebGPU قدرات الأجهزة الحديثة وتسمح بالعرض والعمليات الحسابية على وحدة معالجة الرسومات (GPU)، وذلك على غرار برامج Direct3D 12 وMetal وVulkan.
على الرغم من أنّ هذه القصة صحيحة، إلا أنّها غير مكتملة. وهي نتيجة جهود تعاونية تشمل الشركات الكبرى مثل Apple وGoogle وIntel وMozilla وMicrosoft. ومن بين هؤلاء، أدرك بعضهم أنّ WebGPU يمكن أن يكون أكثر من واجهة برمجة تطبيقات JavaScript، بل واجهة برمجة تطبيقات رسومات لجميع المنصات للمطوّرين في جميع الأنظمة المتكاملة، بخلاف الويب.
لتحقيق حالة الاستخدام الأساسية، تم إدخال واجهة برمجة تطبيقات JavaScript في الإصدار 113 من Chrome. ومع ذلك، تم تطوير مشروع آخر مهم إلى جانب واجهة برمجة التطبيقات: webgpu.h C API. يسرد ملف الرأس هذا بتنسيق C جميع الإجراءات وبنى البيانات المتاحة لـ WebGPU. وهي بمثابة طبقة تجريد للأجهزة غير المرتبطة بالنظام الأساسي، وتسمح لك بإنشاء تطبيقات خاصة بالنظام الأساسي من خلال توفير واجهة متسقة عبر الأنظمة الأساسية المختلفة.
في هذا المستند، ستتعرّف على كيفية كتابة تطبيق صغير بلغة C++ باستخدام WebGPU يعمل على الويب ومنصّات معيّنة. إليك تلميح: سيظهر لك المثلث الأحمر نفسه الذي يظهر في نافذة المتصفّح ونافذة الكمبيوتر المكتبي مع إجراء تعديلات طفيفة على قاعدة بياناتك.
كيف تعمل هذه الميزة؟
للاطّلاع على التطبيق المكتمل، يمكنك الاطّلاع على مستودع تطبيق WebGPU المتوافق مع جميع الأنظمة الأساسية.
التطبيق هو مثال بسيط على لغة C++ يعرض كيفية استخدام WebGPU لإنشاء تطبيقات سطح المكتب والويب من قاعدة رموز برمجية واحدة. وفي التفاصيل، فهي تستخدم webgpu.h في WebGPU كطبقة تجريد للأجهزة غير المرتبطة بالنظام الأساسي من خلال برنامج تضمين 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 إلى مستودعك. تجلب الأوامر التالية الملف في مجلد فرعي باسم "الفجر/".
$ 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)
فتح نافذة
الآن بعد أن أصبح الفجر متاحًا، استخدم 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 for Emscripten وDawn مع تقديم اقتراحات وأسئلة.
الموارد
يمكنك استكشاف رمز المصدر لهذا التطبيق.
وإذا كنت تريد التعمق أكثر في إنشاء تطبيقات ثلاثية الأبعاد أصلية في C++ من البداية باستخدام WebGPU، يمكنك الاطّلاع على مستندات WebGPU for C++ وأمثلة على Degn Native WebGPU.
إذا كنت مهتمًا باستخدام Rust، يمكنك أيضًا استكشاف مكتبة الرسومات wgpu المستندة إلى WebGPU. ألقِ نظرة على العرض التوضيحي لميزة مثلث hello.
خدمات الإقرار
راجع هذه المقالة كلّ من كورينتين واليز وكاي نينومييا وراشيل أندرو.
الصورة مقدمة من Marc-Olivier Jodoin على Unsplash.