สร้างแอปด้วย WebGPU

François Beaufort
François Beaufort

สําหรับนักพัฒนาเว็บ WebGPU คือ API กราฟิกบนเว็บที่ให้การเข้าถึง GPU แบบรวมและรวดเร็ว WebGPU แสดงความสามารถของฮาร์ดแวร์สมัยใหม่และอนุญาตให้ทำการเรนเดอร์และการดำเนินการประมวลผลใน GPU ได้ คล้ายกับ Direct3D 12, Metal และ Vulkan

แม้ว่าจะเป็นเรื่องจริง แต่เรื่องราวนั้นยังไม่สมบูรณ์ WebGPU เป็นผลมาจากการทำงานร่วมกันของบริษัทรายใหญ่ เช่น Apple, Google, Intel, Mozilla และ Microsoft ในจำนวนนี้ บางคนตระหนักว่า WebGPU อาจไม่ใช่แค่ JavaScript API แต่เป็น API กราฟิกข้ามแพลตฟอร์มสำหรับนักพัฒนาซอฟต์แวร์ในระบบนิเวศต่างๆ นอกเหนือจากเว็บ

เราได้เปิดตัว JavaScript API ใน Chrome 113 เพื่อตอบสนอง Use Case หลัก อย่างไรก็ตาม ยังมีโปรเจ็กต์สำคัญอีกโปรเจ็กต์หนึ่งที่กำลังพัฒนาควบคู่ไปด้วย นั่นก็คือ webgpu.h C API ไฟล์ส่วนหัว C นี้จะแสดงรายการขั้นตอนและโครงสร้างข้อมูลที่พร้อมใช้งานทั้งหมดของ WebGPU โดยทำหน้าที่เป็นเลเยอร์การแยกแยะฮาร์ดแวร์ที่ไม่ขึ้นอยู่กับแพลตฟอร์ม ซึ่งช่วยให้คุณสร้างแอปพลิเคชันเฉพาะแพลตฟอร์มได้โดยให้อินเทอร์เฟซที่สอดคล้องกันในแพลตฟอร์มต่างๆ

ในเอกสารนี้ คุณจะได้เรียนรู้วิธีเขียนแอป C++ ขนาดเล็กโดยใช้ WebGPU ที่ทำงานได้ทั้งบนเว็บและแพลตฟอร์มที่เฉพาะเจาะจง โปรดทราบว่าคุณจะเห็นรูปสามเหลี่ยมสีแดงเดียวกันนี้ปรากฏในหน้าต่างเบราว์เซอร์และหน้าต่างเดสก์ท็อปโดยมีการปรับโค้ดฐานเพียงเล็กน้อย

ภาพหน้าจอของรูปสามเหลี่ยมสีแดงที่ทำงานด้วย WebGPU ในหน้าต่างเบราว์เซอร์และหน้าต่างเดสก์ท็อปบน macOS
ส่วนสามเหลี่ยมเดียวกันที่ทำงานด้วย WebGPU ในหน้าต่างเบราว์เซอร์และหน้าต่างเดสก์ท็อป

หลักการทำงาน

หากต้องการดูแอปพลิเคชันที่เสร็จสมบูรณ์ โปรดไปที่ที่เก็บแอปข้ามแพลตฟอร์ม WebGPU

แอปนี้เป็นตัวอย่าง C++ ที่เรียบง่ายซึ่งแสดงวิธีใช้ WebGPU เพื่อสร้างเดสก์ท็อปและแอปเว็บจากฐานโค้ดเดียว เบื้องหลัง จะใช้ webgpu.h ของ WebGPU เป็นเลเยอร์การแยกแยะฮาร์ดแวร์ที่ไม่ขึ้นอยู่กับแพลตฟอร์มผ่าน Wrapper ของ C++ ที่ชื่อ webgpu_cpp.h

บนเว็บ แอปจะสร้างขึ้นโดยใช้ emdawnwebgpu (Emscripten Dawn WebGPU) ซึ่งมีไบน์ดิ้งที่ใช้ webgpu.h บน JavaScript API ในแพลตฟอร์มบางแพลตฟอร์ม เช่น macOS หรือ Windows โปรเจ็กต์นี้สามารถสร้างขึ้นโดยใช้ Dawn ซึ่งเป็นการใช้งาน WebGPU แบบข้ามแพลตฟอร์มของ Chromium โปรดทราบว่ามี wgpu-native ซึ่งเป็นการใช้งาน webgpu.h ใน Rust ด้วย แต่ไม่ได้ใช้ในเอกสารนี้

เริ่มต้นใช้งาน

ในการเริ่มต้น คุณต้องมีคอมไพเลอร์ 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

แอปทำงานได้แต่ยังไม่มีเอาต์พุต เนื่องจากคุณต้องมีวิธีวาดสิ่งต่างๆ บนหน้าจอ

Get Dawn

หากต้องการวาดรูปสามเหลี่ยม คุณสามารถใช้ Dawn ซึ่งเป็นการใช้งาน WebGPU แบบข้ามแพลตฟอร์มของ Chromium ซึ่งรวมถึงไลบรารี C++ ของ GLFW สำหรับการวาดภาพบนหน้าจอ วิธีดาวน์โหลด Dawn คือการเพิ่มเป็น git submodule ลงในที่เก็บข้อมูล คำสั่งต่อไปนี้จะดึงข้อมูลในโฟลเดอร์ย่อย "dawn/"

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

จากนั้นเพิ่มข้อมูลต่อท้ายไฟล์ CMakeLists.txt ดังนี้

  • ตัวเลือก CMake DAWN_FETCH_DEPENDENCIES จะดึงข้อมูล Dependency ทั้งหมดของ 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() เพื่อส่งคำขอไม่ให้มีการเริ่มต้น Graphics 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

