Kiến Trúc Hiện Tại
Trước khi động vào bất kỳ dòng code nào, cần hiểu điều gì tạo nên lớp rendering của Axmol.
Lớp Trừu Tượng RHI
Toàn bộ quá trình rendering trong Axmol đi qua một tập hợp các interface trừu tượng nằm trong axmol/rhi/:
DriverBase -- GPU device: tạo tài nguyên
RenderContext -- Command encoder: ghi lại các lệnh vẽ
Buffer -- Dữ liệu vertex / index / uniform
Texture -- Texture 2D, cubemap + sampler
ShaderModule -- Giai đoạn shader đã biên dịch
Program -- Cặp vertex + fragment đã liên kết
RenderPipeline -- Pipeline state object
RenderTarget -- Các attachment của framebuffer
DepthStencilState -- Cấu hình depth/stencil
VertexLayout -- Mô tả thuộc tính vertex
Mỗi backend triển khai các interface này trong thư mục con riêng:
axmol/rhi/
opengl/ -- DriverGL, RenderContextGL, BufferGL, ...
metal/ -- DriverMTL, RenderContextMTL, BufferMTL, ...
d3d11/ -- Driver11, RenderContext11, Buffer11, ...
d3d12/ -- Driver12, RenderContext12, Buffer12, ...
vulkan/ -- DriverVK, RenderContextVK, BufferVK, ...
Factory + Lựa Chọn Ưu Tiên
Việc chọn backend sử dụng mẫu Abstract Factory với thứ tự ưu tiên tại runtime:
enum class DriverType { Auto = -1, OpenGL, D3D11, D3D12, Vulkan, Metal, Count };
struct DefaultDriverPriority {
static constexpr int OpenGL = 70;
static constexpr int D3D11 = 80;
static constexpr int Vulkan = 90;
static constexpr int D3D12 = 100;
static constexpr int Metal = 100;
};
Khi khởi động, DriverContext::makeCurrentDriver() tạo factory cho tất cả backend được bật, sắp xếp theo độ ưu tiên, và thử từng cái cho đến khi một cái khởi tạo thành công. OpenGL đóng vai trò fallback toàn năng.
Pipeline Shader
Axmol sử dụng axslcc, một shader cross-compiler tùy chỉnh được xây dựng trên SPIRV-Cross. Shader nguồn được viết bằng GLSL 310 ES và biên dịch thành file chunk .axslc nhúng nhiều target:
.vert / .frag (GLSL 310 ES source)
|
[axslcc]
|
.axslc chunk
+-- GLES (OpenGL ES)
+-- GLSL (Desktop GL)
+-- HLSL (D3D11 / D3D12)
+-- MSL (Metal)
+-- SPIR-V (Vulkan)
Mỗi backend đọc target mà nó cần từ cùng một file chunk. Đây là yếu tố then chốt để thêm backend mới mà không cần chỉnh sửa shader nguồn.
Hỗ Trợ Nền Tảng
Axmol chạy trên Windows, macOS, iOS, tvOS, Android, Linux và WebAssembly (qua Emscripten). Target WASM trước đây sử dụng WebGL thông qua backend OpenGL ES — hoạt động được, nhưng ngày càng bộc lộ hạn chế so với các API đồ họa native.
Tại Sao Chọn WebGPU?
WebGPU là API đồ họa thế hệ tiếp theo cho web, được thiết kế bởi W3C GPU for the Web Community Group. Đây là một bước chuyển mình căn bản so với WebGL:
Hiệu Năng
- Quản lý tài nguyên tường minh. WebGPU loại bỏ state machine ẩn khiến WebGL chậm. Bạn tạo các pipeline state object bất biến, bind group và command buffer — driver không phải đoán mò nhiều nữa.
- Giảm overhead validation. WebGL validate mỗi lệnh gọi ở tầng API. WebGPU dồn quá trình validation vào lúc tạo pipeline, giúp công việc per-frame nhẹ hơn.
- Batching tốt hơn. Command encoder cho phép ghi công việc GPU trước và nộp một lần, giảm thiểu đồng bộ CPU-GPU.
Tính Năng Hiện Đại
- Compute shader. WebGL không có hỗ trợ compute. WebGPU cung cấp GPU compute đa năng đầy đủ, mở ra khả năng cho particle system, vật lý và post-processing trên GPU.
- Render pass tường minh. Ngữ nghĩa render pass đúng đắn cho phép tối ưu tile-based deferred rendering trên GPU mobile, quan trọng khi WebGPU chạy trên điện thoại.
- Định dạng texture tốt hơn. Hỗ trợ tùy chọn cho texture nén BC, ETC2 và ASTC thông qua feature query.
Câu Chuyện Đa Nền Tảng
WebGPU không chỉ là một web API. Thư viện Dawn của Google triển khai cùng API này một cách native trên Windows (dùng D3D12), macOS (Metal) và Linux (Vulkan). Điều này có nghĩa là backend WebGPU có thể trở thành backend toàn năng — một implementation nhắm đến mọi nền tảng. Với Phase 1, chúng tôi chỉ target Emscripten, nhưng kiến trúc đã sẵn sàng cho tích hợp Dawn.
Sự Chuyển Đổi Tất Yếu
WebGL đang ở chế độ bảo trì. Các nhà cung cấp trình duyệt đang đầu tư vào WebGPU. Chrome, Firefox và Safari đều đã ship hỗ trợ WebGPU. Với một game engine nhắm đến web, đây là chuyện sớm muộn mà thôi.
Kiến Trúc Khi Thêm WebGPU
Mục tiêu thiết kế rất đơn giản: WebGPU phải là backend thứ sáu, tuân theo đúng các pattern của năm backend hiện có. Không có trường hợp đặc biệt, không rò rỉ abstraction.
Vị Trí của WebGPU
axmol/rhi/
opengl/ -- GL / GLES
metal/ -- Apple Metal
d3d11/ -- Direct3D 11
d3d12/ -- Direct3D 12
vulkan/ -- Vulkan
webgpu/ -- WebGPU (MỚI)
WebGPU có giá trị enum DriverType::WebGPU riêng, WebGPUDriverFactory riêng, và độ ưu tiên riêng (95 — cao hơn 70 của OpenGL, khiến nó trở thành backend ưu tiên trên WASM).
Chiến Lược Shader: Tái Sử Dụng SPIR-V
Phần thanh lịch nhất của tích hợp: WebGPU tái sử dụng output SPIR-V mà backend Vulkan đang dùng. Implementation WebGPU của Emscripten bao gồm shader compiler Tint của Google, tự động chuyển đổi SPIR-V sang WGSL tại runtime.
Điều này có nghĩa là:
- Không cần thay đổi gì trong
axslcchay file shader nguồn - Không cần target ngôn ngữ shader mới
- Các file chunk
.axslcđã chứa dữ liệu mà WebGPU cần
Phân công ngôn ngữ shader trong DriverContext giống hệt Vulkan:
case DriverType::WebGPU:
_currentShaderLang = axslc::SHADER_LANG_SPIRV;
_currentShaderProfile = 100;
break;
Backend Tham Chiếu: Vulkan
WebGPU được thiết kế theo phong cách “Vulkan-like” với quản lý tài nguyên tường minh, nên backend Vulkan là tài liệu tham khảo chính của chúng tôi:
| Khái niệm | Vulkan | WebGPU |
|---|---|---|
| Tạo device | vkCreateDevice |
wgpuAdapterRequestDevice |
| Ghi command | VkCommandBuffer |
WGPUCommandEncoder |
| Render pass | vkCmdBeginRenderPass |
wgpuCommandEncoderBeginRenderPass |
| Pipeline state | VkPipeline (PSO bất biến) |
WGPURenderPipeline (bất biến) |
| Cập nhật buffer | vkCmdCopyBuffer / staging |
wgpuQueueWriteBuffer |
| Upload texture | vkCmdCopyBufferToImage |
wgpuQueueWriteTexture |
| Shader input | SPIR-V | SPIR-V (qua Tint) hoặc WGSL |
| Binding descriptor | Descriptor set | Bind group |
| Present | vkQueuePresentKHR |
wgpuSurfacePresent |
Những Khác Biệt Kiến Trúc Quan Trọng
Dù mapping rất gần nhau, WebGPU có những điểm khác biệt đáng chú ý:
1. Tính bất biến của pipeline state nghiêm ngặt hơn. Vulkan hỗ trợ dynamic state extension (cull mode, front face, topology có thể thay đổi mà không cần rebind pipeline). WebGPU đóng cứng mọi thứ vào pipeline. Điều này có nghĩa là cache key của pipeline phải bao gồm cull mode, front face và primitive topology — tương tự cách backend D3D12 hoạt động.
2. Không có primitive LINE_LOOP. WebGPU bỏ hoàn toàn LINE_LOOP. Backend fallback về LINE_STRIP.
3. Khởi tạo bất đồng bộ. wgpuInstanceRequestAdapter và wgpuAdapterRequestDevice là bất đồng bộ trên web. Chúng tôi dùng emscripten_sleep() của Emscripten để block đến khi callback kích hoạt, đòi hỏi linker flag -sASYNCIFY.
4. Quản lý buffer đơn giản hơn. Trong khi Vulkan yêu cầu staging buffer tường minh và cấp phát bộ nhớ (qua VMA), WebGPU cung cấp wgpuQueueWriteBuffer và wgpuQueueWriteTexture — driver tự xử lý staging nội bộ. Điều này giúp đơn giản hóa đáng kể các implementation buffer và texture.
Hướng Dẫn Triển Khai
Bước 1: Hệ Thống Kiểu RHI
Nền tảng — thêm WebGPU vào hệ thống kiểu để phần còn lại của engine nhận biết nó tồn tại.
RHITypes.h — Giá trị enum và độ ưu tiên mới:
// (xem file gốc để biết chi tiết code)
Bước 5: Shader Module
Phần đơn giản nhất, nhờ tái sử dụng SPIR-V:
void ShaderModuleImpl::compileShader(WGPUDevice device) {
auto codeData = getChunkData(); // Trích SPIR-V từ .axslc
WGPUShaderModuleSPIRVDescriptor spirvDesc{};
spirvDesc.chain.sType = WGPUSType_ShaderModuleSPIRVDescriptor;
spirvDesc.codeSize = codeData.second / sizeof(uint32_t);
spirvDesc.code = reinterpret_cast<const uint32_t*>(codeData.first);
WGPUShaderModuleDescriptor moduleDesc{};
moduleDesc.nextInChain = &spirvDesc.chain;
_shader = wgpuDeviceCreateShaderModule(device, &moduleDesc);
}
Bước 6: Buffer và Texture
Đơn giản hơn rất nhiều so với Vulkan nhờ wgpuQueueWriteBuffer:
void BufferImpl::updateData(const void* data, std::size_t size) {
if (size > _size) {
// Tạo lại với kích thước mới
wgpuBufferDestroy(_buffer);
_size = size;
createNativeBuffer(data);
} else {
wgpuQueueWriteBuffer(_driver->getQueue(), _buffer, 0, data, size);
}
}
Không staging buffer, không truy vấn loại bộ nhớ, không VMA allocator. WebGPU tự lo.
Bước 7: Pipeline Cache
Đây là phần thú vị nhất về mặt kiến trúc. Pipeline WebGPU hoàn toàn bất biến — thậm chí còn hơn cả Vulkan (vốn có dynamic state extension). Cache key phải bao gồm mọi thứ:
// Hash kết hợp: program ID + blend state + depth/stencil +
// topology + cull mode + front face + render target format
uintptr_t psoKey = 0;
hashCombine(psoKey, program->getProgramId());
hashCombine(psoKey, blendDesc.blendEnabled);
hashCombine(psoKey, blendDesc.sourceRGBBlendFactor);
hashCombine(psoKey, topology); // đóng cứng vào pipeline WebGPU
hashCombine(psoKey, cullMode); // đóng cứng vào pipeline WebGPU
hashCombine(psoKey, frontFace); // đóng cứng vào pipeline WebGPU
hashCombine(psoKey, rt->getColorFormat());
Trên thực tế, hầu hết các frame tái sử dụng cùng một vài pipeline, nên overhead tìm kiếm hash là không đáng kể.
Bước 8: Render Context
Vòng lặp rendering cốt lõi ánh xạ gọn gàng sang model command của WebGPU:
beginFrame()
|-- wgpuSurfaceGetCurrentTexture()
|-- wgpuDeviceCreateCommandEncoder()
|
+-- beginRenderPass()
| |-- wgpuCommandEncoderBeginRenderPass()
| |-- setViewport() -> wgpuRenderPassEncoderSetViewport()
| |-- drawElements() -> wgpuRenderPassEncoderDrawIndexed()
| +-- endRenderPass() -> wgpuRenderPassEncoderEnd()
|
+-- endFrame()
|-- wgpuCommandEncoderFinish()
|-- wgpuQueueSubmit()
+-- wgpuSurfacePresent()
Quản lý surface sử dụng canvas của Emscripten:
WGPUSurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
canvasDesc.chain.sType = WGPUSType_SurfaceDescriptorFromCanvasHTMLSelector;
canvasDesc.selector = "#canvas";
_surface = wgpuInstanceCreateSurface(instance, &surfaceDesc);
Build và Kiểm Thử
Build
Với các thay đổi CMake đã có, build cho WASM với WebGPU chỉ cần thay một flag:
# Trước (WebGL)
cmake -DAX_RENDER_API=gl -DCMAKE_TOOLCHAIN_FILE=$EMSDK/emscripten/cmake/Modules/Platform/Emscripten.cmake ..
# Sau (WebGPU -- giờ là mặc định cho WASM)
cmake -DAX_RENDER_API=wgpu -DCMAKE_TOOLCHAIN_FILE=$EMSDK/emscripten/cmake/Modules/Platform/Emscripten.cmake ..
Hoặc đơn giản là bỏ AX_RENDER_API — auto selection giờ ưu tiên WebGPU trên WASM.
Chiến Lược Kiểm Thử
- Smoke test: Chạy
cpp-teststrong Chrome với WebGPU được bật. Kiểm tra rendering sprite 2D cơ bản. - Kiểm tra tính năng: Kiểm tra có hệ thống các chế độ blend, stencil operation, depth testing, instanced rendering.
- Ma trận trình duyệt: Chrome (chính), Firefox, Safari — tất cả đều hỗ trợ WebGPU.
- Fallback: Build với
AX_RENDER_API=wgpu;glđể xác minh GL fallback khi WebGPU không khả dụng. - So sánh hiệu năng: So sánh frame time giữa WebGL và WebGPU trên cùng scene kiểm thử.
Hạn Chế Đã Biết
- readPixels: Chưa triển khai. WebGPU yêu cầu mapping staging buffer bất đồng bộ (
wgpuBufferMapAsync), cần tích hợp cẩn thận với callback API đồng bộ củareadPixelstrong engine. - LINE_LOOP: Được giả lập bằng
LINE_STRIP. Hầu hết code engine dùng triangle, nên vấn đề này hiếm khi gặp. - Phụ thuộc SPIR-V: Spec WebGPU chỉ bắt buộc WGSL. Tint của Emscripten xử lý chuyển đổi SPIR-V hiện tại, nhưng một target WGSL output trong
axslcctương lai sẽ bền vững hơn.
Tổng Kết File
Implementation chỉnh sửa 10 file hiện có và thêm 22 file mới:
Các file đã chỉnh sửa:
axmol/rhi/RHITypes.h– Enum DriverType, độ ưu tiênaxmol/rhi/DriverContext.h– HelperisWebGPU()axmol/rhi/DriverContext.cpp– Đăng ký factory, ngôn ngữ shaderaxmol/rhi/DriverFactory.h–WebGPUDriverFactoryaxmol/CMakeLists.txt– API hợp lệ, auto selection, macro propagationaxmol/rhi/CMakeLists.txt– Đăng ký file nguồnaxmol/platform/PlatformConfig.h– MacroAX_ENABLE_WGPUaxmol/platform/desktop/RenderViewImpl.cpp– Surface handlecmake/Modules/AXConfigDefine.cmake– Linker flagcmake/Modules/AXSLCC.cmake– Chia sẻ target SPIR-V
Các file mới (tất cả trong axmol/rhi/webgpu/):
| File | Mục đích |
|---|---|
UtilsWGPU.h/.cpp |
Chuyển đổi format (14 hàm) |
DriverWGPU.h/.cpp |
Khởi tạo device, tạo tài nguyên, truy vấn feature |
ShaderModuleWGPU.h/.cpp |
SPIR-V sang WGPUShaderModule |
ProgramWGPU.h/.cpp |
Wrapper module VS + FS |
BufferWGPU.h/.cpp |
Quản lý GPU buffer |
TextureWGPU.h/.cpp |
Tạo và upload texture |
VertexLayoutWGPU.h/.cpp |
Layout thuộc tính vertex |
DepthStencilStateWGPU.h/.cpp |
Cấu hình depth/stencil |
RenderTargetWGPU.h/.cpp |
Attachment của framebuffer |
RenderPipelineWGPU.h/.cpp |
Tạo pipeline + cache |
RenderContextWGPU.h/.cpp |
Mã hóa command + present |
Bài Học Rút Ra
-
Abstraction tốt sẽ trả lại xứng đáng. Lớp RHI được thiết kế chính xác cho kịch bản này. Thêm backend thứ sáu không cần thay đổi gì ở renderer, scene graph hay bất kỳ code cấp game nào. Các interface trừu tượng định nghĩa hợp đồng; chúng ta chỉ điền vào thêm một implementation nữa.
-
SPIR-V là định dạng trao đổi shader toàn năng. Bằng cách biên dịch sang SPIR-V một lần và để các công cụ backend xử lý bước cuối (SPIRV-Cross cho MSL/HLSL/GLSL, Tint cho WGSL), pipeline shader vẫn gọn gàng và dễ mở rộng.
-
WebGPU gần Vulkan hơn vẻ ngoài. Việc ánh xạ khái niệm gần như 1:1. Điểm ma sát chính là tính bất biến của pipeline state (cull mode và topology đóng cứng vào) và khởi tạo bất đồng bộ — cả hai đều xử lý được một cách đơn giản.
-
Đơn giản hơn không có nghĩa là kém năng lực hơn.
wgpuQueueWriteBuffervàwgpuQueueWriteTexturecủa WebGPU loại bỏ cả mảng boilerplate của Vulkan (staging buffer, cấp phát bộ nhớ, layout transition) mà không hi sinh hiệu năng. Các implementation buffer và texture có kích thước xấp xỉ một nửa so với phiên bản Vulkan. -
Nền tảng web đang bắt kịp. WebGPU trao cho ứng dụng web quyền truy cập vào những tính năng GPU mà ứng dụng native đã được hưởng từ lâu. Với một engine đa nền tảng như Axmol, điều này thu hẹp khoảng cách về chất lượng rendering giữa web và native.
Công trình này thêm backend WebGPU nền tảng vào Axmol Engine. Phase 2 sẽ khám phá tích hợp Dawn cho WebGPU native trên desktop, hỗ trợ compute shader, và profiling hiệu năng so với các backend hiện có.
Bài viết liên quan: Build Web Game bằng Cocos2dx - Axmol Engine