با WebGPU یک برنامه بسازید

فرانسوا بوفور
François Beaufort

منتشر شده: ۲۰ ژوئیه ۲۰۲۳، آخرین به‌روزرسانی: ۲۲ اکتبر ۲۰۲۵

برای توسعه‌دهندگان وب، WebGPU یک API گرافیکی وب است که دسترسی یکپارچه و سریع به GPUها را فراهم می‌کند. WebGPU قابلیت‌های سخت‌افزاری مدرن را در معرض نمایش قرار می‌دهد و امکان رندر و عملیات محاسباتی را روی یک GPU فراهم می‌کند، مشابه Direct3D 12، Metal و Vulkan.

اگرچه این داستان درست است، اما ناقص است. WebGPU نتیجه یک تلاش مشترک، از جمله شرکت‌های بزرگی مانند اپل، گوگل، اینتل، موزیلا و مایکروسافت است. در میان آنها، برخی متوجه شدند که WebGPU می‌تواند چیزی بیش از یک API جاوا اسکریپت باشد، بلکه یک API گرافیکی چند پلتفرمی برای توسعه‌دهندگان در اکوسیستم‌های مختلف، غیر از وب، باشد.

برای برآورده کردن مورد استفاده اصلی، یک API جاوا اسکریپت در کروم ۱۱۳ معرفی شد . با این حال، پروژه مهم دیگری نیز در کنار آن توسعه داده شده است: webgpu.h C API. این فایل هدر C تمام رویه‌ها و ساختارهای داده موجود WebGPU را فهرست می‌کند. این فایل به عنوان یک لایه انتزاعی سخت‌افزاری مستقل از پلتفرم عمل می‌کند و به شما امکان می‌دهد با ارائه یک رابط کاربری سازگار در پلتفرم‌های مختلف، برنامه‌های مختص پلتفرم بسازید.

در این سند، یاد خواهید گرفت که چگونه یک برنامه کوچک C++ با استفاده از WebGPU بنویسید که هم در وب و هم در پلتفرم‌های خاص اجرا شود. هشدار اسپویل، با حداقل تنظیمات در کدبیس خود، همان مثلث قرمز رنگی را که در پنجره مرورگر و پنجره دسکتاپ ظاهر می‌شود، دریافت خواهید کرد.

تصویری از یک مثلث قرمز که توسط WebGPU در یک پنجره مرورگر و یک پنجره دسکتاپ در macOS پشتیبانی می‌شود.
همان مثلثی که توسط WebGPU در یک پنجره مرورگر و یک پنجره دسکتاپ پشتیبانی می‌شود.

چگونه کار می‌کند؟

برای مشاهده‌ی برنامه‌ی تکمیل‌شده، مخزن برنامه‌های چند پلتفرمی WebGPU را بررسی کنید.

این برنامه یک مثال مینیمالیستی C++ است که نحوه استفاده از WebGPU را برای ساخت برنامه‌های دسکتاپ و وب از یک کدبیس واحد نشان می‌دهد. در باطن، از webgpu.h مربوط به WebGPU به عنوان یک لایه انتزاعی سخت‌افزار مستقل از پلتفرم از طریق یک پوشش C++ به نام webgpu_cpp.h استفاده می‌کند.

در وب، این برنامه بر اساس emdawnwebgpu (Emscripten Dawn WebGPU) ساخته شده است که دارای اتصالاتی است که webgpu.h را بر روی API جاوا اسکریپت پیاده‌سازی می‌کند. در پلتفرم‌های خاص مانند macOS یا ویندوز، این پروژه می‌تواند بر اساس Dawn ، پیاده‌سازی چند پلتفرمی WebGPU کرومیوم، ساخته شود. شایان ذکر است که 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")

برای ایجاد فایل‌های ساخت در زیرپوشه "build/" cmake -B build و برای ساخت واقعی برنامه و تولید فایل اجرایی cmake --build build build را اجرا کنید.

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

برنامه اجرا می‌شود اما هنوز هیچ خروجی وجود ندارد، زیرا به روشی برای رسم چیزها روی صفحه نیاز دارید.

سپیده دم را بگیر