ใน JavaScript นั้น navigator.gpu คือจุดแรกเข้าสำหรับการเข้าถึง GPU ใน C++ คุณต้องสร้างตัวแปร wgpu::Instance ด้วยตนเองเพื่อวัตถุประสงค์เดียวกัน ประกาศ instance ที่ด้านบนของไฟล์ main.cpp และเรียกใช้ wgpu::CreateInstance() ภายใน Init() เพื่ออำนวยความสะดวก

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  wgpu::InstanceDescriptor instanceDesc{};
  instanceDesc.capabilities.timedWaitAnyEnable = true;
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

ประกาศตัวแปร 2 ตัว ได้แก่ wgpu::Adapter และ wgpu::Device ที่ด้านบนของไฟล์ main.cpp อัปเดตฟังก์ชัน Init() ให้เรียกใช้ instance.RequestAdapter() และกำหนด Callback ของผลลัพธ์เป็น adapter จากนั้นเรียกใช้ adapter.RequestDevice() และกำหนด Callback ของผลลัพธ์เป็น device

#include <dawn/webgpu_cpp_print.h>

#include <iostream>


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 Chain จะไม่แสดงใน JavaScript API เนื่องจากเบราว์เซอร์จะจัดการให้ ใน C++ คุณจะต้องสร้างด้วยตนเอง อีกครั้ง ประกาศตัวแปร wgpu::Surface ที่ด้านบนของไฟล์ main.cpp เพื่ออำนวยความสะดวก หลังจากสร้างหน้าต่าง GLFW ใน Start() แล้ว ให้เรียกใช้ฟังก์ชัน wgpu::glfw::CreateSurfaceForWindow() ที่มีประโยชน์เพื่อสร้าง wgpu::Surface (คล้ายกับ Canvas ของ 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,
      .presentMode = wgpu::PresentMode::Fifo};
  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{};
  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();
}

สุดท้าย ให้ส่งคําสั่งการแสดงผลไปยัง GPU ในฟังก์ชัน 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 และเรียกใช้แอปตอนนี้ทำให้เห็นสามเหลี่ยมสีแดงในหน้าต่างที่รอคอยมาอย่างยาวนาน พักสักหน่อย คุณสมควรได้รับ

ภาพหน้าจอของรูปสามเหลี่ยมสีแดงในหน้าต่าง macOS
สามเหลี่ยมสีแดงในหน้าต่างเดสก์ท็อป

คอมไพล์เป็น WebAssembly

มาดูการเปลี่ยนแปลงขั้นต่ำที่จําเป็นในการปรับโค้ดเบสที่มีอยู่เพื่อวาดสามเหลี่ยมสีแดงนี้ในหน้าต่างเบราว์เซอร์กัน อีกครั้ง แอปสร้างขึ้นโดยใช้ emdawnwebgpu (Emscripten Dawn WebGPU) ซึ่งมีไบน์ดิ้งที่ใช้ webgpu.h บน JavaScript 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++ แบบซิงโครนัสโต้ตอบกับ JavaScript แบบอะซิงโครนัสได้
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")

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)
  target_link_options(app PRIVATE "-sASYNCIFY=1")
else()
  target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()

อัปเดตโค้ด

ใน Emscripten การสร้าง wgpu::surface ต้องใช้องค์ประกอบ HTML Canvas ในการดําเนินการนี้ ให้เรียกใช้ instance.CreateSurface() และระบุตัวเลือก #canvas เพื่อจับคู่กับองค์ประกอบ Canvas ของ HTML ที่เหมาะสมในหน้า HTML ที่ Emscripten สร้างขึ้น

แทนที่จะใช้วงวน while ให้เรียกใช้ emscripten_set_main_loop(Render) เพื่อให้แน่ใจว่ามีการเรียกใช้ฟังก์ชัน Render() ในอัตราที่ราบรื่นที่เหมาะสมซึ่งสอดคล้องกับเบราว์เซอร์และจอภาพ

#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>

#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <GLFW/glfw3.h>
#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::EmscriptenSurfaceSourceCanvasHTMLSelector source{};
  source.selector = "#canvas";
  wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &source};
  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
ภาพหน้าจอของรูปสามเหลี่ยมสีแดงในหน้าต่างเบราว์เซอร์
สามเหลี่ยมสีแดงในหน้าต่างเบราว์เซอร์

ขั้นตอนถัดไป

สิ่งที่จะเกิดขึ้นในอนาคตมีดังนี้

  • การปรับปรุงความเสถียรของ API webgpu.h และ webgpu_cpp.h
  • การรองรับ Dawn เบื้องต้นสำหรับ Android และ iOS

ในระหว่างนี้ โปรดแจ้งปัญหา WebGPU สำหรับ Emscripten และปัญหา Dawn พร้อมคำแนะนำและคำถาม

แหล่งข้อมูล

คุณสามารถสำรวจซอร์สโค้ดของแอปนี้ได้

หากต้องการเจาะลึกการสร้างแอปพลิเคชัน 3 มิติแบบเนทีฟใน C++ ตั้งแต่ต้นด้วย WebGPU โปรดดูดูเอกสารประกอบ WebGPU สําหรับ C++ และตัวอย่าง WebGPU ของ Dawn แบบเนทีฟ

หากสนใจ Rust คุณยังสำรวจไลบรารีกราฟิก wgpu ที่อิงตาม WebGPU ได้ด้วย โปรดดูที่เดโม hello-triangle

คำขอบคุณ

บทความนี้ได้รับการตรวจสอบโดย Corentin Wallez, Kai Ninomiya และ Rachel Andrew

รูปภาพโดย Marc-Olivier Jodoin ใน Unsplash