站点链接:技术博客页 | 公众号:溯光的图形笔记
切口:为什么"深度时灵时不灵"是可以指到源码的
好了,第六篇讲完 add-on 接口面之后,会在读者心里留下一个很自然的期待:既然接口都摊开了,那写个深度类效果应该是一路顺风了。 现实恰恰相反——中文圈里关于 ReShade 最深的一条经验是"深度这东西时灵时不灵"。
这句话本身没错,但它是症状,不是诊断。症状背后有四件事,每一件都能在官方仓库里指到具体的文件与行号:
- 自动关闭——深度会被"关掉",而且关它的代码不在深度那边;
- MSAA——开了多重采样之后,深度要么取不到,要么干脆整条链路缺席;
- UI 缺失——深度缓冲里天生就没有界面,于是效果会糊在 UI 上;
- 子矩形错位——同一个深度缓冲可能服务好几种分辨率,启发式会选错。
这四件事有一个共同点:它们都不是 bug,而是工程条件。 这一篇要做的,就是把"时灵时不灵"这句经验,翻译成四句可以核对的判断。
先说清楚这一篇和上一篇的关系。第六篇讲的是"接口面长什么样",其中有一样东西当时只提了一句就放下了:内置 add-on。官方在 5.0 发布说明里写过一句很关键的话——
internally only hooks a very limited set of graphics API functions, just enough to make the built-in depth buffer detection work in singleplayer games still.
那句话里"内置的深度缓冲检测"到底是什么、它凭什么"刚好够用",正是这一篇的第一、第二与第六章。
这一篇在系列里的位置也清楚:本篇占的是"深度这一类的工程条件"那一层——左边第五、六篇把抽象层与接口面讲完了,本篇往下钻一层,讲"这一类效果为什么会被条件卡住";右边第八篇讲效果生态与许可,第九、十篇才是自跑。(全系列的地图见下面 §0.1 那张表。)
读那张表的正确姿势是从上往下看:先接受"深度不是取一次就永远有"这个前提,再看下面四章各自把那件事钉在哪个文件上。边界先钉死,后面几章才敢往深里走——不钉死的话,写到 MSAA 就会滑进抗锯齿算法,写到 UI 就会滑进后处理链的顺序设计,那两件事都不属于本篇。
0.1 全系列目录总览(十一篇)
依然是惯例,先给系列地图。本系列沿"一个外部注入的运行时"从入口一路拆到效果生态,十一篇的落点如下(本篇即第七行):
| # | 篇目 | 这一篇要回答的那一问 |
|---|---|---|
| 一 | 总论:注入式后处理与管线内后处理的边界 | 它到底动的是哪一段?和引擎自带的后处理差在哪? |
| 二 | 代理 DLL、加载顺序与导出表挂钩 | 它凭什么进得来?进来之后怎么把调用接过去? |
| 三 | ReShade FX:语言血统、独立编译器与纹理语义 | 效果是拿什么写的?编译器为什么能单独拿出去用? |
| 四 | effect runtime:technique / pass 图、render state 与运行时数据源 | 一个 effect 从源码到画面,中间过了几道? |
| 五 | 跨 API 抽象:D3D9/10/11/12、OpenGL、Vulkan 的差异与统一 | 同一套东西怎么盖住六种图形 API? |
| 六 | Add-on API:事件模型、跨 API 资源交换与两种构建的分野 | 想在运行时插手更深的事,接口长什么样? |
| 七 | 本篇:深度缓冲的工程学——自动关闭、MSAA、UI 缺失与子矩形错位 | 为什么深度类效果时灵时不灵? |
| 八 | 效果生态与许可地图:同一份清单里的 MIT 与"禁止再分发" | 那些效果到底能不能用、能不能转? |
| 九 | 自跑一:注入本地 D3D11 demo——逐效果前后对比 | 真跑一遍是什么样子? |
| 十 | 自跑二:效果栈与帧耗账,外加自写一个小 .fx | 代价是多少?自己写一个要几步? |
| 十一 | 合规与工程取舍的合集:把十一篇的账并到一处 | 这条路线值不值得用、边界在哪? |
阅读路径:只想知道"我的深度为什么不动"的朋友,读第一章与第二章就够;想搞明白"MSAA 到底动了什么"的,第三章是核心;被"效果糊在 UI 上"困扰的,第四章直接回答。
0.2 本篇要讲清的一件事
先把结论放这儿:"深度时灵时不灵"不是一个可以"修好"的故障,而是四道各自独立的工程条件叠在一起的结果。 这四道条件是:
- 一道来自运行时策略:检测到网络流量就整体关掉 add-on(第二章);
- 一道来自图形 API 的限制:多采样深度必须先 resolve,而 resolve 需要设备能力(第三章);
- 一道来自渲染顺序的事实:界面在深度写入之后才画,所以深度里没有它(第四章);
- 一道来自检测算法与"子矩形"的冲突:一个深度缓冲可能服务多种分辨率,而"该取哪一次"只能靠启发式(第五章)。
这四道条件的层数完全不同——第一道在 runtime.cpp,第二道在图形 API 的能力位,第三道在游戏的渲染顺序里,第四道在 1,549 行的启发式代码里。把它们混在一起谈,就只能得到"时灵时不灵"这种没有信息量的结论;分开谈,每一道都能给出是/否。
这一篇还会补一处上一轮的尾巴:签名版的构建为什么恰好还能做深度检测。第六篇数出签名版在编译期禁掉了 30 个事件,而本篇会数出这个内置深度 add-on 需要 27 个事件——两者的交集是空的。这不是巧合,是设计。
0.3 本篇不涉及什么(边界声明)
依然是惯例,先把边界摆在前面:
| 不涉及 | 为什么 |
|---|---|
| 深度类效果的算法——SSAO、MXAO、DOF、光追 GI 怎么算 | 算法归后处理链那一系列;本篇只讲"这类效果拿深度时会遇到什么条件" |
| 抗锯齿算法本身——MSAA/FXAA/DLSS 的实现与画质对比 | 本篇只在"MSAA 如何影响深度缓冲的可取性"这个角度提它 |
| hook 机制本身——导出函数、vtable、inline hook | 归 T12《图形 API Hook 机制》;本篇只在讲"网络计数是怎么来的"时提一句 Winsock |
| 一个具体深度的调参教程——"点哪个选项就能让深度出来" | 那是使用向内容;本篇只解释每个选项在源码里对应什么 |
| 任何绕过做法——绕过深度限制、绕过网络检测、绕过反作弊 | 红线。本系列全部实验都在自有开源 demo 上做,商业游戏一次都不碰 |
参数口径也说一句:系列里所有"本机读数"都来自同一台机器——Windows 11(build 26200)、RTX 3060 Laptop(6144 MiB 显存)、.NET 运行时 4.8 以上。第七篇不新做实验,全部读数分两类:一类是对随篇归档的官方源码原件做离线解析,一类是对官方仓库文件清单做清单级核对。
0.4 四条读法约定(沿用本系列体例)
好了,写这个系列的过程中,有四条约定帮我们避开了四类常见误读,先摊在这里:
- 官方文字优先,二手转述只作线索。 凡涉及版本、行为与立场的说法,一律追到官网、发布说明或代码仓库原文;二手内容明标为二手。本篇特别要守这一条——"深度被网络检测关掉"这件事,社区帖里到处都在讲,但判定逻辑与阈值只写在源码里,本篇照源码写。
- "社区经验"与"源码事实"必须分开摆。 社区里最流行的三条经验是"关掉 MSAA""检查是不是在联网""试试反转深度"——它们都对,但都不是机制。本篇会把每一条对到它的源码落点上,并指出哪几条其实是对现象的误归因。
- "没复现"就是"没复现"。 本机做不到的,一律写"测不了",不拿别人的结论替自己作证。这一条在本篇尤其重要:本机没有签名版与 Addon 版两份二进制,所以"某个效果实际会怎么失败"这类运行期问题,本篇不给结论,只给"编译期被关掉"与"源码里怎么判"这两层。
- 读数的口径必须写在读数旁边。 本篇会出现很多"多少行""多少个事件""多少帧"——这些数全部由脚本从归档原件现读,不是估算、不是转述。凡是引用别人的数字,一律注明转述。
这四条在后面几篇里都会再用到。
一、深度检测住在哪:一个 1,549 行的内置 add-on
第一章先解决一个"位置问题"。因为后面四章都会说"这个条件在某某文件里",如果位置本身没定死,四章的证据就没有立足点。
1.1 清单级:405 个文件里只有 2 个
先看看结论:5.0 之后,深度缓冲检测不再是 ReShade 核心的一部分,而是一个内置 add-on。
这个结论不是"翻了几个目录没看到",而是清单级核对出来的。本机把官方仓库的扁平清单取回来,逐个文件名匹配 depth/z_buffer/msaa/multisample/resolve 五个词元:
405 个文件,命中 2 个——examples/09-depth/generic_depth_addon.cpp 与 examples/09-depth/generic_depth.vcxproj。而 source/ 那 72 个文件里,一个都没有。
为什么这件事值得单列一节? 因为它解释了一个常见的误判:很多排障帖会让人去 source/ 里找"深度检测的代码",然后一无所获。代码不在了——它被搬进了 examples/09-depth/,成了示例工程里的内置 add-on。 这个搬动就发生在 5.0:
Reworked depth buffer detection into a built-in add-on, improved the algorithm and made all features available for all APIs.
这句话来自 5.0 的发布说明(官方原文,本机亲核)。注意它同时说了三件事:重构成内置 add-on、算法改进了、所有 API 都能用了。第一件决定了本机要读哪个文件,后两件解释了为什么 5.0 之前"深度工作率低"而 5.0 之后明显变好。
1.2 这个 add-on 有多大
下面进入第一章的第二件事:把它量一下。本机对归档的原件逐行现数:
1,549 行——比第六篇那 100 个事件的 addon_event 头文件还长一点。它注册了 27 个 add-on 事件,而注册与注销的数目正好成对(27 对 27),这说明它对自己的生命周期管得很干净。
还有三个数值得先放这儿,第三章会用到:texture.samples > 1 这个判据在文件里出现 11 次,resolve_texture_region 这个调用出现 2 次,备份纹理的销毁时机是 destroy_after_frame = frame_index + 50(源码里那句注释逐字是 Destroy after 50 frames)。
那 50 帧是什么意思? 深度数据不是"取一次就完",而是要在 clear 之前把它拷一份出来存着,供后面所有效果用。拷出来的这份备份什么时候丢?等 50 帧之后——这是一个"保守到有点浪费"的选择:宁可多占一会儿显存,也不要因为提前释放而让某个效果读到已销毁的资源。
1.3 它注册了哪些事件
27 个事件是哪些?本机把它们逐条列出来(按文件里的注册顺序):
init_device · init_command_list · init_command_queue · init_effect_runtime
destroy_device · destroy_command_list · destroy_command_queue · destroy_effect_runtime
create_resource · create_resource_view · init_resource · destroy_resource
draw · draw_indexed · draw_or_dispatch_indirect · dispatch_mesh
bind_viewports · begin_render_pass · bind_render_targets_and_depth_stencil
clear_depth_stencil_view · reset_command_list
execute_command_list · execute_secondary_command_list · present
reshade_begin_effects · reshade_finish_effects · reshade_reloaded_effects
这份名单不是随便挑的,它对应"一个检测器需要知道什么"。 把它按用途分一下类,三类就清楚了:
| 组 | 包含哪些 | 检测器拿它做什么 |
|---|---|---|
| 生命周期(8 个) | init_*/destroy_* 的 device、command list、command queue、effect runtime |
建立与拆掉"进程级状态表":哪个设备在跑、哪些命令队列要跟踪 |
| 资源(4 个) | create_resource、create_resource_view、init_resource、destroy_resource |
认出哪些资源可能是深度缓冲:只要在创建时看一眼描述(格式、尺寸、用途) |
| 工作量与时机(12 个) | draw*/dispatch_mesh/bind_viewports/begin_render_pass/bind_render_targets_and_depth_stencil/clear_depth_stencil_view/reset_command_list/execute_command_list/execute_secondary_command_list/present |
判断"这一帧的主场景是哪一个":谁在画、视口多大、什么时候被 clear、什么时候呈上屏 |
| ReShade 专有(3 个) | reshade_begin_effects、reshade_finish_effects、reshade_reloaded_effects |
和 ReShade 自己的效果链对齐时点 |
这张分类表是这一篇的骨架之一:后面四章讲的四道条件,本质上都在问"这三组事件里,哪一组的信息不够用了"。
1.4 8 个配置键:这个 add-on 的调节面
言归正传,一个 1,549 行的组件对用户暴露几个旋钮?答案是 8 个,全在 ReShade.ini 的 [DEPTH] 段下。
旋钮的数目本身就是一条信息:这一类"需要用户自己试出来"的组件,通常会把可调的东西压到个位数——8 个就是这个检测器摆在明面上的全部调节面,没有第九个。本机把它们逐个现数出来:
这 8 个键可以二次归成三类,归类本身就把"检测器怎么工作"讲了一半:
| 类 | 键 | 它在管什么 |
|---|---|---|
| 拷贝时机 | DepthCopyAtClearIndex、DepthCopyBeforeClears |
什么时候把深度拷一份出来存着 |
| 筛选 | DrawStatsHeuristic、UseAspectRatioHeuristics、FilterFormat、FilterResolutionWidth、FilterResolutionHeight |
从几百个候选里挑哪一个当"主深度" |
| 格式策略 | DisableINTZ |
要不要禁用 Intel 的特殊深度格式(INTZ) |
第二类有 5 个键,占了多数——这本身就是一条信息:这个检测器的大部分复杂度都在"筛选"上。第五章会把这件事展开。
1.5 三组枚举:检测器的"词汇表"
说白了,筛选要有判据,判据用的就是枚举。本机把文件里的三组枚举逐字抄了出来:
enum class draw_stats_heuristic : unsigned int { prefer_vertices = 0, vertices, drawcalls };
enum class aspect_ratio_heuristic : unsigned int { none = 0, similar_aspect_ratio,
multiples_of_resolution,
match_resolution_exactly,
match_custom_resolution_exactly };
enum class clear_op : uint8_t { clear_depth_stencil_view, fullscreen_draw,
unbind_depth_stencil_view };
三组枚举各说一件事:
draw_stats_heuristic:判断"哪一份快照更像主场景"时,比顶点数还是比绘制次数?默认值是prefer_vertices,而它的实现是一个混合判据——源码里那句话写得很明白:如果间接绘制次数不到总绘制次数的三分之一,就比顶点数;否则比绘制次数(因为用了间接绘制时顶点数不准)。aspect_ratio_heuristic:宽高比检查做到多严?从"不检查"到"必须与自定义分辨率完全相等",一共五档。默认是similar_aspect_ratio。clear_op:clear 事件有三种类型,检测器对它们的处理不一样——这一点第五章会展开。
读到这儿,第一章的"位置问题"就有答案了:深度检测的全部逻辑住在 examples/09-depth/generic_depth_addon.cpp 这一个文件里,它有 1,549 行、注册 27 个事件、对外只有 8 个配置键,而它的复杂度主要集中在"从候选里挑一个"这件事上。
那"自动关闭"呢? 它不在这里。这是第二章要讲的、也是这一篇最反直觉的一处。
二、自动关闭:关它的代码不在深度那边
2.1 先纠一个直觉
上一章最后留了一句:自动关闭不在 generic_depth_addon.cpp 里。这不是推测——本机在那个文件里搜 network 与 multiplayer 两个词,零命中。
这一节的图就是那条链条的全貌:它从 Winsock 上的字节计数开始,到纹理语义被改绑成空视图结束;中间那三步——窗口、阈值、翻假——各自落在核心的哪个文件里,下面四处逐一展开。
所以正确的问法是: 既然关它的不是深度模块,那是谁?答案有两层:判定在核心的 runtime.cpp 里,而它的输入是 Winsock 上的字节计数。这两样都不在 examples/09-depth/ 里——一个深度效果失效,起因可以完全落在它自己的模块之外。
先把链条摆出来(这一段是本篇的第一个"四处源码",后面会逐处展开):
Winsock 的 send / recv 族被挂钩 → 每收发一次累加 g_network_traffic
↓
runtime.cpp 每帧检查:这一帧有没有流量?→ 有就给 traffic 计数加一
↓
每 61 帧结算一次:traffic < 10 ?
↓
否 → addon_enabled = false,并把所有纹理语义绑定改绑成空
2.2 第一处:Winsock 上的字节计数
先来看看链条的起点,它在 source/windows/ws2_32.cpp。本机现读到的第一条是:
volatile long g_network_traffic = 0;
这一个全局变量就是全部"网络检测"的数据源。它在 Winsock 的 send/recv 族函数里被累加——本机现数到 8 处 InterlockedAdd(&g_network_traffic, …)(八个 Winsock 钩子各一处):
InterlockedAdd(&g_network_traffic, lpBuffers[i].len);
InterlockedAdd(&g_network_traffic, *lpNumberOfBytesRecvd);
InterlockedAdd(&g_network_traffic, num_bytes_sent);
注意它累加的是"字节数",不是"连接数"或"包数"。 这个选择有实际后果:一个游戏即使只在单机跑,只要它后台在同步云存档、拉排行榜,字节数就会涨。
不过这处实现里有一条很关键的豁免,本机在第 13 行读到它(逐字):
// Do not count network traffic for local sockets (localhost), so to avoid blocking occuring in games running local servers in single player too
"本地回环不计"——这条豁免解决的是"单机游戏自己开本地服务器"这一类。5.0 发布说明的修 bug 列表里也有一条与此对应:Fixed network detection mistaking traffic on localhost for genuine network traffic。
同一段的另外三处也在这里一并列出(都来自随篇归档的原件):
| 位置 | 逐字内容 | 作用 |
|---|---|---|
source/dll_main.cpp:293 |
if (!GetEnvironmentVariableW(L"RESHADE_DISABLE_NETWORK_HOOK", nullptr, 0)) |
一个环境变量:设了它就完全不去挂钩网络 |
source/dll_main.cpp:301 |
g_network_traffic = std::numeric_limits<long>::max(); // Special value to indicate that add-ons should never be enabled |
一个哨兵值:把计数直接顶满,等价于"永远不要启用 add-on" |
source/runtime_gui.cpp:3249 |
ImGui::TextColored(COLOR_YELLOW, _("High network activity discovered.\nAll add-ons are disabled to prevent exploitation.")); |
用户看到的那句话 |
那张 UI 串值得读两遍:All add-ons are disabled——注意是 add-ons(复数),不是"深度被关了"。它关的是整个 add-on 层,深度检测只是恰好住在这一层里的一个组件而已。这就是"关它的代码不在深度那边"的第一层含义。
2.3 第二处:61 帧的窗口与 ≥10 的阈值
有了字节计数,接下来要有判定。判定在 source/runtime.cpp 第 953 行起,本机把整段逐字抄出来:
#if RESHADE_ADDON == 1
// Detect high network traffic
extern volatile long g_network_traffic;
static int cooldown = 0, traffic = 0;
if (cooldown-- > 0)
{
traffic += g_network_traffic > 0;
}
else
{
const bool was_enabled = addon_enabled;
addon_enabled = traffic < 10;
traffic = 0;
cooldown = 60;
...
}
这段只有十来行,但每条都是可核的判据:
- 窗口长度 61 帧:
cooldown = 60意味着结算之后要等 60 帧,加上结算那一帧本身,一个窗口是 61 帧。以 60 fps 算,大约 1 秒钟。 - 计数口径是"帧"不是"字节":
traffic += g_network_traffic > 0;累加的是一个布尔值——这一帧有没有流量。所以它数的是"61 帧里有几帧在通信"。 - 阈值是 10:
addon_enabled = traffic < 10;——61 帧里有 10 帧以上在通信,就关。反过来,只要 51 帧是安静的,深度就还在。 - 判定结果落在
addon_enabled这一个全局标志上:整套机制只有一个出口。
这个阈值解释了社区里很多"奇怪"的观察:比如"我在单机里深度也没了"——如果那个单机游戏每帧都在跟后端通信(在线验证、云存档),61 帧里凑够 10 帧并不难。
也解释了另一条:"我切出去看了一下网页,回来深度就没了。"——切出去的时候游戏可能没在渲染,回来之后的第一秒里如果后台有同步流量,判定就会翻。
2.4 第三处:深度为什么是"突然没了"
好了,判定翻假之后会发生什么?本机在第 968–981 行读到的是这一段(逐字摘):
if (addon_enabled != was_enabled)
{
if (was_enabled)
_backup_texture_semantic_bindings = _texture_semantic_bindings;
for (const auto &binding : _backup_texture_semantic_bindings)
{
...
update_texture_bindings(binding.first.c_str(), addon_enabled ? binding.second.first : api::resource_view { 0 }, addon_enabled ? binding.second.second : api::resource_view { 0 });
}
}
关键在最后那个三元表达式:addon_enabled ? 原来的视图 : api::resource_view { 0 }。状态一变,就把纹理语义逐个改绑到一个空视图。
这件事对效果的后果很值得说清:深度类效果看到的是"没有这个输入",而不是"输入是黑的"。
- 如果深度被替换成一张全黑的纹理,你会看到"效果还在按黑深度工作"——多半是糊成一片或者全屏压暗;
- 而现在是把绑定整个撤掉,效果拿到的是一张空贴图——表现是"整片效果消失"或"报错跳过"。
这就是"突然没了"的准确含义。 它不是渐变的,也不是画面变黑,而是一次状态翻转:从"有深度"跳到"没有深度",中间没有过渡。
顺带说一处细节:翻假的时候它会把当时的绑定存一份备份(_backup_texture_semantic_bindings),翻真的时候再装回去。所以"网络安静下来之后深度自己回来了"是真的,但不是"功能恢复了",而是"绑定装回去了"。
2.5 第四处:这件事只签名版做
还有一层必须点明:上面那段判定裹在 #if RESHADE_ADDON == 1 里。
这句话要把第六篇的结论接过来用:RESHADE_ADDON = 1 正是签名版(Release Signed)那个配置的取值。往下看一层——
| 构建 | RESHADE_ADDON |
做不做网络检测 |
|---|---|---|
| 签名版(官网主下载) | = 1 |
做(本段代码被编译进去) |
| 全量版(Addon 版) | = 2 |
不做 |
| 测试构建(Release App) | 未定义 | 不存在 add-on 面,无从谈起 |
这与 5.0 发布说明完全对得上(官方原文):签名版 When it detects a multiplayer game, this too is disabled completely,而 Addon 版 does not do any multiplayer detection to disable features。
所以"深度被网络关掉"这个现象,是签名版特有的。 如果你在用 Addon 版却遇到了深度消失,那原因不在这里——应该往第三、四、五章那三道条件上找。这是本篇给出的第一条实用判据。
2.6 小结:这一章的四处落点
言归正传,把第二章收一下。它回答的是"深度为什么会被关",答案是四处源码:
| 处 | 文件与位置 | 它是什么 |
|---|---|---|
| ① | source/windows/ws2_32.cpp:60 + 8 处 InterlockedAdd |
字节计数的来源(含"本地回环不计"的豁免) |
| ② | source/runtime.cpp:953 起 |
61 帧窗口 × "有流量的帧数" ≥ 10 的判定 |
| ③ | source/runtime.cpp:968 起 |
翻假时把纹理语义改绑成空 → 效果"突然没了" |
| ④ | 同一段的 #if RESHADE_ADDON == 1 |
这件事只签名版做 |
四处里没有一处属于 examples/09-depth/。 一个深度效果失效,原因可以完全在它自己的模块之外——这是第一章那句"位置问题"的真正价值。
三、MSAA:三条不同层次的处理
第三章换一道条件。MSAA 是社区里被讲得最多、也讲得最含糊的一条:几乎所有排障帖都写"关掉 MSAA 就好了"。这句话在 5.0 之前基本对,在 5.0 之后只能说对了一半。
3.1 MSAA 的问题在哪
下面进入机制。先把 MSAA 做了什么说清楚:MSAA(multisample anti-aliasing)的做法是:不是每个像素只有一个深度值,而是每个像素有几个子样本、各自带深度(本系列的图形学一篇讲过,"深度测试是对每个子样本做的,所以深度缓冲的大小要按倍数涨")。这句话的后果很直接:
- 无 MSAA 时,深度缓冲是每像素一个值;
- 开了 4x MSAA 时,深度缓冲是每像素四个值。
对深度检测器来说,这意味着一件事:它不能用"整块拷走"的办法拿到深度。 一份四采样的深度资源,拷到任何"单采样"的地方都会失败——必须先解算(resolve):把四个子样本合成一个值。
先看一眼"MSAA 到底改了什么画面",再谈它对深度的后果:
这张图给的是"效果级"的对照:同一条边缘,MSAA 与 FXAA 各自把锯齿压到什么程度。它帮读者建立"MSAA 在做什么"的直觉,但它不是本篇的主角——本篇关心的是同一件事在深度缓冲上留下的后果,而那是"效果图"看不出来的部分。
社区那句"MSAA 会挡住深度"的原始出处也在这儿。 作者在兼容性帖里给过一次精确解释(二手引述):It prevents ReShade from binding the grabbed depth buffer to the shaders, making it unusable.——"抓住了但绑不上去"。抓住 ≠ 能用,这句话是理解第三章的钥匙。
3.2 三条不同层次的处理
那么 5.0 的检测器是怎么处理的?它没有只用一招,而是在三个不同的层次各做了一件事。 本机现数到的判据分布:
第一处:备份纹理侧——"我要的是一张单采样图"(源码第 258–264 行,逐字):
if (desc.texture.samples > 1)
{
desc.texture.samples = 1;
desc.usage |= resource_usage::resolve_dest;
}
它在创建备份纹理之前,把描述里的采样数改成 1,并且标记这张图要当 resolve 的目标。所以备份永远是单采样的——后面所有效果拿到的都是单采样深度,不必自己处理多采样。
第二处:取用侧——"MSAA 就先解算,别直接拷"(源码第 459–467 行):
if (desc.texture.samples > 1)
{
assert(device->check_capability(device_caps::resolve_depth_stencil) && (desc.usage & resource_usage::resolve_source) != 0);
cmd_list->barrier(depth_stencil, resource_usage::depth_stencil_write, resource_usage::resolve_source);
cmd_list->resolve_texture_region(depth_stencil, 0, nullptr, depth_stencil_backup->backup_texture, 0, 0, 0, 0, format_to_default_typed(desc.texture.format));
cmd_list->barrier(depth_stencil, resource_usage::resolve_source, resource_usage::depth_stencil_write);
}
这三行是本篇最"教科书"的一段:先加屏障把资源从"深度写入"切到"解算源",做 resolve_texture_region,再加一道屏障切回去。两道屏障不能省——深度资源和普通贴图不一样,GPU 需要明确的用途声明才能安全地在两种用途之间切换。
第三处:筛选侧——能力不够就整条缺席(源码第 979–980 行,逐字):
if (info.desc.texture.samples > 1 && !device->check_capability(device_caps::resolve_depth_stencil))
continue; // Ignore multisampled textures, since they would need to be resolved first
这是三个层次里"最狠"的一处:如果设备不支持深度解算,那这张多采样深度纹理就根本进不了候选池。注释写得很直白:Ignore multisampled textures, since they would need to be resolved first。
这三层不是"三个备选方案",而是"三道叠加的关卡":第一层决定备份长什么样,第二层决定怎么拿,第三层决定"要不要这张"。任何一层断了,效果都拿不到深度——但断的方式不同:第一、二层断了会走断言或跳过,第三层断了是静默缺席(候选池里就没有它)。
3.3 一条硬边界:resolve_depth_stencil
第三层里那个能力位值得单列,因为它是本篇第三章真正的"边界":
device->check_capability(device_caps::resolve_depth_stencil)
这是一个设备能力查询,不是一个配置项。 这两句话要分开看:
- 用户改不了它——不支持的驱动就是不支持;
- 它决定了"MSAA 下能不能用深度"这件事,而不是"画质好不好"。
这也解释了为什么社区那句"关掉 MSAA"要分版本说:
| 版本 | MSAA 下的深度 | 依据 |
|---|---|---|
| 5.0 之前 | 基本不可用 | 没有解算路径,抓住也绑不上 |
| 5.0 | 能发现但不能用 | 5.0.2 的更新条目:Added notice when MSAA depth buffers were found |
| 6.0 起 | 能用(前提是驱动支持) | 6.0 更新条目:Added support for resolving MSAA depth buffers in D3D9 (using RESZ), D3D12, OpenGL and Vulkan (when supported by the driver) |
所以"关掉 MSAA"这条经验,在 6.0 之后的更新版上已经不是唯一答案了——如果驱动支持解算,MSAA 开着也能拿到深度。这处版本差异,社区帖子里基本没人更新,本篇按发布说明写。
把它当一张"边界图"看更清楚:这一节的三个层次里,只有第三层是用户与驱动都改不了的:
这张图的读法是从下往上看:最底下是驱动能力(改不了)→ 中间是检测器的三层处理(只能绕,不能改)→ 最上面是用户在 UI 上看到的两处反馈(一个标签、一个禁用的勾选框)。三者之间没有"调一下就好"的中间态,这也是第三章最该记住的一句。
3.4 顺带纠一处:MSAA 的"通知"是什么
5.0.2 那条 Added notice when MSAA depth buffers were found 的"通知",在源码里也有落点。本机在第 1342 行与第 1312 行读到:
info.desc.texture.samples > 1 ? " Multisampled" : "",
if (info.desc.texture.samples > 1 && !device->check_capability(device_caps::resolve_depth_stencil)) // Disable widget for multisampled textures
第一行给条目加标签,第二行直接禁用勾选。 那个"通知"不是一个弹窗,而是深度缓冲列表里多了一个 Multisampled 的字样,并且那一项点不动。
这条比弹窗好用:它把"为什么这一项不能选"直接写在了那一项旁边。
3.5 采样点分布:MSAA 与 supersampling 不是一回事
第三章最后补一个容易混的概念。检索素材的时候,本机在 Commons 上找到一族很漂亮的"采样点分布"图——N-Rooks、Quincunx、Checker、RGSS、Poisson Disc……它们的文件名里写的是 supersampling,不是 MSAA。
两者的差别值得用一句话说清:
- supersampling(SSAA):把整帧渲染到更大的分辨率再缩回来——每个输出像素对应多个完整样本,这些样本都着色;
- MSAA:只在几何边缘的像素上多放几个样本,而且这些样本共享一次着色(除非是覆盖到不同三角形)。
为什么这个区别对本篇重要? 因为"MSAA 到底动了深度缓冲的什么"这件事,答案取决于采样点在哪里:
| 维度 | SSAA | MSAA |
|---|---|---|
| 样本分布 | 每个像素内规则/抖动网格 | 只在被几何覆盖的像素上,每像素固定位置的样本 |
| 深度缓冲 | 整帧多份 | 每像素多份(本像素的样本位置对应的深度) |
| 与深度的关系 | 缩回来时深度已经没了(在色彩空间里平均) | 深度需要单独解算(不能平均,要挑一个代表值) |
最后一行是 MSAA 的独家麻烦:颜色的多采样可以"平均"(那就是抗锯齿本身),而深度的多采样不能平均——把 4 个深度值取平均会得到一个"物理上不存在"的深度。所以深度解算必须用 MIN/MAX 或"取第 0 号样本"这类非平均的模式。
这也解释了官方在 6.0.1 修的那一处:Changed D3D12 depth-stencil resolve to prefer average filtering over min/max——在 D3D12 上它改成了偏好平均。这个改动与"深度不该平均"的直觉相反,原因得看 D3D12 的解算模式限制(D3D12_RESOLVE_MODE 里 MIN/MAX 对深度是允许的,AVERAGE 对非整数格式才是允许的,而深度格式正属于此类)。
四、UI 缺失:深度缓冲里本就没有 UI
第四章换第三道条件。这一道和前两道有个根本差别:前两道是"能不能拿到",这一道是"拿到的东西里本来就没有它"。
4.1 顺序决定一切
先给结论:深度缓冲里没有界面,这件事不是 ReShade 造成的,是渲染顺序的必然。
正常的游戏渲染一帧,大致是这个顺序:
1. 清空深度与颜色
2. 画世界几何(此时写入深度)
3. 画透明物体(读深度、不写深度)
4. 画界面 / HUD(通常不写深度,或者干脆关掉深度测试)
5. 呈上屏
界面在第 4 步,而深度缓冲的主要内容在第 2 步就写完了。 如果界面不写深度(绝大多数游戏如此),那么深度缓冲里就只有世界几何——界面在深度图上根本不存在。
所以"深度里没有 UI"是设计目标,不是缺陷。 对深度类效果来说,这恰恰是好事——SSAO、DOF、MXAO 这些效果本来就是冲着"世界几何"去的,如果深度里混进了 HUD,阴影和环境光就会算到界面上去。
但另一面就是那条常见的抱怨:"为什么我的 MXAO 在 UI 上也有一层暗影?"
4.2 深度图长什么样
先来看看深度图本身,要理解这个抱怨得先看一眼它。深度缓冲不是一个抽象概念,它就是一张图——一张灰度图,越近越亮(或越远越亮,取决于方向)。
这张图值得盯着看两秒。它没有纹理、没有颜色、没有光照——只有距离。 这就是所有深度类效果唯一的输入。
看清楚了就明白第四章的核心矛盾:这张图里只有世界。界面不在里面,天空盒可能是最远处那一片平坦的灰,透明物体基本不出现。效果只看得见这张图,所以它也只能对这张图里的东西作出反应。
4.3 为什么"糊在 UI 上"
好了,那"糊在 UI 上"是怎么发生的?答案在后处理的顺序里。
ReShade 的效果链挂在 present 之前——也就是上面那段流程的第 5 步之前。在它跑的时候,界面已经画进颜色缓冲了,但深度缓冲里没有界面。
于是效果面对的是一组"错配"的输入:
- 颜色缓冲:世界 + UI(UI 已经画上去了);
- 深度缓冲:只有世界(UI 不在里面)。
效果按深度判断"这里是远处的地面",于是给它加了环境光遮蔽;可这个像素上现在画着的其实是 UI。 结果就是暗影落在了界面上。
这不是 bug,是"两个输入的时间点不一致"。 颜色是第 4 步之后的,深度是第 2 步之后的——它们差了两步。
这也是为什么它没法在 ReShade 这一侧"修好":效果手上只有这两张图,它能做的只是"猜哪一块是 UI"。
4.4 三种处置与它们的代价
说白了,面对这件事,实际可选的路只有三条。本机把它们和代价列在一起:
| 处置 | 做法 | 代价 |
|---|---|---|
| 不管 | 接受界面被影响 | UI 上会有一层不该有的效果 |
| UI 遮罩(UI mask) | 对界面不变的游戏,预先划出界面区域,让效果跳过 | 只适用于静态 UI;界面一变就要重划 |
| 等深度里出现 UI | 指望游戏把界面也写进深度 | 极少见;写进去之后反而会污染 SSAO/DOF |
第二条是本系列里唯一"能用"的方案,但它的适用面被第一句限制死了:界面不变。一个会弹提示、会换 HUD 的游戏,遮罩就得跟着改。
社区里还有一条更硬的说法值得引一次。 那位第三方效果的作者在论坛上写过(二手引述):
if an object doesn't show up in the depth buffer, it's invisible to any depth shader.
"深度缓冲里没有的东西,对任何深度着色器来说都是不存在的。" 这句话把第四章的全部内容压成了一句。他还举过一个更具体的例子:某个游戏在小地图打开时,深度缓冲里会出现一个白方块——那是"界面写进了深度"的反例,而且是污染型。
所以第四章的结论是一条判据:遇到"效果糊在 UI 上",不要去找 ReShade 的 bug——去看那个游戏的界面是不是写深度。写了,就是上面表格第三行那种污染;没写,就只能用遮罩,或者接受它。
五、子矩形错位:启发式为什么会选错
第五章是四道条件里最复杂的一道。前四章讲的都是"能不能拿到",这一章讲的是"拿到的是不是我要的那一张"。
5.1 一个深度缓冲,多种分辨率
先把问题的形状说清楚。一个游戏在一帧里可能创建几十个深度资源,但其中只有一个(或几个)是"主场景深度"。检测器要从这几十个里挑一个出来,靠的是启发式——而启发式会看一件事:宽高比。
为什么要看宽高比?因为一个深度资源可能被用到完全不同的分辨率上。源码里的注释直接点了名(逐字):
Mass Effect 3 in Mass Effect Legendary Edition sometimes uses a larger common depth buffer for shadow map and scene rendering, where the former uses a 1024x1024 viewport and the latter uses a viewport matching the render resolution
同一张深度缓冲,先用 1024×1024 的视口画阴影图,再用"与渲染分辨率一致"的视口画主场景。 如果检测器只看"这张图有多大",它就无法判断当前这一次到底是阴影还是主场景。
本机把那个判据函数逐字抄了出来:
函数的判据是一条双条件(源码第 372–378 行):
float w_ratio = width / width_to_check;
float h_ratio = height / height_to_check;
const float aspect_ratio_delta = (width / height) - (width_to_check / height_to_check);
// Accept if dimensions are similar in value or almost exact multiples
return std::abs(aspect_ratio_delta) <= 0.1f && ((w_ratio <= 1.85f && w_ratio >= 0.5f && h_ratio <= 1.85f && h_ratio >= 0.5f) ||
(s_aspect_ratio_heuristic == aspect_ratio_heuristic::multiples_of_resolution && std::modf(w_ratio, &w_ratio) <= 0.02f && std::modf(h_ratio, &h_ratio) <= 0.02f));
两个条件缺一不可:
- 宽高比之差 ≤ 0.1:
16:9(1.778)与4:3(1.333)差 0.44,直接排除;16:9与1920×1088(1.765)差 0.013,通过。 - 两个方向上的缩放比都在 [0.5, 1.85] 之间——尺寸不能差太多:一半到 1.85 倍是可接受的,再往外就认为"这不是同一件事"。
为什么这两个数要写得这么具体? 因为它们是被具体游戏逼出来的:
- 下限 0.5、上限 1.85 是"阴影图与主场景共存"的合理范围(1024×1024 对 1080p 大致落在里面);
- 0.1 的宽高比容差 是为了容忍"同样 16:9 但高度差几像素"的情况——第五章最后会讲那个"多 1 像素"的真实案例。
这处判据也不是总是开启的。 它的严格度由 [DEPTH] UseAspectRatioHeuristics 控制,一共五档(第一章那张枚举表里的 aspect_ratio_heuristic):从"完全不检查"一直到"必须与自定义分辨率完全相等"。默认是 similar_aspect_ratio——也就是上面那两个条件的形式。
5.2 视口与子矩形:为什么会有"分辨率不一样"
先来看看一个更基础的问题。上一节说到"同一张深度缓冲服务两种分辨率",读者可能会问:一张图怎么会服务两种分辨率? 答案在视口(viewport)这个概念上。
视口是"把归一化设备坐标(NDC)映射到渲染目标的哪一块矩形"的那一步。换句话说,同一张渲染目标,可以被切成任意多个子矩形分别渲染:
这张图解释了第五章的根源:渲染目标是一张图,而视口是"这张图上的哪一块"。 于是同一张深度缓冲:
- 可以用 全屏视口 渲染主场景;
- 也可以用 角上一小块 渲染小地图、水下效果、镜面反射;
- 还可以用 一张小图(比如 1024×1024)当阴影图,然后用它渲染到全屏。
"子矩形错位"这个说法的准确含义就在这里:检测器看到的是一张深度资源,但它服务的是哪一个子矩形,取决于当帧的视口。视口一会儿大一会儿小,检测器如果只看"资源本身多大",就会选错。
5.3 四个具名游戏的补丁
下面进入最有说服力的一节:源码注释里直接写了游戏名。 本机把四处逐字抄出来:
| 游戏 | 注释逐字(节选) | 对应的补丁 |
|---|---|---|
| Mirror's Edge | occasionally render something into a small viewport (16x16 in Mirror's Edge, ...) |
视口太小就不拷贝:last_viewport.width > 1024 |
| Portal | (512x512 in Portal to render underwater geometry) |
同上 |
| Mass Effect 3(LE) | uses a larger common depth buffer for shadow map and scene rendering |
走 check_aspect_ratio 判据 |
| Garry's Mod | Special case for Garry's Mod which chooses the last clear operation that has a high workload |
do_copy = current_stats.vertices >= 5000; |
这四处合起来,把"子矩形错位"讲透了:
- 前两个是"太小的视口":16×16 和 512×512 都远小于 1080p。检测器如果老老实实按"谁画的次数多"来选,就会把这种小视口的 clear 当成主场景的 clear。所以它加了一条闸门——本机在第 414 行读到:
do_copy = current_stats.last_viewport.width > 1024 || (current_stats.last_viewport.width == 0 || depth_stencil_backup->frame_width <= 1024);
"视口宽度超过 1024 才拷贝"——这条闸门的代价是可预期的:真正的主场景如果也被渲染到小于 1024 的视口里(比如游戏内部分辨率缩放),这条规则就会把它一起筛掉。
- 第三个是"两种分辨率共存":这正是 5.1 节那段注释讲的情形,处理办法是走宽高比判据。
- 第四个是"Garry's Mod 特例":它不走宽度判据,而是走顶点数——
>= 5000才算主场景。一个游戏一条特例,这就是启发式的真实形态。
读到这里应该有一个清醒的认识:这套检测器不是"算法",而是"一批具体案例的堆积"。 它的 1,549 行里,相当一部分是在处理"某个游戏会怎么出人意料"。这既是它能工作的原因,也是它必然不完美的原因。
5.4 为什么"时灵时不灵"
第五章最后回答一开始那个问题。"时灵时不灵"的机制层原因,是"该取哪一次"这条规则会随帧漂移。
具体地说,检测器在"自动模式"下的做法是:把当前帧里所有 clear 事件的信息攒起来,挑一个"最有可能是主场景"的。这个"挑"用的判据,就是 5.3 节那些——比顶点数、比绘制次数、看视口宽度、看宽高比。
问题在于:这些量每帧都在变。
- 玩家视角一转,可见的物体变了,
vertices与drawcalls就变了; - 场景里多了一个反射面,就多了一次 clear;
- 阴影图与小地图的渲染次数也可能随场景变。
于是同一条规则,这一帧挑中的是 A,下一帧挑中的是 B。 在读者眼里,那就是"深度时不时就不对了"。
社区把这件事归因给"游戏 bug"是常见误判。 官方在 PR 里给过机制层的说明(二手引述):某一次修复的动机是 because the number of clearing instance evolves on each frame——"clear 的次数每帧都在变"。这句话就是 5.4 节的全部内容。
这也解释了为什么"固定一个 clear 索引"这个选项有用。 回头翻第一章那 8 个配置键,DepthCopyAtClearIndex 的作用就是把"自动挑"换成"固定挑第几个":
- 值为
0(默认):自动检测; - 值为正数 n:固定取第 n 次 clear;
- 值为
INT32_MAX:走 Garry's Mod 那种"末次高负载 clear"的特例。
用户手动固定一个索引,等于把"随帧漂移"这件事关掉。 代价是:如果那个游戏的主场景 clear 次数本身不稳定,固定索引就会稳定地选错。这是一次纯粹的两难,没有"更好的默认值"。
5.5 一像素的经典案例
好了,第五章收尾,讲一个本机在社区里核到的最小、也最有说服力的案例。
有一个玩家报过这样的问题(二手引述):某个游戏里有 3 个深度模板,其中 2 个的高度比窗口多 1 像素,1 个与窗口等高;而"应该选的那一个",恰好是多 1 像素的那个。检测器按"最匹配"去挑,挑了等高那个——于是深度全错。
1 像素的差别,让整个深度选择翻车。
这处案例的价值在于它把 5.1 节那两个判据解释清楚了:
- 宽高比容差 0.1 是从哪来的:1080p 的宽高比是 1.7778;高度多 1 像素(1081)之后是 1.7761——差 0.0017,远小于 0.1。所以这两个候选在"宽高比"这一关上是同时通过的,检测器必须靠别的判据来分。
- 为什么"必须完全相等"会成为一个选项:
match_resolution_exactly那一档就是为这类游戏准备的——不要容差,要求像素级相等。但它的代价同样明显:一旦游戏做了任何分辨率缩放,这一档就什么都选不中。
"子矩形错位"这个词,说的就是这一类:不是差得离谱,而是差得刚好——刚好在容差之内、刚好在两个候选之间。
六、与第六篇接上:27 ∩ 30 = 空
第六章把这一篇和第六篇接起来。这件事在第五章结束时已经埋了伏笔:签名版在编译期禁掉了一大批事件,那深度检测凭什么还能用?
6.1 一边是 30 个被禁事件
先来看看第六篇那份名单,读数搬过来:第六篇现数出:签名版(RESHADE_ADDON = 1)在编译期禁掉了 30 个事件,手段是 source/addon_manager.hpp 里的两处 static_assert(13 + 17 = 30),错误串是:
"Event that is disabled with limited add-on support was used!"
那 30 个事件的共同特征,第六篇里的原话是:"被砍掉的全是'能改渲染命令本身'的那一类。" 具体包括 bind_pipeline/bind_pipeline_states/push_constants/push_descriptors/bind_descriptor_tables/bind_index_buffer/bind_vertex_buffers/bind_stream_output_buffers/barrier/map_*/unmap_*/copy_*/update_*/resolve_texture_region/generate_mipmaps/begin_query/end_query/get_query_heap_results 等等。
注意最后几个"拷贝与查询"类——它们在名单里,这件事第六章会用到。
6.2 另一边是 27 个被需要的事件
另一边,本篇第一章现数出:内置深度 add-on 注册了 27 个事件(init_*/destroy_*/create_resource/create_resource_view/init_resource/destroy_resource/draw*/dispatch_mesh/bind_viewports/begin_render_pass/bind_render_targets_and_depth_stencil/clear_depth_stencil_view/reset_command_list/execute_command_list/execute_secondary_command_list/present/reshade_*)。
把它们按用途分一下,就是第一章那张四组分类表的来源:生命周期组八个、资源组四个、工作量与时机组十二个、ReShade 专有组三个。现在的问题是:这四组里的每一条,签名版都留着吗?
现在把两个名单做交集。 本机用的是第六篇归档的探针读数(那 30 个名单)与本篇现数的 27 个名单:
结果是:交集为空。27 个里,一个都没落在那 30 个里——签名版砍掉的那一批,与这个检测器需要的这一批,重合的部分是零。这个零不是碰巧凑出来的:它是"哪一类事件能被砍"这条设计边界的直接后果。
6.3 交集为空意味着什么
说白了,这不是巧合,是设计。它同时解释了四件事:
第一,它解释了 5.0 发布说明那句"just enough"。 官方原话是签名版 internally only hooks a very limited set of graphics API functions, just enough to make the built-in depth buffer detection work in singleplayer games still。第六章这一节的交集为空,就是"just enough"的机制层含义:签名版留下的那一小部分 hook,恰好覆盖了深度检测需要的全部事件。
第二,它解释了"为什么是这 27 个"。 反过来看:如果深度检测需要注册 resolve_texture_region 这个事件的回调(它在 30 个名单里),那么签名版就会编译不过——而它其实不需要注册这个事件,它只需要调用这个 API 方法。
这处细微差别很关键,值得单独说清:
| 事 | 签名版能不能做 | 为什么 |
|---|---|---|
注册 resolve_texture_region 事件的回调 |
不能 | 它在两处 static_assert 的名单里,编译期就报错 |
调用 cmd_list->resolve_texture_region(...) |
能 | 那是 command_list 对象的方法,不是事件回调;与 add-on 事件的开关无关 |
"注册事件"与"调用 API"是两件事。 第三章那两处 resolve 调用之所以在签名版里也能跑,正是因为它是后者。
第三,它给了一条判断某段代码"能不能在签名版里跑"的实用办法:看它注册了哪些事件,而不是看它调用了什么。注册的事件全部落在那 30 个之外,那段代码就能在签名版里编译通过。
第四,它解释了为什么"内置"这个词这么重要。 一个第三方 add-on 如果想做深度检测,它要注册的事件多半也在那 27 个里(因为需要的都是一样的信息)——所以它其实也编译得过。那为什么检测器要做成"内置"?
因为内置的才能被签名版带着走。 回想第六篇的读数:签名版不加载任何外部 add-on(ReShadeRegisterAddon 对非内置模块直接拒绝,源码里那句判定是 info->handle != g_module_handle)。内置的意思不是"代码在核心目录里",而是"它不受'不加载外部 add-on'这条限制"。
第六章把前面五章串起来了:第二章说网络检测只签名版做,第六章说深度检测刻意留在签名版可用——两件事都落在同一个宏 RESHADE_ADDON 上。这个宏一手砍功能、一手保功能,而"保下来的那部分"正好是深度。
七、两个常被混进这一题的概念
第七章处理两处"高频道听途说"。它们都不是本篇的四道条件,但经常被当成条件讲,不把它们分开会说乱。
7.1 三个宏:REVERSED、UPSIDE_DOWN、LOGARITHMIC
先来看看社区里出现频率最高的三条建议——一定是这三个词:
RESHADE_DEPTH_INPUT_IS_UPSIDE_DOWN:设成 1,修"深度图上下颠倒";RESHADE_DEPTH_INPUT_IS_REVERSED:设成 1,修"深度图远近反了";RESHADE_DEPTH_INPUT_IS_LOGARITHMIC:设成 1,修"深度范围不对"。
本机在这一节要纠一个很常见的误会。 不少人以为"这三个是 ReShade 里的开关"。本机在归档的 source/ 与 include/ 全量搜 RESHADE_DEPTH_INPUT_IS/IS_REVERSED/REVERSED/LOGARITHMIC——零命中。
它们是着色器侧约定的宏。 逐条说清楚:
| 事 | 事实 |
|---|---|
| 谁定义它 | 用户——在 ReShade 的 [GENERAL] PreprocessorDefinitions 里自己填,或者在预设文件里写 |
| 谁读它 | 效果文件(.fx)——效果在编译时读这个宏,决定怎么解释深度 |
| 核心代码管不管它 | 不管。ReShade 不定义、不校验、不自动设置这三个宏 |
ReShade 真正自动注入的宏是另一族(本机在 runtime.cpp 第 1651–1665 行读到,逐字):
pp.add_macro_definition("__RESHADE__", ...);
pp.add_macro_definition("__RESHADE_PERMUTATION__", ...);
pp.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", ...);
pp.add_macro_definition("__VENDOR__", ...);
pp.add_macro_definition("__DEVICE__", ...);
pp.add_macro_definition("__RENDERER__", ...);
pp.add_macro_definition("BUFFER_WIDTH", ...);
pp.add_macro_definition("BUFFER_HEIGHT", ...);
pp.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", ...);
注意这一族里没有"深度方向"。 __RENDERER__ 与 __DEVICE__ 是硬件标识,BUFFER_* 是分辨率——ReShade 告诉效果"你在什么硬件上、画布多大",但不告诉它"深度是正是反"。
这个分工不是疏漏,是必然。 深度方向取决于游戏怎么建那张深度缓冲,ReShade 只能"观察到"而不能"知道"——所以它把这个判断交给了效果作者,用宏的形式让作者去试。
这就解释了为什么"三个宏"在社区里是万能答案、在源码里却找不到:它们本来就不在源码里。这一处是本篇要给读者的第二条件实用判据:如果某个教程让你"去 ReShade 里改三个宏",那只是在说"让效果换个方式读同一张深度"——它不会让被网络关掉的深度回来,也不会让 MSAA 下的深度出现。
7.2 一处必须澄清的口径:LOGARITHMIC 不是万能修复
那三个宏里,最容易被滥用的是 LOGARITHMIC。社区经常建议"开上它试试";而第三方效果的作者明确反对过(二手引述):
LOGARITHMIC ... the formula will cause the depth buffer to lose a lot of precision ... it is recommended to leave this at 0 at all times.
"它会损失精度,建议恒为 0。" 换句话说,LOGARITHMIC 是修特定游戏的对数深度格式用的,不是通用优化——在正常的线性/反转深度游戏上开它,只会让精度变差。
对照一下另外两个:
| 宏 | 什么时候真的需要 | 误用的后果 |
|---|---|---|
UPSIDE_DOWN |
深度图上下颠倒(少数后端/翻转 Y 的情形) | 深度图方向更乱 |
REVERSED |
游戏用了反转深度(近处值大、远处值小) | 深度图远近反了 |
LOGARITHMIC |
游戏真的写了对数深度 | 精度下降——作者建议恒为 0 |
这张表值得和第五章横向看:第五章那四道条件都是"拿不到"或"选错了",而这三个宏是"拿到了但读法不对"。两类问题必须分开诊断——这也是本篇在边界声明里写"不写调参教程"的原因:读法是效果侧的事,条件是运行时侧的事。
7.3 深度精度与 z-fighting
第七章最后补一个背景概念:为什么"深度的值"这么容易出问题。 关键词是 z-fighting。
z-fighting 的现象很好认:两个几乎共面的多边形,渲染出来是一片细碎闪烁的斑。原因是它们的深度值在深度缓冲的精度里无法区分——差值小于一个深度单位的可表示间隔。
这张图里两组立方体摆在几乎相同的位置上,一组出现细碎斑点,另一组干净。差异只来自它们的深度值能不能被区分。
这件事和本篇的四道条件有什么关系? 关系在"精度预算"上:
- 非反转深度(近处值小)的精度分布极不均匀:绝大部分精度挤在近处,远处几乎没有——16 位深度缓冲下,远平面附近可能出现"整整几十米共用一个深度值";
- 反转深度(近处值大)把精度翻过来,让远处也有可用的分辨率——这就是 NVIDIA 那篇《Depth Precision Visualized》给出的结论:"在透视投影下,直接用反转 Z 的浮点深度缓冲"。
为什么这值得放一篇关于"工程条件"的文章里? 因为它说明深度类效果对深度的要求不止是"有没有",还有"够不够"。
一个具体后果:检测器拷出来的是那张深度缓冲的原样(copy_resource 或 resolve_texture_region),它不做精度提升。所以如果游戏用的是 16 位深度,效果拿到的也是 16 位的粗糙深度——这时候再怎么调效果的参数也补不回精度。
这一处也顺便解释了一个现象:同一个效果在 A 游戏上干净、在 B 游戏上"有一圈圈阶梯"——那多半是两张深度缓冲的位深或精度分布不同,而不是效果本身的问题。
八、本机的口径与边界
第八章照例把"能说什么、不能说什么"钉死。
8.1 没有两个二进制
下面进入本篇最硬的一条边界。本机只有一份 ReShade 运行环境,没有"签名版"与"Addon 版"两份二进制可以对比。
所以下面这类问题,本篇不给结论:
- 「签名版里,一个深度效果实际会怎么失败?」——测不了;
- 「网络检测翻假之后,画面是黑一帧还是直接静默?」——测不了(源码只告诉我们"绑定被撤掉",不告诉我们"第几帧开始");
- 「Addon 版里深度会不会更稳?」——测不了(源码告诉我们它不做网络检测,但运行期表现不在本机可测范围)。
能给的只有两层,而这两层本篇都给足了:
| 层 | 能说到什么程度 | 本篇的落点 |
|---|---|---|
| 编译期 | 哪些代码在哪个构建里被编译进去、哪些在编译期就报错 | 第二章那处 #if RESHADE_ADDON == 1;第六章那两处 static_assert |
| 源码逻辑 | 判定的输入、阈值、输出分别是什么 | 第二章的 61 帧/≥10;第三章的三层处理;第五章的宽高比判据 |
这条边界看起来是"少说了",实际是"把话说实了"。 一个"可能在签名版下会失败"的推断,价值远低于"这段代码只在 = 1 时编译进去"这句事实。
8.2 一处工具自纠
第八章还有一处要照实写:本篇用来做检索初筛的脚本,第一版判准偏高。
阶段 2 的题域初筛脚本(evidence/build/ep07_screen_pool.py)按标题词元判"题域内/题域外",它给出的是 96 条题域内。但人工复核之后,这 96 条里有一批是假命中:
| 假命中 | 为什么被误收 |
|---|---|
Benesi - Pine Tree ... 8K zbuffer.png 等一批 8000×8000 的图 |
标题含 zbuffer——但那是用 z-buffer 渲染出来的分形艺术,与深度缓冲的工程条件无关 |
Beringia ... snow depth ...(雪深地图) |
标题含 depth——那是地理数据 |
Subpixel-Rendering*.JPG(次像素字体渲染) |
标题含 anti-aliasing 的词元近邻——那是 ClearType,与 3D 抗锯齿无关 |
Tudor-stuart scissor handle(剪刀实物) |
标题含 scissor——"子矩形"那个 scissor 与"剪刀"同名 |
这篇记录的意义有两个:一是说明"自动筛一遍就能定论"是不成立的——词元匹配会把所有"词形撞车"收进来;二是说明那份 318 条候选池里真正可用的部分很小,这一点已经在 material-inventory.md 与 render-assets-plan.md 里如实登记(题域外 222 条,占 69.8%)。
这与本篇的选题判断是一致的:这一题的外部素材本来就薄,初筛偏偏又是机器判不准的地方——所以"薄"这个结论是两条通道互相印证之后得到的,不是单点判断。
8.3 一版时间线
好了,第八章最后把散在各章的版本信息拼成一条时间线。年份口径取自官方发布说明的页面标识(本机亲核 5.0 与首页;6.0 的条目引自发布说明原文,标为转述):
| 版本 | 与深度有关的变化 | 依据 |
|---|---|---|
| 5.0(2021) | 深度缓冲检测重构成内置 add-on;算法改进;所有 API 都能用 | 发布说明原文(本机亲核) |
| 5.0.0 | 修 Fixed network detection mistaking traffic on localhost for genuine network traffic |
同上 |
| 5.0.2 | Added notice when MSAA depth buffers were found——MSAA 深度会被发现并标注 |
同上 |
| 5.2 | [DEPTH] 新增"帧内拷贝深度"选项(D3D12/Vulkan),用于抖动时的取舍 |
发布说明(转述) |
| 6.0 | Added support for resolving MSAA depth buffers in D3D9 (using RESZ), D3D12, OpenGL and Vulkan (when supported by the driver) |
发布说明(转述) |
| 6.0.1 | 修 D3D12 MSAA 下 "Failed to close immediate command list";Changed D3D12 depth-stencil resolve to prefer average filtering over min/max |
发布说明(转述) |
这条时间线有两条叙事线:一条是精度(加解算),一条是兼容(一个游戏一个补丁)。前者解决"理论上能不能",后者解决"这个游戏上能不能"——第五章那张四个具名游戏的表就是后者的样本。
九、三条结论与路线图
9.1 本篇三条结论
言归正传,这一章只给三条结论,每条都带数。
结论一:深度不是"取一次就永远有"的资源,它受四道条件约束,而四道条件的落点全在深度模块之外。
四道条件的落点,本机逐个查过:
| 条件 | 落点 | 是不是 examples/09-depth/ |
|---|---|---|
| 自动关闭 | source/runtime.cpp + source/windows/ws2_32.cpp + source/dll_main.cpp + source/runtime_gui.cpp |
不是(四处都在核心) |
| MSAA | examples/09-depth/generic_depth_addon.cpp(三层处理)+ 图形 API 的能力位 |
是(但受能力位约束) |
| UI 缺失 | 游戏的渲染顺序(第 2 步定稿、第 4 步才画 UI) | 不是(在游戏那边) |
| 子矩形错位 | 同上的 1,549 行启发式 | 是 |
四道里有两道完全不在深度模块里。 这就是本篇开头那句"位置问题"的最终答案。
结论二:一套看似"算法"的检测器,实际是 1,549 行的具体案例堆积。
1,549 行里能数出来的具体案例至少四处:Mirror's Edge 的 16×16、Portal 的 512×512、Mass Effect 3(LE)的 1024×1024、Garry's Mod 的 vertices >= 5000。没有一个是"通用算法",每一条都对应"某个游戏会怎么出人意料"。
这个形态决定了它的上限:新游戏永远可能撞上没见过的模式,所以"时灵时不灵"不会消失,只会被一条条补丁压得更窄。第五章 5.4 节那条"clear 次数随帧漂移"就是它的下限——连同一个游戏内部都不稳定。
结论三:RESHADE_ADDON 这个宏一手砍功能、一手保功能,而保下来的正好是深度。
砍的那一手:签名版编译期禁掉 30 个"插手渲染命令"的事件。保的那一手:内置深度检测需要的 27 个事件与那 30 个的交集为空,所以它照样编译得过、跑得起来。第六章那处空集,就是官方那句 "just enough" 的机制层解释。
9.2 从这一篇往后怎么走
- 第八篇(效果生态与许可地图) 接的是"效果从哪来"这条线——本篇只在第六章用了"一个第三方 add-on 要注册哪些事件"这个角度,那些效果各自的许可与可分发性,留给第八篇;
- 第九、十篇(自跑) 接的是"真跑一遍"这条线——本篇所有读数都是离线解析源码得来的,一次实验都没做;实际注入与前后对比在第九、十篇;
- 第十一篇(合规与取舍合集) 会把本篇这四道条件并进合集的"代价"那一栏。
9.3 三类读者的最短路径
- 只想修自己那个游戏:读第一章(确认代码在哪)+ 第二章 2.5 节(先判"你是不是签名版")+ 第三章那三条处理(判"是不是 MSAA")。这三处的判据都是"是/否";
- 想知道这套东西怎么工作:第一章(规模与枚举)+ 第三章(三层处理)+ 第五章(启发式与具名补丁)。第五章那四个游戏是最有信息量的一节;
- 想接着上一篇读代码:第三章 3.3 节(能力位)+ 第六章(27 ∩ 30 = 0)。第六章是本篇与第六篇的接缝。
9.4 本篇与后面几篇的接口
- 本篇讲"深度这类效果会被什么条件卡住",第八篇讲"这些效果从哪来、能不能用":两篇共用"add-on 注册事件"这个机制(第六篇的产物),但本篇不涉及任何效果的许可与来源;
- 本篇的读数全部是离线的,第九、十篇的读数是运行期的:两边的口径在第九篇开头会再交代一次,本篇的"测不了"那一节(8.1)是它的前置声明;
- 第二、三章那两处"只签名版做"与"签名版恰好保留",在第十一篇的取舍栏里会合成一条:安全策略与功能可用性是同一个开关的两面。
资源清单
官方源码(本篇引用并随篇归档,逐行可复核):
- examples/09-depth/generic_depth_addon.cpp(1,549 行,5.0 起深度检测的唯一实现)
- examples/09-depth/generic_depth.vcxproj
- source/runtime.cpp(第 953–981 行的网络判定;第 1651–1665 行的自动宏)
- source/runtime_gui.cpp(第 3249 行那条 UI 串)
- source/windows/ws2_32.cpp(
g_network_traffic与本地回环豁免) - source/dll_main.cpp(
RESHADE_DISABLE_NETWORK_HOOK与"永不启用"哨兵) - source/addon_manager.hpp(两处
static_assert的名单) - source/addon_manager.cpp
- examples 目录
官方页面(本机亲核):
- ReShade 官网首页("latter is automatically disabled during multiplayer to prevent exploitation")
- 5.0 发布说明(深度检测重构成内置 add-on;签名版与 Addon 版的差别)
- 发布说明索引
- 6.0 发布说明(MSAA 深度解算;本条引自发布说明,标为转述)
- 5.2 发布说明(帧内拷贝深度选项,转述)
- 官方 API 文档
官方论坛与 PR(本篇标注为二手引述的部分):
- The equivalent of get_current_back_buffer for depth buffer("There is no one depth buffer to get")
- Common Issues and Their Solutions
- Introducing: Easy Troubleshooting
- Compatibility list(第 380 页)("Only MSAA. It prevents ReShade from binding...")
- Depth buffer detection modifications
- Dof work with ingame aa on("缓存上一帧"那处)
- Depth buffer disables on low fps("与帧率无关"那处)
- DisplayDepth not working on AC Origins("多 1 像素"那处)
- What to do if ReShade doesn't find the Depthbuffer?
- PR #177:fix d3d9 depth buffer detection
- PR #129:Fix dx10 dx11 depth buffer detection
- PR #270:Generic depth resolve MSAA depth buffer if running Vulkan
第三方效果与竞品文章(只引述,不转载):
- Marty's Mods:THE DEPTH BUFFER
- Marty's Mods Guides:Depth
- iMMERSE / RTGI 官方指南
- PCGamingWiki:ReShade
- Depth3D 官方页
图形 API 与通用技术权威:
- Microsoft:D3D12_RESOLVE_MODE
- Microsoft:Programmable Sample Positions
- Microsoft:Depth Buffers(Direct3D 9)
- Vulkan 规范:Render Pass
- Vulkan 样例:MSAA
- A Quick Overview of MSAA(MJP)
- NVIDIA:Depth Precision Visualized
外部示意图(Wikimedia Commons,图注内已逐张署名):
- Z buffer.svg
- Z-buffer.svg
- ZfightingCB.png
- Z buffer-Z fighting.png
- Esempio di Depth Map.jpg
- MSAA and FXAA antialiasing.png
- Supersampling - N-Rooks.svg
- Viewport transformation.png
- Hidden Surface Determination.svg
- Rasterisation-triangle example.svg
本系列前六篇(跨篇指代的落点):
- 第一篇 总论:注入式后处理与管线内后处理的边界
- 第二篇 代理 DLL 与导出表挂钩
- 第三篇 ReShade FX:语言血统、独立编译器与纹理语义
- 第四篇 effect runtime:technique / pass 图
- 第五篇 跨 API 抽象
- 第六篇 Add-on API:事件模型、跨 API 资源交换与两种构建的分野
下篇预告
下一篇是《效果生态与许可地图:同一份清单里的 MIT 与"禁止再分发"》——把"这些效果从哪来、能不能用"讲透。本篇提到的那些深度类效果(SSAO、MXAO、DOF、光追 GI),会在下一篇里被按许可分成几档:MIT 的可以自由改,"禁止公开再分发"的只能自己用,而 Patreon 付费的那一类连获取路径都不写。里面有一条和第二、三章直接相关:为什么有些效果要求"深度必须可用",而另一些能退化成"没有深度也跑"。
写在最后
好了,这一篇是系列里最"工程"的一篇:它讲的不是画面,也不是接口,而是四个条件。分层、判据、落点——三件事说清了,"深度时灵时不灵"就不再是一句玄学;反过来,这三件事里任何一件没搞清,遇到问题都会以"深度又没了""效果糊在 UI 上""怎么调都不对"这三种形式回报你。希望能对大家有所帮助——尤其是那句"深度被网络关掉",它的判定其实只有十来行,而这十来行一直在 runtime.cpp 里,只是很少有人去翻。