برای توسعه دهندگان وب، WebGPU یک API گرافیکی وب است که دسترسی یکپارچه و سریع به GPU ها را فراهم می کند. WebGPU قابلیتهای سختافزاری مدرن را به نمایش میگذارد و امکان پردازش و عملیات محاسباتی روی یک GPU، مشابه Direct3D 12، Metal و Vulkan را میدهد.
در حالی که درست است، آن داستان ناقص است. WebGPU نتیجه یک تلاش مشترک از جمله شرکت های بزرگی مانند اپل، گوگل، اینتل، موزیلا و مایکروسافت است. در میان آنها، برخی متوجه شدند که WebGPU می تواند چیزی بیش از یک API جاوا اسکریپت باشد، اما یک API گرافیکی بین پلتفرمی برای توسعه دهندگان در سراسر اکوسیستم ها، به غیر از وب، باشد.
برای انجام موارد استفاده اولیه، یک JavaScript API در Chrome 113 معرفی شد . با این حال، پروژه مهم دیگری در کنار آن توسعه یافته است: webgpu.h C API. این فایل هدر C تمام رویه ها و ساختارهای داده موجود WebGPU را فهرست می کند. این به عنوان یک لایه انتزاعی سخت افزاری مبتنی بر پلتفرم عمل می کند و به شما این امکان را می دهد که برنامه های کاربردی مخصوص پلتفرم را با ارائه یک رابط سازگار در بین پلتفرم های مختلف بسازید.
در این سند، نحوه نوشتن یک برنامه کوچک C++ با استفاده از WebGPU را یاد خواهید گرفت که هم بر روی وب و هم در پلتفرم های خاص اجرا می شود. هشدار اسپویلر، شما همان مثلث قرمزی را دریافت خواهید کرد که در پنجره مرورگر و پنجره دسکتاپ با حداقل تنظیمات در پایگاه کد شما ظاهر می شود.
چگونه کار می کند؟
برای دیدن برنامه تکمیل شده، مخزن برنامه چند پلتفرمی WebGPU را بررسی کنید.
این برنامه یک مثال C++ مینیمالیستی است که نحوه استفاده از WebGPU برای ساخت برنامه های دسکتاپ و وب از یک پایگاه کد واحد را نشان می دهد. در زیر هود، از webgpu.h WebGPU به عنوان یک لایه انتزاعی سخت افزاری مبتنی بر پلتفرم از طریق یک بسته بندی C++ به نام webgpu_cpp.h استفاده می کند.
در وب، برنامه بر اساس Emscripten ساخته شده است که دارای پیوندهایی است که webgpu.h را در بالای 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 ، پیادهسازی WebGPU بین پلتفرمی Chromium استفاده کنید. این شامل کتابخانه GLFW C++ برای طراحی روی صفحه است. یکی از راه های دانلود Dawn این است که آن را به عنوان یک زیر ماژول git به مخزن خود اضافه کنید. دستورات زیر آن را در یک پوشه فرعی "dawn/" واکشی می کنند.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
سپس به صورت زیر به فایل CMakeLists.txt
اضافه کنید:
- گزینه CMake
DAWN_FETCH_DEPENDENCIES
همه وابستگی های 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()
در اینجا برای درخواست هیچ مقدار اولیه API گرافیکی خاصی استفاده می شود.
#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();
}
بازسازی برنامه و اجرای آن مانند قبل در حال حاضر منجر به یک پنجره خالی می شود. شما در حال پیشرفت هستید!
دستگاه GPU را دریافت کنید
در جاوا اسکریپت، navigator.gpu
نقطه ورود شما برای دسترسی به GPU است. در C++، باید به صورت دستی یک متغیر wgpu::Instance
ایجاد کنید که برای همین منظور استفاده می شود. برای راحتی، instance
در بالای فایل main.cpp
اعلام کنید و wgpu::CreateInstance()
در main()
فراخوانی کنید.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
دسترسی به GPU به دلیل شکل جاوا اسکریپت API ناهمزمان است. در C++، دو تابع کمکی به نامهای GetAdapter()
و GetDevice()
ایجاد کنید که به ترتیب تابع callback را با 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()
فراخوانی کنید و قبل از فراخوانی Start()
نتیجه را به device
اختصاص دهید.
wgpu::Adapter adapter;
wgpu::Device device;
…
int main() {
instance = wgpu::CreateInstance();
GetAdapter([](wgpu::Adapter a) {
adapter = a;
GetDevice([](wgpu::Device d) {
device = d;
Start();
});
});
}
یک مثلث بکشید
زنجیره swap در JavaScript API نمایش داده نمی شود زیرا مرورگر از آن مراقبت می کند. در C++، باید آن را به صورت دستی ایجاد کنید. یک بار دیگر، برای راحتی، یک متغیر wgpu::Surface
در بالای فایل main.cpp
اعلام کنید. درست پس از ایجاد پنجره GLFW در Start()
، تابع wgpu::glfw::CreateSurfaceForWindow()
را فراخوانی کنید تا یک wgpu::Surface
(مشابه بوم HTML) ایجاد کنید و با فراخوانی تابع کمکی جدید ConfigureSurface()
آن را پیکربندی کنید. در InitGraphics()
. همچنین برای ارائه بافت بعدی در حلقه while باید surface.Present()
را فراخوانی کنید. این هیچ اثر قابل مشاهده ای ندارد زیرا هنوز هیچ رندری اتفاق نمی افتد.
#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()
به نام هر فریم به GPU ارسال کنید.
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 را در بالای API جاوا اسکریپت پیاده سازی می کند.
تنظیمات CMake را به روز کنید
پس از نصب Emscripten، فایل ساخت CMakeLists.txt
را به صورت زیر به روز کنید. کد هایلایت شده تنها چیزی است که باید تغییر دهید.
-
set_target_properties
برای افزودن خودکار پسوند فایل "html" به فایل مورد نظر استفاده می شود. به عبارت دیگر، شما یک فایل "app.html" تولید خواهید کرد. - برای فعال کردن پشتیبانی WebGPU در Emscripten، گزینه پیوند برنامه
USE_WEBGPU
مورد نیاز است. بدون آن، فایل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
را مشخص کنید تا با عنصر 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
shell اضافه کنید. این بار، برنامه را در یک زیر پوشه 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
بعدش چی
در اینجا چیزی است که می توانید در آینده انتظار داشته باشید:
- بهبود در تثبیت APIهای webgpu.h و webgpu_cpp.h.
- پشتیبانی اولیه Dawn برای اندروید و iOS.
در ضمن، لطفاً مشکلات WebGPU را برای مسائل Emscripten و Dawn با پیشنهادات و سؤالات ارسال کنید.
منابع
به راحتی می توانید کد منبع این برنامه را بررسی کنید.
اگر میخواهید بیشتر در ایجاد برنامههای سهبعدی بومی در C++ از ابتدا با WebGPU غوطهور شوید، Learn WebGPU را برای اسناد C++ و Dawn Native WebGPU Examps بررسی کنید.
اگر به Rust علاقه مند هستید، می توانید کتابخانه گرافیکی wgpu مبتنی بر WebGPU را نیز بررسی کنید. به دموی hello-triangle آنها نگاهی بیندازید.
قدردانی ها
این مقاله توسط کورنتین والز ، کای نینومیا و ریچل اندرو بررسی شده است.
عکس از Marc-Olivier Jodoin در Unsplash .