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

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

برای توسعه دهندگان وب، 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 در یک پنجره مرورگر و یک پنجره دسکتاپ در macOS طراحی شده است.
همان مثلثی که توسط 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/ فرعی در هدف گنجانده شده است.
  • برنامه شما به اهداف webgpu_cpp ، webgpu_dawn ، و webgpu_glfw بستگی دارد تا بتوانید بعداً از آنها در فایل main.cpp استفاده کنید.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn 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();
}

بازسازی برنامه و اجرای آن مانند قبل در حال حاضر منجر به یک پنجره خالی می شود. شما در حال پیشرفت هستید!

اسکرین شات از پنجره خالی macOS.
یک پنجره خالی

دستگاه 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++، یک تابع GetDevice() کمکی ایجاد کنید که آرگومان تابع callback را می گیرد و آن را با wgpu::Device حاصل فراخوانی می کند.

#include <iostream>
…

void GetDevice(void (*callback)(wgpu::Device)) {
  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);
        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);
            },
            userdata);
      },
      reinterpret_cast<void*>(callback));
}

برای دسترسی راحت‌تر، یک متغیر wgpu::Device در بالای فایل main.cpp اعلام کنید و تابع main() برای فراخوانی GetDevice() به‌روزرسانی کنید و قبل از فراخوانی Start() نتیجه تماس را به device اختصاص دهید.

wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetDevice([](wgpu::Device dev) {
    device = dev;
    Start();
  });
}

یک مثلث بکشید

زنجیره swap در JavaScript API نمایش داده نمی شود زیرا مرورگر از آن مراقبت می کند. در C++، باید آن را به صورت دستی ایجاد کنید. یک بار دیگر، برای راحتی، یک متغیر wgpu::SwapChain در بالای فایل main.cpp اعلام کنید. درست پس از ایجاد پنجره GLFW در Start() ، تابع wgpu::glfw::CreateSurfaceForWindow() را فراخوانی کنید تا یک wgpu::Surface (مشابه بوم HTML) ایجاد کنید و از آن برای راه اندازی زنجیره swap با فراخوانی new استفاده کنید. تابع SetupSwapChain() helper در InitGraphics() . همچنین باید swapChain.Present() را فراخوانی کنید تا بافت بعدی در حلقه while ارائه شود. این هیچ اثر قابل مشاهده ای ندارد زیرا هنوز هیچ رندری اتفاق نمی افتد.

#include <webgpu/webgpu_glfw.h>
…

wgpu::SwapChain swapChain;

void SetupSwapChain(wgpu::Surface surface) {
  wgpu::SwapChainDescriptor scDesc{
      .usage = wgpu::TextureUsage::RenderAttachment,
      .format = wgpu::TextureFormat::BGRA8Unorm,
      .width = kWidth,
      .height = kHeight,
      .presentMode = wgpu::PresentMode::Fifo};
  swapChain = device.CreateSwapChain(surface, &scDesc);
}

void InitGraphics(wgpu::Surface surface) {
  SetupSwapChain(surface);
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  …
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics(surface);

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.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 = wgpu::TextureFormat::BGRA8Unorm};

  wgpu::FragmentState fragmentState{.module = shaderModule,
                                    .targetCount = 1,
                                    .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{
      .vertex = {.module = shaderModule},
      .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics(wgpu::Surface surface) {
  …
  CreateRenderPipeline();
}

در نهایت، دستورات رندر را در تابع Render() به نام هر فریم به GPU ارسال کنید.

void Render() {
  wgpu::RenderPassColorAttachment attachment{
      .view = swapChain.GetCurrentTextureView(),
      .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

بیایید اکنون به حداقل تغییرات مورد نیاز برای تنظیم پایگاه کد موجود برای ترسیم این مثلث قرمز در پنجره مرورگر نگاهی بیندازیم. مجدداً، این برنامه بر اساس 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 webgpu_cpp webgpu_dawn 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};
  wgpu::Surface surface = instance.CreateSurface(&surfaceDesc);
#else
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics(surface);

#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.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 .