Zenith.NET v0.0.7:Metal后端落地,.NET GPU抽象的跨平台旅程,能否一帆风顺?
摘要:从第一行代码写下 GraphicsContext.CreateDirectX12() 到今天 GraphicsContext.CreateMetal() 跑通全部测试,Zenith.NET 终于实现了最初的承诺——用同一套 .NET API
从第一行代码写下 GraphicsContext.CreateDirectX12() 到今天 GraphicsContext.CreateMetal() 跑通全部测试,Zenith.NET 终于实现了最初的承诺——用同一套 .NET API 覆盖三大图形后端。
这篇文章聊聊 Metal 后端的技术选型、架构设计,以及 Zenith.NET 作为一个 .NET GPU 抽象层的设计哲学。
为什么要做 Zenith.NET?
.NET 生态有不少图形相关的库——绑定层如 Silk.NET、Vortice,抽象层如 Veldrid、Evergine。但现有的抽象层要么停留在较旧的 API 版本(如 DX11/OpenGL),要么是商业引擎的一部分,难以作为独立的 GPU 抽象层使用。
Zenith.NET 的定位是:一个面向现代图形 API(DirectX 12、Metal 4、Vulkan 1.4)的轻量 GPU 抽象层,只做抽象、不做引擎,让开发者写一次代码、跑在所有平台上。
这就是 Zenith.NET 要做的事:
后端
策略
DirectX 12
Windows 独占,性能天花板
Metal 4
Apple 全平台,仅支持 Apple Silicon
Vulkan 1.4
跨平台兜底,覆盖 Linux/Android
三个后端不是互相替代的关系,而是各守一方——在每个平台上选最原生的那个 API。
Metal 后端:架构决策
为什么选 Metal.NET?
v0.0.6 的 release notes 里提到过,当时在 SharpMetal 和 .NET macios TFM 之间评估。最终选了 Metal.NET(NuGet 包 Metal.NET 2.3.0)——这是我在开发期间制作的绑定库。相比 SharpMetal,Metal.NET 提供更完善的 Metal 4 API 覆盖,并且所有接口都是类型安全的。
不过坦率地说,Metal.NET 基于 class 封装 Objective-C 对象,在 GC 方面会有一定开销。
整体结构
Zenith.NET 抽象
Metal 实现
GraphicsContext
MTLDevice + MTL4Compiler + MTLResidencySet
CommandBuffer
MTL4CommandBuffer + 双编码器(Render/Compute)
ResourceLayout
绑定槽位计数(Buffer/Texture/Sampler)
ResourceTable
MTL4ArgumentTable,通过 GPU 地址绑定资源
Pipeline
MTLRenderPipelineState + MTLDepthStencilState
SwapChain
CAMetalLayer + CAMetalDrawable
AccelerationStructure
MTLAccelerationStructure + 实例缓冲区
Metal 4 新特性的应用
Metal 4 引入了几个对抽象层至关重要的新特性,Zenith.NET 的 Metal 后端全面采用了它们。
MTL4ArgumentTable——这是 Metal 4 全新的资源绑定模型。相比旧版 Metal 需要逐个 setBuffer/setTexture/setSampler 绑定资源,Argument Table 允许将所有资源打包到一张表中,通过 GPU 地址一次性绑定。这与 Zenith.NET 的 ResourceLayout + ResourceTable 抽象天然吻合:
Zenith.NET
Metal 4
ResourceLayout
声明 Buffer/Texture/Sampler 槽位计数
ResourceTable
创建 MTL4ArgumentTable,填入 GPU 地址
SetResourceTable()
一次调用绑定整张表
MTL4CommandBuffer 采用双编码器模型——同一时刻只能有一种活跃编码器。CommandBuffer 默认开启 Compute 编码器,当用户开启渲染 Pass 时关闭 Compute、切换到 Render 编码器;Pass 结束后自动切回 Compute:
[Compute 编码器] → 开启 Pass → [Render 编码器] → 结束 Pass → [Compute 编码器]
这样设计的好处是:Compute 编码器始终可用于拷贝(Blit)和计算调度,用户无需手动管理编码器生命周期。所有拷贝操作都走 Compute Encoder 的 Blit 路径,统一了屏障语义。
MTL4Compiler 支持设备端编译——把 Slang 输出的 metallib IR 在目标 GPU 上编译为最终 ISA,比传统 offline 编译能更好地利用 GPU 特定优化。
Objective-C 内存桥接
Metal API 基于 Objective-C 运行时,返回的对象都是 autoreleased 的——出了当前 autorelease pool 就会被回收。在 .NET 的托管环境里,这是个隐蔽的坑。
解决方案是一个统一的桥接工具:
public static T Own<T>(Func<T> func) where T : NSObject
{
using NSAutoreleasePool _ = new();
return func().Retain();
}
所有从 Metal API 获取的对象都通过 NSAutorelease.Own() 包装,确保 Retain 延长生命周期,后续由 .NET 的 Dispose 模式释放。
Shader 编译:Slang 统一管线
三个后端共享同一套 Slang 着色器源码:
.slang 源文件
├─→ metallib (Metal Shader Library)
├─→ dxil (DirectX Intermediate Language)
└─→ spirv (SPIR-V for Vulkan)
开发者只需维护一份 .slang 着色器,编译到哪个后端由 Slangc.NET 自动处理。
光线追踪
Metal 后端完整支持硬件光线追踪:
BLAS/TLAS:标准的两级加速结构
实例缓冲区:CPU 可写的间接寻址,更新 transform 无需重建 TLAS
RayQuery:在任意着色器阶段内联查询,无需专用光追管线
这与 v0.0.6 移除 RayTracingPipeline 的决策一脉相承——统一用 RayQuery,三个后端的光追能力完全对齐。
设计哲学
只暴露共同能力
Zenith.NET 的核心原则是:采用最新 API 版本,只暴露三个后端共同支持的能力。平台特有的特性被刻意排除,以维护一致的跨平台体验。
这意味着你不会在 Zenith.NET 的 API 里看到 DX12 的 Enhanced Barriers、Vulkan 的 Push Descriptors 或 Metal 的 Tile Shading——这些都是某个 API 独有的。暴露出来只会让其他后端无法实现,破坏"一次编写、处处运行"的承诺。
对于硬件能力差异(比如并非所有 GPU 都支持光追),则通过 Capabilities 动态查询:
if (context.Capabilities.RayTracingSupported) { /* 光追路径 */ }
if (context.Capabilities.MeshShadingSupported) { /* Mesh Shader 路径 */ }
共同能力统一暴露,硬件差异动态检测——这是 Zenith.NET 和其他抽象层最大的区别。
每个平台用最原生的 API
Zenith.NET 不像 bgfx 那样用 Vulkan 覆盖所有平台。在 Windows 上用 DirectX 12,在 Apple 上用 Metal 4,在 Linux/Android 上用 Vulkan 1.4。
虽然上层只暴露共同能力,但每个后端内部都用对应 API 最地道的方式实现——不需要在一种 API 上模拟另一种 API 的行为模式。
AOT 友好
整个库从第一天就为 Native AOT 设计。没有反射、没有动态代码生成、没有 Activator.CreateInstance。Metal.NET 和 Silk.NET 底层都是 P/Invoke + 函数指针,AOT 编译器能完整处理。
下一步
Metal 后端落地后,Zenith.NET 的三大后端全部就位。接下来的重点:
SkiaSharp 集成——用 GPU 后端加速 2D 渲染
API 稳定化——向 1.0 迈进
Zenith.NET 是开源项目,欢迎关注:
GitHub:github.com/qian-o/Zenith.NET
文档站:qian-o.github.io/Zenith.NET
NuGet:搜索 Zenith.NET
本文由 AI 辅助生成,经作者审核校对。
