站点链接:技术博客页 | 公众号:溯光的图形笔记
切口:上一篇把顺序算完了,可顺序要落到哪张桌子上
上一篇我们跟着 RenderingDevice(渲染设备)那条线走到收口,末尾留下一句结论:一张无环渲染依赖图会把「该按什么顺序发」算出来。那一篇把「图」当成一个已经存在的东西来用——它讲了图怎么建、依赖从哪来、屏障为什么能变少,没说的是:这张图算出来的顺序,最后要交给谁、交给的是同一种东西吗。
这一章问的就是这个。看着像实现细节,其实是「一次绘制怎么落到 GPU 上」这句话里最容易被跳过的一段:图只管「先做什么后做什么」,至于「这件事在该后端上到底怎么表达」,它一句都不管。
0.1 本系列的位置:从「顺序」往下走到「桌子」
好了,前面七篇把一次绘制的落点链走了一大半:对象模型(第二篇)→ 服务层接口(第三篇)→ 画布剔除与排序(第四篇)→ 画布渲染抽象与中间表示(第五篇)→ 批处理(第六篇)→ RenderingDevice 与渲染图(第七篇)。本篇接在第七篇后面,往同一条链的再下游走一步:第七篇讲「顺序是怎么算出来的」,本篇讲「同一份顺序落到四张不同的桌子上,会变成什么」。
整个系列按这条链路铺开,目录如下(本篇即第八行):
| # | 篇目 | 它回答的问题 | 状态 |
|---|---|---|---|
| 01 | 全景与边界 | 四层架构是什么、一次 2D 绘制要经过谁 | 已发布 |
| 02 | 对象模型 | 谁在什么时候把命令层下发 | 已发布 |
| 03 | 服务层接口 | 命令长什么样、怎么被排队 | 已发布 |
| 04 | 画布剔除与排序 | 画什么、按什么顺序 | 已发布 |
| 05 | 画布渲染抽象 | 绘制项里存了什么、怎么被提交 | 已发布 |
| 06 | 批处理 | 一批能装多少、按什么切、代价在哪 | 已发布 |
| 07 | RenderingDevice 抽象 |
命令列表怎么变成一张依赖图 | 已发布 |
| 08 | 后端分叉(本篇) | 四条驱动怎么选、分叉长在哪几处 | 本篇 |
| 09 | 主循环与多线程 | 逻辑与渲染怎么错位 | 待写 |
| 10 | 资源侧 | 一张纹理从盘上到显存 | 待写 |
| 11 | 收官 | 这条链整体买到了什么 | 待写 |
好了,其中:这张表的作用是标出本篇与相邻两篇的接缝——第七篇交出一套「该做什么、按什么顺序」的账,下一篇(主循环与多线程)接手「这套账在何时被谁算」。本篇要讲的,就是夹在这两句话中间的那一段:同一套账,在四个不同的图形 API 上各自怎么落地、各自的取舍写在哪。
0.2 口径先对齐:三种渲染方法、四种驱动、七种组合
对齐几个词,免得后面对不上。官方把这件事分成两层:渲染方法(rendering method)是三种——Forward+、Mobile、Compatibility;渲染驱动(rendering driver)是四种——Vulkan、Direct3D 12、Metal、OpenGL。理论上可用的组合一共 7 种(三种方法里 Forward+ 与 Mobile 各配三种驱动、Compatibility 只配 OpenGL)。
但「理论上 7 种」和「你这台机器上能选几种」是两件事。这不是修辞,是引擎启动时会直接把不合法的那一种拒掉。下图是这一拒绝的原文:
if (!valid_combination) {
OS::get_singleton()->print("Invalid renderer/driver combination '%s' and '%s', aborting. %s only supports the following drivers ", rendering_method.utf8().get_data(), rendering_driver.utf8().get_data(), rendering_method.utf8().get_data());
for (int d = 0; d < available_drivers.size(); d++) {
OS::get_singleton()->print("'%s', ", available_drivers[d].utf8().get_data());
}
OS::get_singleton()->print(".\n");
goto error;
}
其中:这段的用处不是吓人,而是给出本篇的第一个判据:「两种东西配不配」不是运行时商量的,是启动时一次性判定的;而「哪些配得上」的清单,是在构建期用编译宏拼出来的(第四章会把那段贴全)。所以本篇不写「引擎支持 7 种组合」这种话,只写「引擎的白名单里可能有哪些」和「本机实际是哪几种」。
0.4 先把七种组合列全,再归类
上面说得比较抽象,这里按「全量清单 → 二次归类」两趟走。先列全:三种渲染方法 × 可用驱动,一共七种:
| 渲染方法 | 可选驱动 | 条数 |
|---|---|---|
| Forward+ | Vulkan / Direct3D 12 / Metal | 3 |
| Mobile | Vulkan / Direct3D 12 / Metal | 3 |
| Compatibility | OpenGL(桌面为 OpenGL 3.3,移动为 OpenGL ES 3.0,Web 为 WebGL 2.0) | 1 |
再归类,按三条不同的尺子各切一遍——这正是本篇的骨架:
| 尺子 | 切出来的两组 | 用在哪一章 |
|---|---|---|
| 走不走第三层 | 走 RenderingDevice(Forward+、Mobile 六种)/ 不走(Compatibility 一种) |
一、八章 |
| 谁在答能力位 | 三个现代后端各答各的(Vulkan/D3D12/Metal)/ 根本不参与 | 九章 |
| 默认值在哪一档 | 引擎内置 / 构建期是否编进 / 新建工程写什么 | 四、五、六章 |
其中:同一批七种组合,三把尺子切出三组不同的「两半」。这件事本身值得留意——分叉不是一条线,是一把扇子;所以后面每讲一处取舍,都要先说清「是按哪把尺子在切」。
0.5 本篇的取证方式与三条读法约定
本篇的材料分三层,缺一层就会留下缺口:官方文档定口径(三层概念、特性矩阵、命令行开关)、源码定落点(main.cpp 的白名单、两张能力表、四个 drivers/ 目录)、本机定实况(三档启动行与读数)。三层的分工是固定的:文档说「设计上是这样」,源码说「代码里确实这样」,本机说「这台机器上跑出来是这样」。
另外三条读法约定,请在读完全篇时记住:
| 约定 | 为什么 |
|---|---|
| 行号属于某个提交,不属于版本号 | 本篇所有行号按 master 快照 2026-10-09,逐条回本机 clone 复核,不做算术平移 |
| 本机读数一律写明前提 | 引擎为自建版本、必须带窗口、关垂直同步;少了任一条读数就不可比 |
| 测不了就写「测不了」 | 本篇有一档根本起不来、有一档在本机跑不了——两条都如实入表,不用别的数据填空 |
本章出处:源码 main.cpp(:2592–2642,本机 master 快照 2026-10-09)。
0.3 三个可检验命题
本篇只回答三个问题,每个都要能落到可核对的东西上:
好,第一个是「分叉长在哪几处」。答案不是一处,是三处:编译期(宏决定编进哪些驱动)、工程设置(七个平台键 + 三个回退开关)、运行期(命令行参数与各后端自己的环境变量)。下图把这三处并排:
第二是「同一件事在四个后端上答得一样吗」。答案是不一样,而且保守方向都不一样:引擎里有两张「问后端」的能力表,一张有默认值、一张没有(第九章)。
第三是「代价写在哪」。答案分两半:一半在官方明确写出来的地方(比如 D3D12 与 Metal 的首帧着色器编译更慢),另一半在本机的启动行里——本篇会把本机的三档启动行原样贴出来,包括其中一档根本起不来这件事。
本章出处:编译期白名单见 main.cpp(:2592–2642)|工程设置见 ProjectSettings 类参考|运行期见 命令行教程。
一、三层概念:渲染方法、渲染驱动,以及那个「不是所有后端都进」的第三层
这一章要把三个名词钉死。钉不住的话,后面所有「默认是谁」「回退到谁」都是糊涂账。
1.1 官方怎么说这三层
官方文档在这一节写得比前几篇引到的更完整,逐字如下:
RenderingDevice 夹在哪一层、它只被谁用@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0RenderingDevice is a rendering backend, an abstraction layer between the renderer and the rendering driver.
It is used by the Forward+ and Mobile renderers, and these renderers are sometimes called
"RenderingDevice-based renderers".
说白了,其中:这句话把「渲染方法」与「渲染驱动」之间插进来的那一层点了名——它就叫 RenderingDevice,位置在渲染器与驱动之间。这解释了为什么「三种方法四种驱动」不能简单地两两相乘:第三层是中间人,而中间人可以缺席。
1.2 缺席的那一位:Compatibility 根本不进第三层
官方在内部架构文档里有一句更硬的表述,逐字:
RenderingDevice 上@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0Godot's renderers are built entirely on OpenGL and RenderingDevice, which is our abstraction over
Vulkan/Direct3D 12/Metal.
Only the Compatibility rendering method can be used with the OpenGL driver.
好了,其中:这两句合起来是本篇的题眼。它们说的是:引擎的渲染器要么搭在 OpenGL 上,要么搭在 RenderingDevice 上——前者只有一条路(Compatibility),后者是三条路(Forward+ 与 Mobile,各可配 Vulkan/D3D12/Metal)。换句话说,四张桌子上只有三张共享同一层抽象,第四张连抽象层都不过。
这个结论后面会反复用到:第九章之所以能问「同一批能力位在四个后端上怎么答」,前提就是三个现代后端各答各的、第四个压根不参与。
1.3 三层画在一起
其中:这张图要记住的不是「3 × 4」这个乘法,而是一条断口:从渲染方法这一层往下看,三条走第三层、一条绕过它。所以后文只要出现「Compatibility 怎么样」,问的都不是「它比 Forward+ 差在哪」,而是「它少了一整层中间人,所以每件事都得自己实现」。三层这个概念的全部价值,就压在这一句上。
1.4 三层各管什么
三层的关系可以用一句「谁负责什么」说清,这是后面反复要用的框架:
| 层 | 它决定的事 | 说人话 |
|---|---|---|
| 渲染方法(rendering method) | 用哪套渲染结构、开哪些效果 | 「画成什么样」 |
RenderingDevice |
同一件事怎么被表达成 API 无关的命令 | 「怎么把话说成通用语」 |
| 渲染驱动(rendering driver) | 用哪个图形 API 把通用语翻出来 | 「说给谁听」 |
官方还有一句更完整的图注(就是那张三层示意图的说明文字),逐字如下:
The Compatibility renderer runs on the OpenGL driver. The Forward+ and Mobile renderers run on
RenderingDevice, which can use Vulkan, Direct3D 12, or Metal as a rendering driver.
其中:注意「拥有关系」是自上而下的:渲染器持有 RenderingDevice,RenderingDevice 持有一个驱动。所以「换驱动」不改变渲染器,「换渲染方法」才换渲染器。这一点解释了一个常见困惑:同一个项目在 Vulkan 与 D3D12 之间切换,画面通常是一样的——因为上面那两层没动,动的只是最底下「说给谁听」。
1.5 四个容易混用的词
官方文档里 renderer 这个词既指「方法」也指「对象」,所以先把四个词钉死:
| 词 | 指什么 | 本篇里的例子 |
|---|---|---|
| 渲染器(renderer) | 一个真的实现了渲染的对象 | RendererCompositor 的子类们 |
| 渲染方法(rendering method) | 配置里选的那个字符串 | forward_plus / mobile / gl_compatibility |
| 渲染驱动(rendering driver) | 某个图形 API 的实现 | vulkan / d3d12 / metal / opengl3 |
| 图形 API | 那套标准本身 | Vulkan / Direct3D 12 / Metal / OpenGL |
OK,其中:这四个词在后文会反复出现,混用一个就会把结论说错。最容易出错的是第二与第三个:「我换了渲染方法」和「我换了渲染驱动」是两件不同量级的事——前者换渲染器,后者只换最底下那层翻译。
本章出处:官方文档 Overview of renderers 与 Internal rendering architecture(本机 rst 快照 evidence/stage1/docs_renderers.rst.txt、docs_internal_rendering_architecture.rst.txt)。
二、官方那张能力矩阵:8 张子表,两处反直觉
官方文档里有一张完整的能力对照表,按列分成 Compatibility / Mobile / Forward+,一共 8 张子表(总体、光照与阴影、全局光照、环境与后处理、抗锯齿、StandardMaterial、着色器特性、其它)。这一章不抄全,只挑两处和直觉相反的地方。
2.1 表面上看,它就是一张「档次表」
先看三行,它们给人的第一印象是「低端—中端—高端」:
Rendering driver / Feature set / Advanced rendering features@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0Rendering driver | OpenGL. | Vulkan, Direct3D 12, or Metal.
Feature set | 2D and core 3D features. | Most rendering features. | All rendering features.
Advanced rendering features | ❌ No. | ⚠️ Yes, limited by mobile hardware. | ✔️ Yes.
OK,其中:这张表的三列确实是按「低—中—高」排的,所以很容易顺势得出「Compatibility 什么都缺」这个结论。但请注意它给的是三档符号(❌/⚠️/✔️),不是「差多少」的量化——这张表能告诉你「有没有」,不能告诉你「值不值」。下面三行就是专门用来打这个结论的,看完之后再回来看这一节,感觉会不一样。
2.2 反直觉之一:Compatibility 有 SSAO,Mobile 反而没有
RenderingDevice access——两处反直觉所在@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0Screen-Space Ambient Occlusion (SSAO) | ✔️ Supported. | ❌ Not supported. | ✔️ Supported.
Compute shaders | ❌ Not supported. | ⚠️ Supported, but comes with a performance penalty on older devices. | ✔️ Supported.
RenderingDevice access | ❌ Not supported. | ✔️ Supported. | ✔️ Supported.
说白了,其中:第一行是这场「档次」直觉的反例:最省的那一档有屏幕空间环境光遮蔽,中间那一档反而没有。可见缺什么不是按档次排的,是按「实现成本」排的——SSAO 在 Compatibility 那条路上有直截了当的做法,在 Mobile 那套受限的渲染结构里不值当。
2.3 反直觉之二:RenderingDevice access 是「用户可见能力」
第三行更值得单独说。它写着 Forward+ 与 Mobile「✔️ Supported」、Compatibility「❌ Not supported」——这不是一句内部实现说明,而是承诺给使用者的能力。
也就是说,如果你在项目里直接用 RenderingDevice(自建计算管线、自己走一遍 RD 的接口),那么选 Compatibility 的那一刻,这段代码就没有立足之地了。这正好把第一章那句「Compatibility 不进第三层」从概念翻译成了后果。
其中:这两条合起来给本篇定了一个基调:讨论「哪个后端更强」没有意义,讨论「哪一个组合满足你这套功能」才有意义。
2.4 另外几张子表里,还有几行值得一并记住
只看上面两组容易漏掉「缺的清单长什么样」。再补几行,它们把「按成本排、不按档次排」这条规律坐实:
Lighting approach | Forward single-pass. Lights with shadows use a multi-pass approach and less accurate blending. | Forward single-pass. | Clustered forward.
Custom post-processing (CompositorEffects) | ❌ Not supported. | ✔️ Supported. | ✔️ Supported.
Sub-surface scattering | ❌ Not supported. | ❌ Not supported. | ✔️ Supported.
VoxelGI / SSIL / SDFGI / Volumetric Fog | ❌ Not supported. | ❌ Not supported. | ✔️ Supported.
MSAA 2D / TAA / FSR2 / FXAA | ❌ | ❌ | ✔️
说白了,其中:这张补充表里有两条值得单独记。第一条是 Lighting approach 那一格:Compatibility 是「forward 单趟,但带阴影的灯走多趟、混合精度更低」——官方在别处专门解释过原因:它的光照计算是在 sRGB 编码后的颜色上做的,不是在颜色空间里做的,所以结果会偏。这不是省事的偷工,而是那一代 API 能力决定的。第二条是「缺的清单」的形状:Compatibility 缺的是后处理、全局光照、抗锯齿这一整片,偏偏不缺 SSAO——又一次说明这张表不是按档次排的。
2.5 这张表该怎么用
其中:把它当筛选器,不当排行榜。正确的用法是三步:先在目标平台上确定渲染方法,再用这张表核「我需要的功能列里有没有 ❌」,最后才去选驱动。反过来做——先选驱动再找功能——一定会撞上「设备支持、方法不支持」这种最难查的组合。
2.6 这张矩阵没告诉你的事
它很有用,但边界也要说清,否则容易把它当全部:
| 它不讲 | 后果 |
|---|---|
| 代价 | 只有 ❌/⚠️/✔️ 三档,没有「慢多少」——代价要去别处读(本章 10.2) |
| 设备差异 | 同一格在不同硬件上可能不同,比如 Mobile 的 compute「在旧设备上有性能代价」 |
| 驱动差异 | 三列是渲染方法,不是渲染驱动 ⇒ 「Direct3D 12 与 Vulkan 差在哪」这张表一格都没写 |
其中:最后一行最要紧。这张矩阵回答的是「选哪套渲染方法」,不是「选哪个驱动」。两件事在文档里是分开的,本篇也就必须分开写:二章讲方法,九、十章讲驱动。
本章出处:官方文档 Overview of renderers · Feature comparison(本机快照 evidence/stage1/docs_renderers.rst.txt,逐行取自第 167/187/267/341 行附近)。
三、版本节奏:分叉不是一次设计出来的
有一种误读是「引擎一开始就规划了四条驱动」。看时间线就知道不是。
3.1 4.3:先加 D3D12
4.3(2024-08-15)的发布说明里,D3D12 是作为一个新驱动被介绍的:
#### New rendering driver supported — Direct3D 12
Since Direct3D is preferred by Windows, the addition of this new rendering driver improves compatibility
with any Microsoft platform. ... Until now, you had to build a custom Godot version yourself and include
the DXIL.dll library in your project in order to use Direct3D 12. This complication stemmed from the file
being proprietary ... Thanks to Microsoft open-sourcing the DXIL validator hash last month, this is not a
problem anymore ...
好了,其中:这段里最值得记住的是为什么以前没有:不是技术做不出来,是那个校验器是专有文件、不能随引擎分发。所以 D3D12 能进主版本,一半功劳在法务。同页还有一句可以一并记住——4.3 这一版把 Compatibility 那条路「视为功能完整」了。
3.2 4.4:再加 Metal
一个版本之后,Metal 进来了。它的官方描述里有「更快」这两个字,但同一段还带着一条限定:
#### Metal rendering backend
This direct implementation is more efficient and allows greater control over features and performance
trade-offs in the future. Early results have shown that this backend is at least as fast as Vulkan and
in many cases much faster on Apple hardware. ... Note that Metal support is currently limited to Apple
Silicon (ARM) devices.
其中:注意最后那句限定——当时的 Metal 只支持 Apple 芯片(arm64),Intel Mac 不在内。这类限定必须一并转述,否则「Mac 上 Metal 更快」很容易被读成「所有 Mac 都快」。本篇沿用本系列的一条口径:官方给的每一个「更快/更好」,都要连同它的小字一起搬过来,因为小字往往划定了适用边界。
3.3 之后两次:换默认、推版本
再往后是两次性质不同的改动。4.6 dev5(2025-12-01)宣布:Windows 上把 D3D12 设为默认(这一条第六章要专门拆,因为它比标题复杂)。4.7 dev1 则是把两个驱动各往前推了一版——Vulkan 侧更新到 SDK 1.4.335.0,Metal 侧在 4.7 之后开始用 Metal 4。
其中:四个版本、四次改动,合起来才是今天这张「三方法 × 四驱动」的表。分叉不是设计出来的,是四年里一段一段挤出来的——这也是为什么它的默认值会散落在三个地方(第六章)。
3.4 四支 PR 的账
时间线背后是四支改动。它们的元数据本身就是一份「分叉是怎么长出来的」的台账:
| PR | 标题 | 作者 | 合并 | 里程碑 |
|---|---|---|---|---|
| #70315 | Add Direct3D 12 rendering driver (Mesa NIR approach) | RandomShaper | 2023-12-12 | 4.3 |
| #83452 | Split RenderingDevice into API-agnostic and RenderingDeviceDriver parts |
RandomShaper | 2023-12-20 | 4.3 |
| #88199 | Add Metal support for macOS (arm64) and iOS | stuartcarnie | 2024-08-21 | 4.4 |
| #113213 | Windows: Make Direct3D 12 the default RD driver for new projects | akien-mga | 2025-11-27 | 4.6 |
其中:请注意前两支都在 4.3,而且顺序很关键——#83452 先把抽象层拆开(把「与 API 无关的记账」和「与 API 相关的实现」分成两半),#70315 才把 D3D12 塞进来。#83452 的自述把这件事说得很清楚:
A huge chunk of RenderingDeviceVulkan is API-agnostic bookkeeping, validation, etc. Such code is also
shared with others, like RenderingDeviceD3D12. This PR keeps a single, non-abstract RenderingDevice
with all those API-agnostic elements and moves all the API-specific parts to a thin wrapper around the
driver, which is RenderingDeviceDriver for Vulkan. This makes much easier to write additional rendering
drivers, by allowing one to focus on the API-specific stuff.
换句话说,「再加一个后端」这件事之所以在 4.3 之后变便宜了,是因为先把地基拆层了。而 #70315 那边也交代了另一处取舍:D3D12 那条路不用「渲染通道(render pass)」这套 API——用 Vulkan 的说法就是走了 dynamic rendering,好处是简单,代价它也承认了「came with a couple of downsides」。至于 Metal,#88199 的自述把边界写得很直白:「should support all of Godot's 2D and 3D features, except multiple viewports」——多视口不支持是它当时的已知缺口。这三条都不是「谁更强」,而是各自带着自己的缺口进来的。
3.5 为什么版本节奏值得单列一章
其中:因为「分叉」这个词容易给人一种一次性设计的错觉。把四个版本排开后才有两个发现:第一,每次加进来的后端都带着自己的缺口(D3D12 早年要自带 DXIL.dll、Metal 最初不支持多视口),第二,换默认这件事比加后端更晚——4.3 就有 D3D12 了,可直到 4.6 才把它设成新工程的默认。加进来 ≠ 推荐用,中间隔了两个版本。这条时间差本身就是本章的结论。
本章出处:官方发布说明与开发快照:4.3、4.4、4.4 dev1、4.6 dev5、4.7 dev1(本机 HTML 快照 evidence/stage1/release43.html 等 5 份)|PR 元数据 evidence/stage1/pr{70315,83452,88199,113213}.json。
四、「怎么选」的源码答案:白名单是构建期拼出来的
前两章说的「7 种组合」是文档口径。真正决定「你能选哪几种」的,是 main.cpp 里的一段 #ifdef。
4.1 三段宏,拼出一张清单
引擎在校验组合之前,先按渲染方法把可用的驱动名收进一个数组:
#ifdef RD_ENABLED 下按渲染方法收驱动名——白名单是构建期用宏拼出来的@ godot master 快照(2026-10-09)原文截取 · MIT#ifdef RD_ENABLED
if (rendering_method == "forward_plus" || rendering_method == "mobile") {
#ifdef VULKAN_ENABLED
available_drivers.push_back("vulkan");
#endif
#ifdef D3D12_ENABLED
available_drivers.push_back("d3d12");
#endif
#ifdef METAL_ENABLED
available_drivers.push_back("metal");
#endif
}
#endif // RD_ENABLED
说白了,其中:这段代码里没有任何「查询」动作——它只是把编译时定好的宏展开成一串名字。也就是说:驱动能不能用,在你拿到二进制的那一刻就已经定了,运行期没有任何回旋余地。
4.2 另一条分支:那三个 opengl3* 与 dummy
同一次校验里还有两段,处理另外两种情况:
#ifdef GLES3_ENABLED 段:三个 opengl3* 与 dummy 分支@ godot master 快照(2026-10-09)原文截取 · MIT#ifdef GLES3_ENABLED
if (rendering_method == "gl_compatibility") {
available_drivers.push_back("opengl3");
available_drivers.push_back("opengl3_angle");
available_drivers.push_back("opengl3_es");
}
#endif
if (rendering_method == "dummy") {
available_drivers.push_back("dummy");
}
好了,其中:值得注意两件事。第一,gl_compatibility 名下有三个名字——opengl3、走 ANGLE 的 opengl3_angle、OpenGL ES 的 opengl3_es,它们各自只在部分平台成立。第二,dummy 也是一条正规分支:它不画东西,但它是 --headless 的落点,也是「渲染器还没起来」时引擎还能跑下去的原因。
4.3 运行期那张清单:在 --help 里
文档另外给了一套口径,说的是运行期怎么查:
--rendering-method / --rendering-driver / --gpu-index 三个开关@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0--rendering-method <renderer> Renderer name. Valid values are forward_plus, mobile, and gl_compatibility. Requires driver support.
--rendering-driver <driver> Rendering driver (depends on display driver). Use --help first to display the list of available drivers.
--gpu-index <device_index> Use a specific GPU (only available on the Forward+/Mobile renderers; run with --verbose to get a list of available devices).
其中:这三句里藏着两条本篇要用的线索。其一,--rendering-driver 的说明是「depends on display driver」——驱动清单挂在显示驱动名下,所以 --help 打印出来的那一行才是权威(本机那一行我们在第十一章会原样引用)。其二,--gpu-index 明说「only available on the Forward+/Mobile renderers」——又是一处「Compatibility 不在内」的官方确认。
4.4 从「文档说 7 种」到「本机能跑几种」
把四章的内容串起来,就是一条四段的过滤链:
其中:这条链的实用价值只有一句:「文档里能选」不等于「你的构建里能选」,而「构建里有」也不等于「这次运行能用」。第一刀在构建期,第二刀在显示驱动初始化,第三刀在命令行。本篇第十一章会给出一条真实的、被第三刀砍掉的名字。
4.5 三把刀各砍什么
把四章收一下口。三把刀砍的位置和后果完全不同:
| 第几刀 | 在哪 | 砍掉什么 | 砍掉之后的后果 |
|---|---|---|---|
| 第一刀 | 构建期(#ifdef) |
根本没编进去的驱动 | 永远选不到;回退开关也救不了(第五章) |
| 第二刀 | 显示驱动初始化 | 这个平台上不存在的驱动 | 列表里看不到,--help 就不会印它 |
| 第三刀 | 命令行参数 | 名字打错或与渲染方法不配的组合 | 直接 abort,不降级(本章开头那段报错) |
其中:三刀里最容易被忽略的是第一刀——因为它在源码里长得像一段无害的 push_back。但它的后果最重:其它两刀都只影响「这次能不能用」,第一刀影响「这个能力存不存在」。第五章那句「构建期关掉的话开关根本不管用」,说的就是这件事。
4.6 顺带一句:报错文本也是文档
其中:上面那段报错值得再读一遍——它不是「出错了」三个字,而是把可用清单逐个念出来(for 循环拼 '%s',)。这个写法在工程上很有价值:用户遇到「我按文档选了却起不来」时,不需要再查任何资料。本篇第十一章引用的本机那条输出(Valid options are 'vulkan', 'opengl3' and 'dummy'.)就是这段代码的产物——它不是我们整理的,是引擎自己打印的。
4.7 第二刀在哪:显示驱动自己的名单
「第二刀」这句话容易含糊,说具体一点:渲染驱动是挂在显示驱动名下的。引擎启动时先定显示驱动(Windows 上是 windows,无窗口时是 headless),由它报告自己支持哪些渲染驱动——这正是 --display-driver 那一行的来历。
windows : 渲染驱动 = vulkan / opengl3 / dummy
headless : 渲染驱动 = dummy
其中:所以「第二刀」其实不是引擎砍的,是显示驱动报的。这解释了两件事:① 为什么 --rendering-driver 的官方说明是「depends on display driver」;② 为什么 --headless 下再怎么选都只有 dummy——那条路上根本没有 GPU 渲染驱动可报。本篇第十一章引用的本机输出里,headless 那一格正好只有 dummy,与此完全一致。
本章出处:源码 main.cpp(:2592–2642)|官方文档 Command line tutorial(本机快照 evidence/stage1/docs_command_line_tutorial.rst.txt)。
五、七个平台键与三个回退开关
第四章说的是「能不能用」。这一章说「默认用哪个」,以及当首选不成立时会发生什么。
5.1 七个键:默认值是按平台写的
引擎在启动时注册了一批工程设置,其中与驱动相关的有七个平台键:
driver.windows 的内置默认仍是 vulkan@ godot master 快照(2026-10-09)原文截取 · MIT // RenderingDevice driver overrides per platform.
GLOBAL_DEF_RST("rendering/rendering_device/driver", "vulkan");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.windows", PROPERTY_HINT_ENUM, "vulkan,d3d12"), "vulkan");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.linuxbsd", PROPERTY_HINT_ENUM, "vulkan"), "vulkan");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.android", PROPERTY_HINT_ENUM, "vulkan"), "vulkan");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.ios", PROPERTY_HINT_ENUM, "metal,vulkan"), "metal");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.visionos", PROPERTY_HINT_ENUM, "metal"), "metal");
GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "rendering/rendering_device/driver.macos", PROPERTY_HINT_ENUM, "metal,vulkan"), "metal");
其中:三个可写进正文的结论。① 宏名里的 _RST 指的是「改后需重启」——驱动不是运行期能热切换的东西。② 默认值是按平台分的:Windows 与 Linux 是 vulkan,Apple 一家子是 metal,Android 只有 vulkan 一条。③ ★ 注意 driver.windows 的内置默认此刻仍然是 vulkan——这一点非常要紧,第六章会说清楚它和「4.6 之后新工程默认 d3d12」为什么不矛盾。
5.2 为什么把所有驱动都塞进 hint
上面那段上面还有一段注释,专门解释这件事:
// Always include all supported drivers as hint, as this is used by the editor host platform
// for project settings. For example, a Linux user should be able to configure that they want
// to export for D3D12 on Windows and Metal on macOS even if their host platform can't use those.
好了,其中:这段注释是第四章那条过滤链的官方注脚:编辑器要允许你为「别的平台」预配置,所以下拉框里出现的选项天然比本机能跑的多。「下拉框里有」是给人看的,「能不能跑」是给机器看的——两者不是一回事,这是本篇反复强调的一个区分。
5.3 三个回退开关
同一段里还定义了三个回退开关,默认全开:
fallback_to_* 定义,默认全 true——前两个互为备份@ godot master 快照(2026-10-09)原文截取 · MIT GLOBAL_DEF_RST("rendering/rendering_device/fallback_to_vulkan", true);
GLOBAL_DEF_RST("rendering/rendering_device/fallback_to_d3d12", true);
GLOBAL_DEF_RST("rendering/rendering_device/fallback_to_opengl3", true);
其中:三个名字看着像一条链,实际上方向是互相的。类参考里对前两个的说明分别是「Direct3D 12 不支持时回退到 Vulkan」与「Vulkan 不支持时回退到 Direct3D 12」——这是两个平级后端互为备份;只有第三个 fallback_to_opengl3 才是「连 RD 都不通时降到 Compatibility」。
5.4 还有一条更硬的规定
「互为备份」这个说法还有一个更硬的补充,藏在类参考的词条里:
fallback_to_vulkan: ... The fallback is always attempted regardless of this setting if Direct3D 12
(Windows) or Metal (macOS) driver support was disabled at compile time.
fallback_to_d3d12: ... The fallback is always attempted regardless of this setting if Vulkan driver
support was disabled at compile time.
说白了,其中:这两句说的是:如果某个后端在构建期就被关掉了,那么这个开关根本不起作用——引擎会直接回退过去。也就是说,第四章那把「构建期的刀」优先级高于本章这三个开关。类参考在 Linux 那条上把这个后果写得更直白:「If Vulkan was disabled at compile time, there is no alternative RenderingDevice driver.」
5.5 这七个键各自写的是什么
类参考把每个平台键的取值都写清了,其中几条很能说明「分叉是按平台算的」:
driver.ios : - metal (default), Metal from native drivers.
- vulkan, Vulkan over Metal via MoltenVK.
driver.macos : - metal (default), Metal from native drivers, only supported on Apple Silicon Macs.
On Intel Macs, it will automatically fall back to vulkan as Metal support is not implemented.
- vulkan, Vulkan over Metal via MoltenVK, supported on both Apple Silicon and Intel Macs.
driver.linuxbsd : Only one option is supported: - vulkan, Vulkan from native drivers.
Note: If Vulkan was disabled at compile time, there is no alternative RenderingDevice driver.
言归正传,其中:三条值得记住。① Apple 平台上的 vulkan 不是原生 Vulkan,是经 MoltenVK 把 Vulkan 翻译到 Metal 上——也就是说那一档本身就多一层翻译。② driver.macos 的说明里藏着一条自动行为:Intel Mac 上 Metal 没实现,会自动落到 vulkan——又是一处「回退」的实例。③ Linux 那条的 Note 最狠:「没有替代的 RenderingDevice 驱动」——这条正好是本章 4.5 那张表里「第一刀」的官方注脚。
5.6 还有一处同名不同物的分支:Compatibility 的 Windows 档
前五章讲的都是「基于 RenderingDevice 的渲染器」那一族。但 gl_compatibility 名下另有一整套同名键,其中 Windows 那一条值得单独看一眼:
gl_compatibility/driver.windows : - opengl3 (default), OpenGL 3.3 from native drivers.
- opengl3_angle, OpenGL ES 3.0 using the ANGLE compatibility layer
over native Direct3D 11 drivers.
说白了,其中:这里出现了一个容易混淆的组合:Compatibility 那一档的 ANGLE 选项,底下骑的是 Direct3D 11——不是本章主角 Direct3D 12。也就是说,Windows 上「降级到 OpenGL」这条路,走到底可能还是落在 Direct3D 上,只是版本不同。同类名字下面藏着不同世代的 API,这也是本篇要专门分开写「渲染驱动」与「图形 API」的原因。
本章出处:源码 main.cpp(:2366–2391)|类参考 ProjectSettings(本机快照 godot_src/ep08_doc__classes__ProjectSettings.xml,2026-10-09)。
六、「默认」这件事,分成了三层
上一章末尾那个矛盾要在这一章解决:博客说 4.6 起 Windows 默认用 D3D12,可源码里 driver.windows 的内置默认明明是 vulkan。
6.1 编辑器在建新工程时写一行
答案在编辑器里,不在引擎里:
get_initial_settings() 往新工程写 driver.windows = d3d12(上一条是 Jolt Physics)@ godot master 快照(2026-10-09)原文截取 · MIT// This is used to set better defaults for new projects without affecting existing projects.
// Keep the list alphabetically sorted.
HashMap<String, Variant> EditorNode::get_initial_settings() {
HashMap<String, Variant> settings;
settings["display/window/stretch/aspect"] = "expand";
settings["display/window/stretch/mode"] = "canvas_items";
settings["gui/common/auto_focus_strategy"] = Control::AutoFocusStrategy::STRATEGY_BALLOON;
settings["input_devices/joypads/ignore_joypad_on_unfocused_application"] = true;
settings["physics/3d/physics_engine"] = PhysicsServer3DManager::JOLT_PHYSICS_NAME;
settings["rendering/rendering_device/driver.windows"] = "d3d12";
settings["rendering/lights_and_shadows/multi_bounce_occlusion/enabled"] = true;
return settings;
}
好了,其中:这个函数叫 get_initial_settings()——它只在「新建工程」时被用,把几条值写进新工程的 project.godot。所以「4.6 之后 Windows 默认 D3D12」这句话的准确形式是:新工程的配置文件里默认写着 d3d12,而不是「引擎的默认值是 d3d12」。
顺带一个细节:紧挨着的上一行是 settings["physics/3d/physics_engine"] = PhysicsServer3DManager::JOLT_PHYSICS_NAME;——引擎里「新工程默认」这一档机制是共用的,物理引擎也走这条路(与本系列 T06 那篇对得上)。也请注意函数头上那两行注释:「给新工程设更好的默认、不影响已有工程」+「保持字母序」——前者正是本章的结论,后者是维护约定。
6.2 为什么要换
换默认不是审美问题。当时的 PR 自述写得很坦白:
Why? Mainly because Vulkan support on Windows is giving us a lot of headache, with GPU drivers often
being poorly maintained compared to their Direct3D 12 counterparts. We've seen many regressions in
Nvidia and AMD drivers that would outright break Godot and games released with it (e.g. #109378 still
ongoing). There are other issues such as Vulkan implicit layers which force us to use hacks to work
around their harmful impact on Godot.
其中:这段是「为什么分叉会一直长下去」的最好证据:不是引擎想多养一个后端,而是同一件事在不同驱动上的可靠程度不一样,于是需要一条退路。同一条自述里还有一句限定必须一起转述——这项改动的收益只落在「4.6 及之后新建的工程」上,此前的工程仍然保持 vulkan。
6.3 于是「默认」分成了三层
把第五章和本章合起来看,「默认」这个词在这件事上其实有三个互不相同的位置:
其中:三层分别是:引擎内置默认(main.cpp,仍是 vulkan)、构建期是否编进(platform/windows/detect.py,决定这个选项在不在名单里)、新建工程写什么(editor_node.cpp,写 d3d12)。三处不同步,就会出现「我以为默认是 X」的误判——这正是本篇把它单列一章的原因。官方在类参考的词条里用一句 Note 把最终口径封了口:新工程默认 d3d12,4.6 之前的工程保持 vulkan,建议手动切过去。
6.4 三处不同步会带来的三种误判
把这一章收成一张「误判清单」,三种都是实践中真会撞上的:
| 你看到的 | 你推断的 | 实际 |
|---|---|---|
设置里有 d3d12 这个选项 |
引擎默认就是 d3d12 | 那只是下拉框;内置默认仍是 vulkan |
| 博客说 4.6 起 Windows 默认 D3D12 | 我的 4.5 工程也会自动换 | 只有新建工程写 d3d12;老工程保持 vulkan,要手动改 |
| 我改了设置却没生效 | 设置项坏了 | 可能这个驱动在构建期就没编进去,设置项在名单外 |
OK,其中:三种误判对应三处默认值。记住「默认」在这件事上有三个位置(引擎/构建/新工程),这三种误判就都不会发生。
6.5 那到底该选哪个
把前面几章压成一个可用的选择流程——先按平台,再按需求,最后看风险:
| 情形 | 建议 | 依据 |
|---|---|---|
| Windows,新工程(4.6+) | 跟随默认 d3d12 |
editor_node.cpp 写的就是它(6.1) |
| Windows,老工程 | 想换就手动改 driver.windows |
类参考那条 Note(6.3) |
| macOS / iOS / visionOS | metal(默认即是) |
七个平台键(5.1) |
| Linux | vulkan(只有一个选项) |
七个平台键(5.1) |
| Android | vulkan(只有一个选项) |
七个平台键(5.1) |
| Web 导出 | gl_compatibility(唯一选项) |
官方明说还没有 WebGPU(1.6) |
需要自建 RenderingDevice / 计算着色器 |
不能选 Compatibility | 官方矩阵「RenderingDevice access ❌」(2.3) |
其中:这张表里有两句官方话需要并排看,否则容易被读成矛盾:4.3 的文档说「Vulkan is still recommended for most projects」,而 4.6 起新工程默认 d3d12。这两句其实不冲突——前者是对使用者的「推荐」,后者是工程模板里的「默认值」;一个是建议,一个是初始值。「默认」不等于「推荐」,这是本篇第六章最想说的一句话。
本章出处:源码 editor_node.cpp(:8503–8512)|PR #113213(本机 API 快照 evidence/stage1/pr113213.json)|类参考 ProjectSettings · driver.windows。
七、抽象层的两个世界:基类里就摆着 is_opengl()
前三章讲的都是「选」的动作。这一章换个角度:哪怕不选,抽象层本身也已经承认有两个世界。
7.1 一个不寻常的纯虚函数
RendererCompositor 是所有渲染器的基类。它长这样:
_create_func,以及只为一条分支准备的纯虚 is_opengl()/gl_end_frame()@ godot master 快照(2026-10-09)原文截取 · MITclass RendererCompositor {
private:
bool xr_enabled = false;
static RendererCompositor *singleton;
protected:
static RendererCompositor *(*_create_func)();
bool back_end = false;
static bool low_end;
public:
static RendererCompositor *create();
virtual RendererCanvasRender *get_canvas() = 0;
virtual RendererSceneRender *get_scene() = 0;
// …get_fog() / get_gi() / get_light_storage() / get_material_storage() / get_mesh_storage()
// / get_particles_storage() / get_texture_storage() / get_utilities() 一路纯虚…
virtual bool is_opengl() = 0;
virtual void gl_end_frame(bool p_swap_buffers) = 0;
// …end_frame / finalize / get_frame_number / get_frame_delta_time …
};
其中:三处值得停下来看。① _create_func 是一个函数指针静态成员——这是「注入式工厂」:具体造哪一个渲染器,由构建期决定往这个指针里塞谁。② back_end 与 low_end 两个布尔位,本身就是分叉的记号。③ 最要紧的是 is_opengl():基类里明摆着一个只为一条分支准备的纯虚函数,同一族里还有 gl_end_frame(bool)。抽象层从来不假装自己是平的。
7.2 一个基类,两条链
OK,其中:两条链共享的只有基类,再往下就各走各的。这张图是后文的索引:右边那条链在第九章逐项展开,左边那条链在第八章做一次机械核对。第八章之所以值得单独写一节,是因为「两条链共享的只有基类」这句话光看图还只能算「看起来像」——得有一条不带主观判断的核对方式,把它变成「可核对」的事实。
7.3 那个空槽由谁去塞
_create_func 与 create() 都声明在基类里,但赋值发生在哪,本篇看不到——它不在这个头文件里。
其中:这里按本系列的规矩如实交代:没有抓到赋值处的源文件,就不展示那几行代码。能确定的是两件事,都从上面那份声明本身读得出来:① _create_func 是静态函数指针(static),所以它是「全局唯一的一个槽」;② 它没有初始值(声明里没有 = nullptr),也就是说:这个槽在被塞之前是空的,谁先塞谁生效。这两条合起来足以说明「有哪些渲染器可用」这件事在抽象层里查不到——它只在构建期的注册处、以及 main.cpp 的宏列表里(第四章)出现。
本章出处:源码 renderer_compositor.h(本机 master 快照 godot_src/ep08_servers__rendering__renderer_compositor.h,2026-10-09;create() 的定义体在 renderer_compositor.cpp,本篇未抓取该文件,故不展示)。
本章出处:源码 renderer_compositor.h(本机 master 快照 godot_src/ep08_servers__rendering__renderer_compositor.h,2026-10-09)。
八、Compatibility 差的不是「实现」,是「层数」
第七章末尾留了一句:两条链共享的只有基类。这一章把它变成可核对的事实。
8.1 那条链顶上的类
Compatibility 那条路的入口是 RasterizerGLES3:
RasterizerGLES3 的成员清一色是 GLES3:: 命名空间——它不是同一个东西的另一种写法@ godot master 快照(2026-10-09)原文截取 · MIT#include "servers/rendering/renderer_compositor.h"
class RasterizerGLES3 : public RendererCompositor {
private:
uint64_t frame = 1;
float delta = 0;
double time_total = 0.0;
protected:
GLES3::Config *config = nullptr;
GLES3::Utilities *utilities = nullptr;
GLES3::TextureStorage *texture_storage = nullptr;
// …全部是 GLES3:: 命名空间下的存储类
};
其中:注意成员的类型前缀:清一色是 GLES3:: 命名空间里的东西。它继承的是通用的基类,但往下整套材料和工具都是自备的。这一行也是下一节那次核对的判据来源——正因为成员类型与三个现代后端完全不同名,才值得去机械地搜一遍;如果只是「实现细节不同、名字还通用」,那本节与下一节都不必写。
8.2 一条机械核对:零命中
光看类型前缀还不够硬,所以这里做一次机械核对——在这个文件里检索三个名字:
rasterizer_gles3.h 上原地检索三个名字:三个都 0 命中@ godot master 快照(2026-10-09)· 本机原地检索 · MITgrep -n "RenderingDevice\|rendering_device\|RenderingDeviceDriver" drivers/gles3/rasterizer_gles3.h
(无输出)
好了,其中:这条空结果比任何综述都硬:Compatibility 这条路上,连 RenderingDevice 这个名字都不出现。换句更严格的话说——它和三个现代后端共享的东西,只剩 RendererCompositor 这一个基类。核对的方法本身也可以复现:随便挑一个「如果走了 RD 就一定会出现」的名字,在文件里搜一遍,搜不到就是证据。
8.3 所以差的是层数
把这份核对翻译成结构图:
其中:三个现代后端的目录里各有一份 rendering_device_driver_<api>.*(驱动实现),而 drivers/gles3/ 里根本没有这个角色——它带的是一整套自己的光栅器(shader_gles3.*、shaders/、storage/、effects/、environment/)。这不是「同一个东西的四种写法」,是「三个走一层抽象、一个不走」。
顺带给出四个目录的体量对照(master 快照 2026-10-09 的目录清单):
| 目录 | 文件数 |
|---|---|
drivers/vulkan/ |
10 |
drivers/d3d12/ |
15 |
drivers/metal/ |
25 |
drivers/gles3/ |
15 |
8.4 它自备了哪些东西
把「自备」这件事逐项摊开,就能看清「层数不同」到底差在哪:
| 该有的角色 | 三个现代后端 | Compatibility |
|---|---|---|
| 驱动实现 | rendering_device_driver_vulkan.* / _d3d12.* / _metal.* |
没有这个角色 |
| 着色器容器 | rendering_shader_container_* |
shader_gles3.*(GLSL ES 源码编译) |
| 着色器源码 | 走 SPIR-V 容器 | shaders/ 子目录 |
| 存储(纹理/网格/材质) | 由引擎层统一提供 | storage/(GLES3 自备一套) |
| 后处理与效果 | 由 RendererRD 侧统一提供 |
effects/ + environment/ |
OK,其中:这张表想说明的不是「Compatibility 更土」,而是它替换掉的不是一层,是一整列。三个现代后端在「驱动实现」这一格各写各的,但上面几格共享;Compatibility 是从这一格到最上面一格全部自备。所以「给 Compatibility 加一个功能」和「给 Vulkan 加一个功能」,工作量根本不在一个量级——前者要改一整条自己的链,后者往往只是改一处驱动实现。
本章出处:源码 rasterizer_gles3.h 与目录清单 drivers/(本机 GitHub API 快照 evidence/stage1/dir_drivers*.json)。
九、两张能力表,四种答法
这一章讲本篇的机制核心。引擎里有两张「问后端」的表,好处坏处都摆得很清楚:一张有默认值,一张没有。
9.1 表一:api_trait_get(),基类兜底
第一张表叫 ApiTrait(15 项),取值的接口长这样:
api_trait_get() 非纯虚:基类给出「规范默认值」,末 4 项光追 ERR_FAIL_V(0)@ godot master 快照(2026-10-09)原文截取 · MITuint64_t RenderingDeviceDriver::api_trait_get(ApiTrait p_trait) {
// Sensible canonical defaults.
switch (p_trait) {
case API_TRAIT_HONORS_PIPELINE_BARRIERS:
return 1;
case API_TRAIT_SHADER_CHANGE_INVALIDATION:
return SHADER_CHANGE_INVALIDATION_ALL_BOUND_UNIFORM_SETS;
case API_TRAIT_TEXTURE_TRANSFER_ALIGNMENT:
return 1;
case API_TRAIT_TEXTURE_DATA_ROW_PITCH_STEP:
return 1;
case API_TRAIT_SECONDARY_VIEWPORT_SCISSOR:
return 1;
case API_TRAIT_BUFFER_CLEARS_WITH_COPY_ENGINE:
return true;
case API_TRAIT_TEXTURE_CLEARS_WITH_COPY_ENGINE:
return true;
case API_TRAIT_USE_GENERAL_IN_COPY_QUEUES:
return false;
case API_TRAIT_BUFFERS_REQUIRE_TRANSITIONS:
return false;
case API_TRAIT_TEXTURES_REQUIRE_LAYOUT_TRANSITIONS:
return true;
case API_TRAIT_TEXTURE_OUTPUTS_REQUIRE_CLEARS:
return false;
default:
ERR_FAIL_V(0);
}
}
其中:三件事。① 函数头上那行注释就是这套设计的自述:// Sensible canonical defaults.(合理的规范默认值)。② 注意签名——它不是纯虚函数(没有 = 0),所以基类可以给出实打实的默认值:数一下,11 项由基类兜底。③ 最后那个 default: ERR_FAIL_V(0); 是留给没人答的那几项的——ApiTrait 一共 15 项,减掉这 11 项,剩下 4 项光追类能力没有默认值,只有真正实现了光追的后端才答得上来。
9.2 四种答法
三个现代后端各自覆盖了一部分。第二张表我们放在 9.4,先看第一张表三个后端各自的答法:
api_trait_get:10 项覆盖,其中两处是条件表达式@ godot master 快照(2026-10-09)原文截取 · MITuint64_t RenderingDeviceDriverD3D12::api_trait_get(ApiTrait p_trait) {
switch (p_trait) {
case API_TRAIT_HONORS_PIPELINE_BARRIERS:
return barrier_capabilities.enhanced_barriers_supported;
case API_TRAIT_SHADER_CHANGE_INVALIDATION:
return (uint64_t)SHADER_CHANGE_INVALIDATION_ALL_OR_NONE_ACCORDING_TO_LAYOUT_HASH;
case API_TRAIT_TEXTURE_TRANSFER_ALIGNMENT:
return D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT;
case API_TRAIT_TEXTURE_DATA_ROW_PITCH_STEP:
return D3D12_TEXTURE_DATA_PITCH_ALIGNMENT;
case API_TRAIT_SECONDARY_VIEWPORT_SCISSOR:
return false;
case API_TRAIT_BUFFER_CLEARS_WITH_COPY_ENGINE:
return false;
case API_TRAIT_TEXTURE_CLEARS_WITH_COPY_ENGINE:
return false;
case API_TRAIT_USE_GENERAL_IN_COPY_QUEUES:
return true;
case API_TRAIT_BUFFERS_REQUIRE_TRANSITIONS:
return !barrier_capabilities.enhanced_barriers_supported;
case API_TRAIT_TEXTURE_OUTPUTS_REQUIRE_CLEARS:
return true;
default:
return RenderingDeviceDriver::api_trait_get(p_trait);
}
}
说白了,其中:这个「答案表」比任何文字都说明问题。① 有几处答案是条件表达式——HONORS_PIPELINE_BARRIERS 取决于设备有没有「增强屏障」,BUFFERS_REQUIRE_TRANSITIONS 干脆是它的反。也就是说:同一个后端的答案,在不同显卡上可能不同。 ② 这一档覆盖了 10 项,是三个现代后端里最多的。③ 注意它的 default 分支——不是自己造一个答案,而是回落到基类的默认值(return RenderingDeviceDriver::api_trait_get(p_trait);)。这条与第九章 9.4 里那张表的 default 正好相反,值得并排看。
9.3 第一张表的完整对照
enum ApiTrait(15 项)× 四个后端:覆盖数 Vulkan 6 / D3D12 10 / Metal 4@ 本系列绘制| 项数 | 谁在答 |
|---|---|
15 项 ApiTrait |
基类兜 11 项;Vulkan 覆盖 6 项、D3D12 覆盖 10 项、Metal 覆盖 4 项;光追那 4 项只有 Vulkan 答 |
其中:这张表的结论不是「谁覆盖得多谁强」,而是:抽象层的默认值是有立场的——它把「大多数后端大概都这样」的那些答案先写死,只让各后端去纠正自己不一样的地方。这是「逐项谈判」,不是「一刀切」。
9.4 表二:has_feature(),纯虚,且 D3D12 往保守走一步
第二张表叫 Features(16 项),取值的接口是纯虚的:virtual bool has_feature(Features p_feature) = 0;——没有任何默认值,每个后端必须逐项回答。
enum Features(16 项)× 四个后端,以及 D3D12 那道 default: return false;@ 本系列绘制bool RenderingDeviceDriverD3D12::has_feature(Features p_feature) {
switch (p_feature) {
case SUPPORTS_HALF_FLOAT:
return shader_capabilities.native_16bit_ops;
case SUPPORTS_FRAGMENT_SHADER_WITH_ONLY_SIDE_EFFECTS:
return true;
case SUPPORTS_BUFFER_DEVICE_ADDRESS:
return shader_capabilities.buffer_device_address_supported();
case SUPPORTS_IMAGE_ATOMIC_32_BIT:
return true;
case SUPPORTS_VULKAN_MEMORY_MODEL:
return false;
case SUPPORTS_POINT_SIZE:
return false;
case SUPPORTS_HDR_OUTPUT:
return true;
case SUPPORTS_GPU_MAPPABLE_BUFFER:
return misc_features_support.uma_supported || misc_features_support.gpu_upload_heap_supported;
default:
return false;
}
}
其中:这张表最有意思的地方是最后那个 default: return false;。同一份文件族里,两张表的保守方向是相反的:表一(ApiTrait)是「没人答就用基类的合理默认」,表二(Features)在 D3D12 这里是「没人答就算没有」。同一个作者、相邻文件,两种立场——抽象层不是一个统一意志,是一堆逐项谈出来的约定。
顺带一个可核对的例子:SUPPORTS_VULKAN_MEMORY_MODEL 在 D3D12 上是 false、在 Metal 上是 true。名字里带 Vulkan 的那个能力,在 Vulkan 之外的两个后端上答案完全不同——这正是「同一批问题、四份答案」的缩影。
9.5 三个后端各自纠正了哪几项
把「谁覆盖了哪几项」列出来,就能看出每个后端的性格:
| 后端 | 覆盖数 | 它特别纠正的几项 |
|---|---|---|
| Vulkan | 6 | HONORS_PIPELINE_BARRIERS 直接给 1(原生就遵守屏障语义);SHADER_CHANGE_INVALIDATION 走「不兼容集合 + 级联」;光追 4 项只有它答 |
| D3D12 | 10 | SECONDARY_VIEWPORT_SCISSOR → false;USE_GENERAL_IN_COPY_QUEUES → true;TEXTURE_OUTPUTS_REQUIRE_CLEARS → true;两项对齐按 D3D12 的常量 |
| Metal | 4 | HONORS_PIPELINE_BARRIERS → sync_mode == Barriers;TEXTURES_REQUIRE_LAYOUT_TRANSITIONS → false;BUFFER_CLEARS_WITH_COPY_ENGINE → true |
其中:这张表的价值在「纠正」两个字上。Metal 那一格里的 TEXTURES_REQUIRE_LAYOUT_TRANSITIONS → false 尤其典型:Metal 在架构上就不要求显式的布局转换,所以它必须去纠正基类那个「默认要求」的假设。覆盖数多不等于更麻烦,覆盖数多往往只是「和基类假设差得更远」。
9.6 两张表的差别为什么重要
其中:这两张表的差别不在项数(15 与 16),而在兜底责任归谁:表一「没人答就用基类的合理默认」,表二「没人答就当没有」。前者对新增后端友好(先跑起来再逐项纠正),后者更保守(宁可不做也不能做错)。引擎把这两种态度分别放在两张表上,说明它并没有想用一套规则统一所有情况——这也正是本篇想说的:分叉不是缺陷,是被承认并逐项处理的现实。
9.7 这两张表在代码里的位置
最后把两张表的位置说清,因为它决定了两件事的难度:
| 表 | 枚举声明在哪 | 取值函数在哪 | 新后端要动的地方 |
|---|---|---|---|
ApiTrait(15 项) |
rendering_device_driver.h(:907–923) |
基类默认值在 rendering_device_driver.cpp(:37–65) |
可以只覆盖需要纠正的几项 |
Features(16 项) |
rendering_device_commons.h(:1023–1041) |
纯虚 has_feature(),各后端自己实现 |
必须逐项回答 |
说白了,其中:「一张有默认、一张没有」这件事在代码里的表现,就是「一张的取值函数写在基类、另一张的声明后面跟着 = 0」。新写一个后端的体感因此差别很大:表一可以先跑起来(基类兜着),表二从第一行起就逃不掉——这也正好解释了为什么 Features 那张表只有 16 项、而 ApiTrait 是 15 项:项数相近,但门槛完全不同。
本章出处:源码 rendering_device_driver.h(enum ApiTrait,:907–923)、rendering_device_driver.cpp(基类默认值,:37–65)、rendering_device_commons.h(enum Features,:1023–1041)、rendering_device_driver_d3d12.cpp(:6079–6126)。
十、着色器怎么变成各后端能吃的东西
前三章讲的是「问与答」。这一章讲必须做的那件苦工:SPIR-V 怎么落到各自的着色器格式上。
10.1 三个容器,三种路径
三个现代后端各有一个 rendering_shader_container_*,职责是「把 SPIR-V 变成这个后端能吃的字节」。关键行并排如下:
NIR_ENABLED 与 sign_bytecode、Metal 的 spirv_msl.hpp@ godot master 快照(2026-10-09)原文截取 · MITVulkan :#include <thirdparty/misc/smolv.h>
// Store SPIR-V as is when debug info is required.
// Encode into smolv.
// Compress.
D3D12 :#define NIR_ENABLED 1
#include "drivers/d3d12/d3d12_godot_nir_bridge.h"
struct RenderingDXIL { … };
void sign_bytecode(…);
Metal :#include <spirv_msl.hpp>
#include <spirv_parser.hpp>
#include <Metal/Metal.hpp>
好了,其中:三条路径一句话概括:Vulkan 原样走(只做 smolv 编码与压缩);D3D12 要经过 Mesa 的 NIR 再到 DXIL(而且还要给字节码签名);Metal 要经 SPIRV-Cross 翻成 MSL 再编译成 Metal 库。越不像 SPIR-V 的后端,这条路上要写的代码就越多。
10.2 体量只是形状,代价要引官方
四个容器(含 Compatibility 的 shader_gles3)的文件体量如下——这是 master 快照 2026-10-09 逐字节量出来的:
| 后端 | 文件 | 头 + 实现(字节) | 合计 |
|---|---|---|---|
| Vulkan | rendering_shader_container_vulkan.{h,cpp} |
3,412 + 5,364 | 8,776 |
| Metal | rendering_shader_container_metal.{h,cpp} |
9,209 + 33,199 | 42,408 |
| D3D12 | rendering_shader_container_d3d12.{h,cpp} |
9,057 + 42,892 | 51,949 |
| Compatibility | shader_gles3.h(另有 .cpp) |
9,409 + … | —— |
其中:以 Vulkan 为 1,Metal 约 4.8 倍、D3D12 约 5.9 倍。★ 但这个倍数只说明「容器要写多少代码」,不能拿来说运行时代价。运行时代价的官方定性在另一处,原文如下:
pipeline_compilations 原文:D3D12 与 Metal 的首次着色器编译明显更慢,原因就是那道转换@ godot-docs 官方文档(本机 rst 快照)· CC BY 3.0This is especially the case when using Direct3D 12 or Metal, which have significantly slower initial
shader compilation times than Vulkan due to the conversion step required.
其中:这句才是「代价」的正式说法:D3D12 与 Metal 的首次着色器编译明显慢于 Vulkan,原因就是那道转换。两条证据一起用才完整——体量说明形状,官方原句说明代价;单用体量会被读成「代码多 5 倍所以慢 5 倍」,那是错的。
10.3 三条路画在一起
其中:图右下角留了一块空白,那是 Compatibility:它的 shader_gles3.h 里没有任何 SPIR-V 相关的包含——它压根不走这条路,直接编译 GLSL ES 源码。换句话说,前三家是「同一份 SPIR-V,翻译三次」;它那一家是「另起一份源码,另编一次」。官方那句「Core shaders are entirely different from the Vulkan renderer」,在目录结构上就是这么长的——不是形容词,是事实描述。
10.4 分叉还会长到运行期的环境变量上
着色器那条路是编译期与构建期的事。但 Metal 后端还有一处运行期的旋钮,它的构造函数里读三个环境变量:
if (String res = OS::get_singleton()->get_environment("GODOT_MTL_ARCHIVE_FAIL_ON_MISS"); res == "1") {
archive_fail_on_miss = true;
}
// Barriers is the default; "none" selects native hazard tracking.
// Unrecognized values intentionally keep the default.
if (String res = OS::get_singleton()->get_environment("GODOT_MTL_SYNC_MODE"); res == U"none") {
sync_mode = HazardTracking;
}
if (String res = OS::get_singleton()->get_environment("GODOT_MTL_SHADER_LOAD_STRATEGY"); res == U"lazy") {
_shader_load_strategy = ShaderLoadStrategy::LAZY;
}
其中:这段最值得看的是第二段——GODOT_MTL_SYNC_MODE=none 会把 sync_mode 改成 HazardTracking,而第九章表一里 HONORS_PIPELINE_BARRIERS 的答案恰好是 sync_mode == Barriers。也就是说:一个环境变量能改掉一个能力位的答案。分叉从编译期起步,一路长到「启动时的一个环境变量」——这是本篇能给出的最完整的尺度感。
10.5 三条路的代价各自长什么样
把散在前面几节的代价摆到一张表上,就能看出它们的形状完全不同:
| 后端 | 翻译路径 | 代价落在哪一段 | 官方怎么说 |
|---|---|---|---|
| Vulkan | SPIR-V 原样(+压缩) | 几乎没有转换成本;代价在别处(Windows 驱动维护) | —— |
| D3D12 | SPIR-V → NIR → DXIL(并签名) | 首次着色器编译(多一道转换) | 「significantly slower initial shader compilation times」(10.2) |
| Metal | SPIR-V → MSL → 编译 Metal 库 | 首次着色器编译(多一道转换) | 同上 |
| Compatibility | 无此路(直接 GLSL ES) | 没有转换成本,但功能清单最短(无计算着色器) | 矩阵里的 ❌(2.4) |
其中:这张表想说的是:代价不是「有没有」,而是「放在哪一段」。D3D12/Metal 把成本放在首次编译(之后能吃到管线缓存),Compatibility 把成本放在能力上(少一批功能)。一个换时间,一个换功能——两种代价不可能同时消掉,只能选自己更能接受的那一种。这也解释了为什么官方会说「每个组合都有自己的取舍,改之前尽量在所有渲染方法上都测一遍」。
本章出处:源码 rendering_shader_container_vulkan、rendering_shader_container_d3d12、rendering_shader_container_metal、rendering_device_driver_metal.cpp(本机 master 快照 godot_src/ep08_*)|官方文档 Pipeline compilations。
十一、本机对照:三档启动行,其中一档起不来
前面十章全部落在源码与文档上。这一章换成本机实测——而且第一件事就是如实记下一次失败。
11.1 探针怎么搭的
口径先说清楚,否则读数不可比:唯一自变量是命令行(--rendering-driver 与 --rendering-method),工程与场景完全不动——场景沿用第七篇那个(2000 个 MeshInstance3D 共享同一份球网格,加屏幕空间环境光遮蔽与辉光,窗口 1280×720、关垂直同步)。引擎是本机自建版本,不是 stable。
三档的启动行原样如下:
[1] --rendering-driver vulkan --rendering-method forward_plus
Vulkan 1.3.280 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 3060 Laptop GPU
[2] --rendering-driver opengl3 --rendering-method gl_compatibility
OpenGL API 3.3.0 NVIDIA 560.70 - Compatibility
[3] --rendering-driver d3d12 --rendering-method forward_plus
Unknown rendering driver 'd3d12', aborting.
Valid options are 'vulkan', 'opengl3' and 'dummy'.
其中:★ 第三档就是本篇最该被记住的一段输出。它不是「未测试」,是「起不来」——引擎不但拒绝了,还自己把可选项念了出来。这条输出值得完整引用两次:它同时证明了「本机这份构建里没有 D3D12」与「运行期清单只有三项」。
11.2 清单的第二份证据
第三档之所以是「构建期就没了」,还有一份更早的证据——--help 打印的那一行:
godot --help 的四个相关开关行:--display-driver 那一行只列 vulkan/opengl3/dummy@ 本机实测 · Godot 4.8.dev.custom_build.7177493c1(自定义构建)--display-driver <driver> Display driver (and rendering driver)
["windows" ("vulkan", "opengl3", "dummy"), "headless" ("dummy")].
说白了,其中:这一行是第四章那条过滤链在本机上的落点:文档说 7 种组合,构建期砍到 windows 只有三项,运行期就按这三项执行。★ 补一句诚实的口径:光看二进制里有没有 d3d12 这个字符串是不够的——本机二进制里 d3d12、D3D12、Direct3D 12 字样都在(它们也可能来自工程设置的选项名),判据只能是启动清单与实跑结果。
11.3 读数:三个数,三种记法
三档的读数如下(稳定段为后 240 帧,前 60 帧预热不计):
| 档位 | 平均帧时间 | draw call | 图元 | 对象 | 视口 GPU | 视频显存 |
|---|---|---|---|---|---|---|
| vulkan + forward_plus · 第一次 | 3.332 ms | 5 | 640,000 | 8,000 | 0.750 ms | 131.9 MB |
| vulkan + forward_plus · 第二次 | 2.907 ms | 5 | 640,000 | 8,000 | 0.666 ms | 131.9 MB |
| vulkan + mobile | 4.282 ms | 4000 | 640,080 | 4,000 | 0.678 ms | 83.5 MB |
| opengl3 + gl_compatibility | 6.560 ms | 8000 | 640,000 | 8,000 | 2.374 ms | 25.7 MB |
| d3d12 + forward_plus | ——(起不来) | — | — | — | — | — |
★ 这张表里最容易被误读的就是 draw call 那一列:5、4000、8000,差了三个量级。如果直接读成「Forward+ 的效率是 Compatibility 的 1600 倍」,那就错了。查源码后可以确认,这三档根本不是在数同一种东西:
// ① Forward+(render_forward_clustered.cpp :896–911)—— 只在「开了新的一批」时 +1
if (prev_surface != nullptr && !cant_repeat && prev_surface->sort.sort_key1 == surface->sort.sort_key1 && prev_surface->sort.sort_key2 == surface->sort.sort_key2 && …) {
//this element is the same as the previous one, count repeats to draw it using instancing
repeats++;
} else {
repeats = 1;
if (p_render_info) {
p_render_info[RSE::VIEWPORT_RENDER_INFO_DRAW_CALLS_IN_FRAME]++;
}
}
// ② Mobile(render_forward_mobile.cpp :974–977)—— 直接赋值
p_render_data->render_info->info[RSE::VIEWPORT_RENDER_INFO_TYPE_VISIBLE][RSE::VIEWPORT_RENDER_INFO_DRAW_CALLS_IN_FRAME] = p_render_data->instances->size();
p_render_data->render_info->info[RSE::VIEWPORT_RENDER_INFO_TYPE_VISIBLE][RSE::VIEWPORT_RENDER_INFO_OBJECTS_IN_FRAME] = p_render_data->instances->size();
// ③ Compatibility(rasterizer_scene_gles3.cpp :4055–4059)—— 每个元素 +1
if constexpr (p_pass_mode != PASS_MODE_DEPTH && p_pass_mode != PASS_MODE_MOTION_VECTORS) {
// Don't count draw calls during depth pre-pass or motion vector pass to match the RD renderers.
if (p_render_data->render_info) {
p_render_data->render_info->info[RSE::VIEWPORT_RENDER_INFO_TYPE_VISIBLE][RSE::VIEWPORT_RENDER_INFO_DRAW_CALLS_IN_FRAME]++;
}
}
好了,其中:三段的差别一目了然:Forward+ 数的是「合并后的批数」(2000 个同样的球被归并成极少的批,所以只有 5),Mobile 数的是「实例数」(直接赋值,所以是 4000),Compatibility 数的是「元素数」(每个元素加一,所以是 8000)。而且三者在定义上还有一份共同的说明——它们都会被 Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME 汇总,路径是「各 viewport 的可见 + 阴影 + 画布三格相加」。
所以这张表不能横向比。本篇把它列出来的用处是反过来的:它恰恰证明分叉有多深——连计量方式都分叉了。
11.4 归因边界
最后把口径写死,这三条本章一条都不能省:
其中:① 读数差是整条后端路(驱动 + 抽象层 + 渲染器)的合并结果,不能归因到其中任何一段;② Vulkan 两跑的差是 3.332 → 2.907 ms(约 13%)——这就是本机噪声底,小于它的差异不要当结论;③ D3D12 一档是「起不来」,不是「未测」,也不许用别的后端的数据填空;④ Metal 在本机(Windows)跑不了,Metal 侧的一切结论只从源码与官方文档读。
11.5 表里另外几列也别顺手读成结论
draw call 那一列是最唬人的,但另外几列也有坑:
| 列 | 数值 | 该怎么读 |
|---|---|---|
| 图元 | 640,000 / 640,080 / 640,000 | 三档几乎一致——同一份场景、同一批网格,这说明「画的东西」没变 |
| 对象 | 8,000 / 4,000 / 8,000 | 又是计数口径差异(Mobile 那档写的是实例数),不是「看到的物体少了」 |
| 视频显存 | 131.9 / 83.5 / 25.7 MB | 报告口径可能不同(RD 侧含内部缓冲与池),不能直接当「谁更省」 |
| 视口 GPU | 0.750 / 0.678 / 2.374 ms | 这是单看视口渲染的 GPU 时间,比整帧平均更能反映后端工作量 |
其中:「图元三档几乎一致」这一行反而最有用——它是这张表里唯一一个可以横向读的列,因为它量的是「同一批三角形」,跟后端没关系。其余几列都要先问「这个数是怎么算出来的」,再决定能不能比。
11.6 为什么要先量噪声底
Vulkan 那两跑差了 3.332 − 2.907 = 0.425 ms(约 13%)。这个数字的价值是给后面所有比较划一条线:任何小于 0.4 ms 的档间差异,在本机都不可辨。
其中:所以本篇不去排名「Vulkan 比 Mobile 快」这种话——帧时间那一列的档间差(约 1 ms)与噪声底(约 0.4 ms)太接近,不足以支撑结论。真正站得住的差异只有一个量级以上的:OpenGL 那一档的视口 GPU 时间(2.374 ms)是 Vulkan(0.666–0.750 ms)的三倍多,这个差距远超噪声底。先量噪声,再说差异——这是本篇第十一章唯一一条方法论。
11.7 这台机器代表什么、不代表什么
最后把「这台机器」的边界写清楚,免得读者把本篇的读数拿去当基准:
| 它代表 | 它不代表 |
|---|---|
| 一份 2026-07-03 自建、关掉了 D3D12 的 Windows 构建 | stable 版本(版本号是 4.8.dev.custom_build.…) |
| RTX 3060 Laptop + NVIDIA 560.70 驱动下的行为 | 别的显卡/别的驱动版本的行为 |
| 「同一场景、只换命令行」时的相对关系 | 绝对性能(场景是我们自己搭的压力场景,不是真实游戏) |
| 「运行期白名单只有三项」这一事实 | 「所有 Godot 构建都没有 D3D12」——那是这份构建的选择 |
其中:读法只有一句:把它当「一次可复跑的观测」,不当「一张基准表」。本篇所有读数都可以用 run_probes_ep08.ps1 在你自己的机器上重跑一遍——重跑得到不一样的结果是正常的,那说明差异真的来自环境,而不是我们算错了。
本章出处:本机探针 run_probes_ep08.ps1(仓内 content/blog/godot-engine-ep08-backend-divergence/evidence/probe/,含工程、场景与一键复跑脚本;原始日志 E_ep08_*.txt)|计数写入点源码 render_forward_clustered.cpp、render_forward_mobile.cpp、rasterizer_scene_gles3.cpp|定义链 renderer_viewport.cpp(:966–973)。
十二、收官:分叉的账怎么算
12.1 三句话把本篇收口
其中:第一句讲选:分叉长在三处(构建期的宏、工程设置的七个键与三个回退开关、运行期的命令行参数与各后端自己的环境变量),而且三处不同步——「默认」这个词在这一件事上有三层含义。第二句讲答:抽象层用两张表向四个后端要答案,一张有默认值、一张没有,两张表的保守方向甚至相反。第三句讲做:同一份 SPIR-V,三条翻译路径,第四条路压根不用它。
12.2 一个可直接照用的核对方法
本篇所有结论都可以用同一套动作复核,三步而已:
① 看构建期:这个二进制里到底编进了哪些驱动
→ godot --help # 打印 --display-driver 那一行
② 看运行期:这一次跑起来用的是哪条路
→ --verbose # 引擎自报的启动行:Vulkan/D3D12/OpenGL … - Forward+/Compatibility - Using Device #0
③ 看机制:为什么是这个答案
→ 回源码:main.cpp 的白名单、两张能力表的各后端覆盖处
其中:这三步的顺序不能颠倒。先看清单再看实况,最后才回源码——反过来很容易掉进「文档说支持 7 种,所以我的项目一定跑得起来」这类推断里。本机那句 Valid options are 'vulkan', 'opengl3' and 'dummy'. 就是第一步的现场产物。
12.3 交棒给下一篇
这条链走到本篇,「落到哪张桌子上」的问题算是答完了:桌子有四张,抽象层盖住了三张,而选哪张的规则散在三处。下一篇要换一个完全不同的问题:这套「该做什么、按什么顺序、落到哪张桌子」的账,是在什么时刻、被哪个线程算出来的?逻辑线程与渲染线程怎么错位、一帧里两边的边界画在哪——那是主循环与多线程的题目,也是本系列第九篇。
12.4 哪些结论会随版本变,哪些不会
最后交个底。本篇的结论里,有一批是结构性的(版本变了也大致成立),另一批是时点性的(会随版本翻新):
| 类型 | 例子 | 会变吗 |
|---|---|---|
| 结构性 | 「Compatibility 不走 RenderingDevice」「两张能力表一张有默认一张没有」「白名单按编译宏拼」 |
不易变——它们由架构决定,改起来要动地基 |
| 时点性 | 「Windows 新工程默认 d3d12」「Metal 从 4.7 起用 Metal 4」「Vulkan SDK 到 1.4.335.0」 |
会变——随版本推进,本篇给的是 2026-10 的口径 |
其中:读这类文章时最该带走的是结构那一半;时点那一半请随手核对一次——本篇的做法是「凡是会变的,都给出可复跑的核对动作」(12.2 那三步),而不是让你记住某个值。
12.5 本篇没有回答的三个问题
按本系列的惯例,边界也写出来——这三件本篇没有做,读者别从本篇里读出它们:
| 没答的问题 | 为什么没答 |
|---|---|
| 同一个着色器在四个后端上编译结果差多少 | 那要抓四份编译产物逐条比对,本篇只做了「路径与体量」的形状对照(10.2) |
| 三后端的画面对不对得上 | 官方只说「may look different」;要真对比得同场景逐帧比图,本篇没做 |
| WebGPU 什么时候能来 | 官方把它列为目标但未实现;本篇只如实登记这一状态(1.6),不做时间预测 |
其中:列这三条不是为了显得谦虚,而是因为它们正好是下一篇系列文章可能要接的活。把没做的写出来,比把做了的写夸张更有用——读者据此才知道哪些结论可以放心引用。
本章出处:本篇的取证方式与前七篇一致,全部落在可核对的材料上:官方文档与发布说明(本机 rst/HTML 快照)、源码(master 快照 2026-10-09,与第七篇的 2026-10-08 快照逐字节相同)、以及本机探针(含一次失败记录)。源码入口见 godot 仓库 main.cpp。
相关链接
官方文档
- Overview of renderers(三层概念与 8 张特性矩阵)
- Internal rendering architecture(「built entirely on OpenGL and RenderingDevice」)
- Command line tutorial(三个开关)
- Pipeline compilations(D3D12/Metal 首帧编译更慢的出处)
- ProjectSettings 类参考(七个平台键与三个回退开关,含 4.6 那条 Note)
官方发布说明与文章
- Godot 4.3 发布说明(D3D12 驱动加入)
- Godot 4.4 发布说明(Metal 后端)
- 4.4 dev1|4.6 dev5(Windows 默认换 D3D12)|4.7 dev1
相关 PR
- #83452(把
RenderingDevice拆出RenderingDeviceDriver) - #70315(D3D12 驱动,走 Mesa NIR)
- #88199(Metal 支持)
- #113213(Windows 上把 D3D12 设为新工程默认)
源码入口(master 快照 2026-10-09)
- main.cpp:七个平台键(
:2366–2391)、白名单校验(:2592–2642) - renderer_compositor.h
- rasterizer_gles3.h
- rendering_device_driver.h / rendering_device_driver.cpp / rendering_device_commons.h:两张能力表
- editor_node.cpp(
:8503–8512)
本机材料(可复跑)
- 探针工程与一键复跑脚本:
content/blog/godot-engine-ep08-backend-divergence/evidence/probe/(run_probes_ep08.ps1) - 原始日志:
E_ep08_vulkan_fp.txt、E_ep08_vulkan_fp_2.txt、E_ep08_vulkan_mobile.txt、E_ep08_d3d12_fp.txt、E_ep08_opengl3_compat.txt、E_ep08_help.txt、E_ep08_summary.txt
系列前篇
下篇预告(第九篇 · 主循环与多线程):下一篇换一个完全不同的问题——这套账是在什么时刻、被哪个线程算出来的? 我们会去读引擎的主循环:逻辑帧与渲染帧怎么错位、单线程与多线程两套配置下边界画在哪、为什么有的项目一开多线程就抖。
以上。本篇所有源码行号一律按 godot master 快照(2026-10-09),逐条回本机 clone 复核,不做算术平移;本机读数一律写明引擎为自建版本、必须带窗口与关垂直同步,且 D3D12 一档在本机起不来——这一条是实测结论,不是缺测。希望能对大家有所帮助。
With Best Wishes.