برای رسم مثلث خود، می‌توانید از Dawn ، پیاده‌سازی چند پلتفرمی WebGPU کرومیوم، بهره ببرید. این شامل کتابخانه GLFW C++ برای رسم روی صفحه نمایش است. یک راه برای دانلود Dawn این است که آن را به عنوان یک زیرماژول git به مخزن خود اضافه کنید. دستورات زیر آن را در زیرپوشه "dawn/" دریافت می‌کنند.

$ git init
$ git submodule add https://github.com/google/dawn.git

سپس، به فایل CMakeLists.txt به صورت زیر اضافه کنید:

  • گزینه CMake DAWN_FETCH_DEPENDENCIES تمام وابستگی‌های Dawn را دریافت می‌کند.
  • پوشه‌ی dawn/ sub در پوشه‌ی هدف قرار دارد.
  • برنامه شما به اهداف webgpu_dawn ، webgpu_glfw و glfw وابسته خواهد بود تا بتوانید بعداً از آنها در فایل main.cpp استفاده کنید.

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

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

پنجره‌ای باز کن

حالا که داون در دسترس است، از GLFW برای رسم چیزها روی صفحه استفاده کنید. این کتابخانه که برای راحتی در webgpu_glfw گنجانده شده است، به شما امکان می‌دهد کدی بنویسید که برای مدیریت پنجره مستقل از پلتفرم باشد.

برای باز کردن پنجره‌ای با نام "WebGPU window" با وضوح تصویر ۵۱۲x۵۱۲، فایل 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();
}

حالا که برنامه را دوباره ساخته‌ایم و مثل قبل اجراش کرده‌ایم، یک پنجره خالی نمایش داده می‌شود. شما در حال پیشرفت هستید!

تصویری از یک پنجره خالی macOS.
یک پنجره خالی.

دستگاه GPU را دریافت کنید

در جاوا اسکریپت، navigator.gpu نقطه ورود شما برای دسترسی به 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);
}

یک مثلث رسم کنید

زنجیره‌ی swap در 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::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() که هر فریم را فراخوانی می‌کند، به 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 و اجرای آن، مثلث قرمز مورد انتظار را در یک پنجره ایجاد می‌کند! کمی استراحت کنید - شما لیاقتش را دارید.

تصویر یک مثلث قرمز در پنجره macOS.
یک مثلث قرمز در پنجره دسکتاپ.

کامپایل به WebAssembly

حالا بیایید نگاهی به حداقل تغییرات مورد نیاز برای تنظیم کدبیس موجود شما برای ترسیم این مثلث قرمز در پنجره مرورگر بیندازیم. باز هم، این برنامه بر اساس emdawnwebgpu (Emscripten Dawn WebGPU) ساخته شده است که دارای پیوندهایی است که webgpu.h را بر روی 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++ همگام با جاوااسکریپت ناهمگام را فراهم می‌کند.
  • گزینه‌ی USE_GLFW=3 برای لینک برنامه به Emscripten می‌گوید که از پیاده‌سازی جاوااسکریپت داخلی خود از GLFW 3 API استفاده کند.
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)
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 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
تصویری از یک مثلث قرمز در پنجره مرورگر.
یک مثلث قرمز در پنجره مرورگر.

قدم بعدی چیست؟

با نگاهی به آینده، می‌توانید انتظار پشتیبانی اولیه از Dawn در اندروید و iOS را داشته باشید.

در عین حال، مشکلات WebGPU مربوط به Emscripten و Dawn را به همراه پیشنهادات و سوالات خود ثبت کنید.

منابع

برای بررسی کد منبع این برنامه، می‌توانید به اینجا مراجعه کنید.

اگر می‌خواهید در ایجاد برنامه‌های سه‌بعدی بومی در ++C از ابتدا با WebGPU بیشتر غرق شوید، به مستندات ++C «یادگیری WebGPU برای C» و مثال‌های Dawn Native WebGPU مراجعه کنید.

اگر به Rust علاقه دارید، می‌توانید کتابخانه گرافیکی wgpu مبتنی بر WebGPU را نیز بررسی کنید. نگاهی به دموی hello-triangle آنها بیندازید.

تقدیرنامه‌ها

این مقاله توسط کورنتین والز ، کای نینومیا و ریچل اندرو بررسی شده است.