技术博客

渲染驱动多后端:Vulkan/D3D12/Metal 的统一入口

第 54 / 149 篇 · 2025-05-08 · godot · 渲染管线源码分析跨平台

前面 RD 抽象层那篇留了个尾巴:4.3 拆出 RenderingDeviceDriver(RDD)"给 D3D12/Metal 铺路"。这篇讲铺完的这条路:RDD 到底负责什么、三个后端各自的翻译策略有什么差异、以及为什么窗口表面创建(rendering context)也要跟着拆层。基于 4.3/4.4 源码。

RDD 的职责边界

RDD 的接口在 servers/rendering/rendering_device_driver.h,看完它的纯虚函数列表就明白它的位置:buffer/image 创建与销毁、内存映射、framebuffer 概念化描述(FramebufferID 是 attachment 数组而非 VkFramebuffer 句柄)、pipeline 的描述(VertexFormatID、RenderPrimitive、BlendState 这类 POD 结构)、命令缓冲录制(command_buffer_begin/end、command_buffer_draw/dispatch/copy)、fence/semaphore 原语。一句话:RDD 吃"平台无关的命令描述",吐"平台相关的驱动调用"。

边界划在三个"不"上:不认识 RID(那是 RD 的账簿层)、不认识场景语义、不做任何同步决策(屏障在 RD 层推导完,RDD 只执行)。回头看 4.3 之前的代码——上万行的 rendering_device.cpp 里混着 vkCreateBuffer——就能体会这个边界当初有多模糊,拆层本质是把"记账的"和"跑腿的"分到两个文件。

三个后端各自的文件:

drivers/vulkan/rendering_device_driver_vulkan.h/.cpp     ← 最老,从 RD 里原样搬出
drivers/direct3d12/rendering_device_driver_d3d12.h/.cpp  ← 4.4 起逐步落地
drivers/metal/rendering_device_driver_metal.h/.cpp       ← 4.4 起逐步落地

Vulkan 版是参考实现:接口几乎一一对应 vk*,RDD 层基本是透传加少量参数整理。剩下两个后端的工作量集中在概念阻抗匹配上,这是这篇的核心看点。

D3D12:资源状态的显式翻板

RDD 接口里有一个 CommandBufferID command_buffer_begin(...), 录制期资源被怎么访问 RD 是知道的(usage_scope 账簿),但 D3D12 要求的 resource state(COMMON / RENDER_TARGET / UNORDERED_ACCESS / PIXEL_SHADER_RESOURCE…)是 D3D12 特有的概念,Vulkan 的 image layout 也是。接口怎么抹平?

答案在 TextureView 与 barrier 的参数化上:RD 层把"访问类型 + 作用域"的枚举传给 command_buffer_barrier(),各后端自己翻译成自家概念。D3D12 后端维护纹理当前 state,每次 barrier 调用算增量翻板,攒批后翻译成 ResourceBarrier() 调用。这是纯簿记工作,但bug 密度高:漏翻一个 state 在 D3D12 上是 device removed 级别的硬错误,比 Vulkan 的 validation 报错狠。4.4 的 D3D12 后端在很长一段时间里标着 experimental,主要就是这类状态机翻板没跑全 workload。

另一个 D3D12 特有麻烦是 shader 编译:RD 全层只认 SPIR-V,D3D12 要 DXIL,中间得插一层 SPIR-V → DXIL 转译(DXC)。这意味着 4.4 的构建系统里同时养着 glslang 和 DXC 两条 shader 编译链,构建时间肉眼可见地变长——多后端的成本从来不只在前端代码。

Metal: argument buffer 与内存模型

Metal 后端的阻抗匹配点和 D3D12 不同。Metal 的资源绑定单位是 argument buffer(可编码进 buffer 的参数块),和 Vulkan descriptor set 的对应关系不是一比一:descriptor set 索引空间是静态声明的,argument buffer 是运行时编码的。RDD 的 UniformSet 接口到 Metal 侧要翻译成 argument encoder 的编码调用。Metal 还没有 Vulkan 的 storage image 直接对应物(用 texture2d<T, access::read_write> 近似),以及 Metal 的 command buffer 只能从 command queue 创建、和 swapchain 强绑定,命令录制的生命周期管理比 Vulkan 更刚性。

Vulkan 后端惯用的"小技巧"到 Metal 上要重推:比如 dynamic rendering(RD 不建 VkRenderPass,按 attachment 描述开始渲染),Vulkan 1.3 原生支持,Metal 天然就是这种模式——这一条反而是 Metal 后端最省心的地方。反过来 Vulkan 的 timeline semaphore 在 Metal 上要用 event/shared event 模拟,fence 语义有细微差异。

RenderingContextDriver:为什么窗口也要拆层

4.3 同时拆出来的还有 RenderingContextDriver(RCD,窗口表面创建层)。表面创建依赖 OS 窗口系统(Win32 HWND、X11/xcb、Cocoa NSView、Wayland surface),和多后端正交:Vulkan 在 Windows 上要 VkWin32SurfaceCreateInfoKHR,Metal 要 CAMetalLayer 挂到 NSView。如果把表面创建留在各 RDD 里,M 后端 × N 窗口系统就是 M×N 组合;拆出 RCD 后是 M+N。4.x 里 drivers/vulkan/rendering_context_driver_vulkan.* 与 Metal 对应文件并存,职责就是"给我一个窗口句柄,还你一个可渲染表面"。

统一入口的代价

RD → RDD → 三后端:统一入口与各层特有成本 RD RID 账簿 屏障推导 staging 管理 RDD ×3 概念翻译 状态机翻板 绑定编码 Vulkan D3D12 Metal 参考实现,透传为主 资源 state 翻板 + DXC argument buffer 编码 RCD 独立成层:窗口表面创建与后端正交,M×N 组合降为 M+N

多后端的隐性成本值得给项目规划提个醒。shader 编译链翻倍(glslang + DXC)只是显性的;隐性的是每个后端一套测试矩阵——D3D12 翻板 bug、Metal argument 编码 bug 都是 workload 相关的,CI 要跑游戏 demo 抓黑屏,这个成本摊到每次发版。对引擎用户,现实建议:跨平台项目的发版候选先用 Vulkan 后端过完功能测试,D3D12/Metal 作为平台默认项单独过一遍冒烟——别假设"RD 抹平了差异"就跳过后端级验证,RDD 抹平的是 API 形状,抹不平驱动脾气。

本文基于 Godot 4.3/4.4 源码:servers/rendering/rendering_device_driver.h(RDD 接口)、drivers/{vulkan,direct3d12,metal}/rendering_device_driver_*(三后端)、drivers/*/rendering_context_driver_*(RCD 层)。D3D12/Metal 后端的成熟度以对应版本发布说明为准。