Zenith.NET: Tích hợp Metal Backend, Hành trình trừu tượng GPU đa nền tảng cho .NET

Zenith.NET đã đạt được mục tiêu ban đầu là cung cấp một bộ API .NET duy nhất để hoạt động trên nhiều backend đồ họa khác nhau, với việc tích hợp thành công backend Metal. Việc phát triển đã tiến triển từ việc khởi tạo `GraphicsContext.CreateDirectX12()` đến việc chạy thành công tất cả các bài kiểm tra với `GraphicsContext.CreateMetal()`.

Bài viết này đi sâu vào các lựa chọn kỹ thuật, thiết kế kiến trúc cho backend Metal, và triết lý thiết kế của Zenith.NET như một lớp trừu tượng GPU cho .NET.

Tại sao cần Zenith.NET?

Hệ sinh thái .NET có một số thư viện đồ họa, bao gồm các lớp binding như Silk.NET và Vortice, cũng như các lớp trừu tượng như Veldrid và Evergine. Tuy nhiên, các lớp trừu tượng hiện có hoặc bị giới hạn ở các phiên bản API cũ hơn (như DX11/OpenGL) hoặc là một phần của các engine thương mại, gây khó khăn cho việc sử dụng chúng như các lớp trừu tượng GPU độc lập.

Zenith.NET được định vị là một lớp trừu tượng GPU nhẹ cho các API đồ họa hiện đại (DirectX 12, Metal 4, Vulkan 1.4). Mục tiêu là chỉ tập trung vào việc trừu tượng hóa mà không xây dựng engine, cho phép nhà phát triển viết mã một lần và chạy trên mọi nền tảng.

Các backend được hỗ trợ và chiến lược của Zenith.NET:

  • DirectX 12: Dành riêng cho Windows, mang lại hiệu suất cao nhất.
  • Metal 4: Hỗ trợ toàn bộ nền tảng Apple, chỉ hoạt động trên Apple Silicon.
  • Vulkan 1.4: Giải pháp dự phòng đa nền tảng, bao phủ Linux/Android.

Ba backend này không thay thế lẫn nhau mà hoạt động song song, sử dụng API gốc phù hợp nhất trên mỗi nền tảng.

Backend Metal: Quyết định về Kiến trúc

Lựa chọn Metal.NET

Trong ghi chú phát hành v0.0.6, đã có sự cân nhắc giữa SharpMetal và .NET `macios` TFM. Cuối cùng, Metal.NET (gói NuGet `Metal.NET 2.3.0`) đã được chọn. Đây là một thư viện binding được phát triển trong quá trình phát triển, cung cấp phạm vi API Metal 4 đầy đủ hơn và giao diện an toàn kiểu dữ liệu so với SharpMetal. Tuy nhiên, cần lưu ý rằng Metal.NET bao bọc các đối tượng Objective-C bằng các lớp, có thể gây ra một số chi phí liên quan đến bộ thu gom rác (GC).

Cấu trúc Tổng thể

Dưới đây là sự tương ứng giữa trừu tượng của Zenith.NET và triển khai Metal:

  • GraphicsContext tương ứng với MTLDevice, MTL4Compiler, và MTLResidencySet.
  • CommandBuffer được triển khai bằng MTL4CommandBuffer, sử dụng bộ mã hóa kép (Render/Compute).
  • ResourceLayout đếm số lượng khe cắm (Buffer/Texture/Sampler).
  • ResourceTable được triển khai bằng MTL4ArgumentTable, liên kết tài nguyên thông qua địa chỉ GPU.
  • Pipeline bao gồm MTLRenderPipelineStateMTLDepthStencilState.
  • SwapChain sử dụng CAMetalLayerCAMetalDrawable.
  • AccelerationStructure bao gồm MTLAccelerationStructure và bộ đệm thể hiện.

Áp dụng Tính năng Mới của Metal 4

Metal 4 giới thiệu các tính năng mới quan trọng cho lớp trừu tượng, và backend Metal của Zenith.NET đã tận dụng chúng đầy đủ:

  • MTL4ArgumentTable: Đây là mô hình binding tài nguyên hoàn toàn mới trong Metal 4. Thay vì phải gọi `setBuffer/setTexture/setSampler` cho từng tài nguyên như Metal cũ, Argument Table cho phép đóng gói tất cả tài nguyên vào một bảng duy nhất và liên kết chúng thông qua địa chỉ GPU. Điều này phù hợp tự nhiên với trừu tượng `ResourceLayout` + `ResourceTable` của Zenith.NET:
    • ResourceLayout khai báo số lượng khe cắm Buffer/Texture/Sampler.
    • ResourceTable tạo MTL4ArgumentTable và điền địa chỉ GPU.
    • SetResourceTable() liên kết toàn bộ bảng chỉ với một lệnh gọi duy nhất.
  • MTL4CommandBuffer với Dual-Encoder: Mô hình này cho phép chỉ có một bộ mã hóa (Encoder) hoạt động tại một thời điểm. CommandBuffer mặc định sử dụng Compute Encoder. Khi người dùng bắt đầu một Render Pass, Compute Encoder sẽ bị đóng, chuyển sang Render Encoder. Sau khi Render Pass kết thúc, nó sẽ tự động chuyển về Compute Encoder. Điều này đảm bảo Compute Encoder luôn sẵn sàng cho các thao tác sao chép (Blit) và tính toán, đơn giản hóa việc quản lý vòng đời của Encoder và hợp nhất ngữ nghĩa của các rào cản (barrier) cho các thao tác sao chép.
  • MTL4Compiler cho Biên dịch phía Thiết bị: Cho phép biên dịch mã IR của Slang thành ISA cuối cùng trực tiếp trên GPU đích. Điều này mang lại khả năng tối ưu hóa đặc biệt cho GPU so với biên dịch ngoại tuyến truyền thống.

Kết nối Bộ nhớ Objective-C

API Metal dựa trên runtime Objective-C, và các đối tượng trả về thường được autoreleased, nghĩa là chúng sẽ bị thu hồi khi thoát khỏi autorelease pool hiện tại. Trong môi trường quản lý của .NET, đây là một cạm bẫy tiềm ẩn. Giải pháp là sử dụng một công cụ cầu nối chung:


public static T Own<T>(Func<T> func) where T : NSObject
{
    using NSAutoreleasePool _ = new();
    return func().Retain();
}

Tất cả các đối tượng nhận được từ API Metal đều được gói gọn bằng `NSAutorelease.Own()` để đảm bảo `Retain` kéo dài vòng đời của chúng. Sau đó, chúng sẽ được giải phóng bởi cơ chế `Dispose` của .NET.

Biên dịch Shader: Quy trình Thống nhất với Slang

Cả ba backend đều chia sẻ cùng một mã nguồn shader Slang:

  • Tệp nguồn .slang
  • Chuyển đổi thành metallib (Metal Shader Library)
  • Chuyển đổi thành dxil (DirectX Intermediate Language)
  • Chuyển đổi thành spirv (SPIR-V cho Vulkan)

Nhà phát triển chỉ cần duy trì một tệp shader `.slang`, và `Slangc.NET` sẽ tự động xử lý việc biên dịch cho từng backend.

Ray Tracing

Backend Metal hỗ trợ đầy đủ ray tracing phần cứng:

  • BLAS/TLAS: Cấu trúc tăng tốc hai cấp tiêu chuẩn.
  • Instance Buffers: Cho phép cập nhật biến đổi mà không cần xây dựng lại TLAS.
  • RayQuery: Truy vấn nội tuyến trong mọi giai đoạn shader mà không yêu cầu pipeline ray tracing chuyên dụng.

Điều này phù hợp với quyết định loại bỏ `RayTracingPipeline` trong v0.0.6, cho phép thống nhất khả năng ray tracing trên cả ba backend thông qua `RayQuery`.

Triết lý Thiết kế

Chỉ Phơi bày Khả năng Chung

Nguyên tắc cốt lõi của Zenith.NET là: sử dụng phiên bản API mới nhất và chỉ phơi bày các khả năng được cả ba backend hỗ trợ chung. Các tính năng đặc thù của nền tảng bị loại bỏ có chủ đích để duy trì trải nghiệm đa nền tảng nhất quán.

Điều này có nghĩa là bạn sẽ không tìm thấy các API như Enhanced Barriers của DX12, Push Descriptors của Vulkan hay Tile Shading của Metal trong API của Zenith.NET, vì chúng chỉ có trên một API cụ thể và sẽ phá vỡ cam kết "viết một lần, chạy mọi nơi".

Đối với sự khác biệt về khả năng phần cứng (ví dụ: không phải GPU nào cũng hỗ trợ ray tracing), Zenith.NET sử dụng truy vấn khả năng động thông qua `Capabilities`:


if (context.Capabilities.RayTracingSupported) { /* Đường dẫn Ray Tracing */ }
if (context.Capabilities.MeshShadingSupported) { /* Đường dẫn Mesh Shader */ }

Phơi bày chung các khả năng, phát hiện khác biệt phần cứng động - đây là điểm khác biệt lớn nhất của Zenith.NET so với các lớp trừu tượng khác.

Sử dụng API Nguyên bản trên Mỗi Nền tảng

Zenith.NET không giống như bgfx, nơi Vulkan được sử dụng để bao phủ tất cả các nền tảng. Thay vào đó, nó sử dụng DirectX 12 trên Windows, Metal 4 trên Apple, và Vulkan 1.4 trên Linux/Android. Mặc dù lớp trên chỉ phơi bày các khả năng chung, mỗi backend bên trong đều được triển khai theo cách đặc trưng nhất của API tương ứng, không cần mô phỏng hành vi của API khác.

Thân thiện với AOT

Toàn bộ thư viện được thiết kế ngay từ đầu cho Native AOT, không sử dụng reflection, tạo mã động, hay `Activator.CreateInstance`. Các thư viện nền tảng như Metal.NET và Silk.NET sử dụng P/Invoke và con trỏ hàm ở cấp độ thấp, cho phép trình biên dịch AOT xử lý hoàn toàn.

Bước Tiếp Theo

Với việc backend Metal đã được tích hợp, ba backend chính của Zenith.NET hiện đã sẵn sàng. Các ưu tiên tiếp theo bao gồm:

  • Tích hợp SkiaSharp: Tăng tốc render 2D bằng backend GPU.
  • Ổn định hóa API: Hướng tới phiên bản 1.0.

Zenith.NET là một dự án mã nguồn mở và chào đón sự đóng góp:

  • GitHub: github.com/qian-o/Zenith.NET
  • Trang tài liệu: qian-o.github.io/Zenith.NET
  • NuGet: Tìm kiếm `Zenith.NET`

Thẻ: dotnet metal Graphics GPU Abstraction

Đăng vào ngày 1 tháng 8 lúc 16:40