将质量效应黑斑固定在现代AMD CPU上

2020-07-19 23:42:17

Tl;dr-如果您对游戏的问题和修复方式的深入概述不感兴趣,请向下滚动到下载部分获取下载链接。

“大众效应”是科幻角色扮演游戏中很受欢迎的特许经营游戏。第一款游戏最初是由BioWare于2007年底在Xbox360独家发布的,作为与微软达成的发行协议的一部分。几个月后的2008年年中,这款游戏获得了Demiurge Studios开发的PC端口。这是一个不错的端口,没有明显的缺陷,直到2011年AMD发布了基于推土机的新CPU。当在配备现代AMD处理器的PC上玩游戏时,游戏中的两个区域(Noveria和ilos)显示出严重的图形伪像:

虽然不是不能玩,但肯定会让人分心。谢天谢地,有解决办法-比如通过控制台命令禁用照明,或者修改游戏地图来移除破碎的灯光,但似乎这个问题一直没有完全被理解。一些消息来源声称FPS计数器模块也可以修复这个问题,但我找不到太多关于这个问题的信息,国防部的源代码似乎也无法在线获得,也没有关于国防部如何解决这个错误的文档。

是什么让这个问题特别有趣?特定于供应商的漏洞并不是什么新鲜事,游戏已经有几十年的历史了。然而,据我所知,这是唯一由处理器而不是显卡引起的图形问题。在大多数情况下,问题发生在特定的GPU供应商身上,他们不关心CPU,而在这种情况下,问题恰恰相反,这使得这个问题非常独特,值得研究。

在网上查阅现有的讨论,这个问题似乎影响到AMD FX和Ryzen芯片。与老式的AMD芯片相比,这些芯片缺少3D Now!指令集。不管是否相关,社区的共识是这是错误的原因,游戏在检测到AMD CPU时试图使用这些指令。鉴于英特尔CPU和3D Now!上没有发生此错误的已知案例!说明是AMD独有的,难怪社区认为这就是问题所在。

这真的是问题所在,还是完全不同的原因造成的?让我们来看看吧!

尽管这个问题重现起来微不足道,但我无法深入研究很长时间,原因很简单--我无法接触到任何装有AMD硬件的PC!值得庆幸的是,这一次我不再孤军奋战--拉斐尔·里维拉(Rafael Rivera)在整个研发过程中都得到了我的支持,为我提供了一个拥有AMD芯片、洞察力和想法的测试环境,同时还忍受了我在试图找到这些未知问题的根源时通常会胡乱猜测的数百种盲目猜测。

既然我们现在有了一个很好的测试环境,第一个要测试的理论当然是cpuid-如果人们假设3DNow是正确的!指令是罪魁祸首,在游戏代码中应该有一个地方,让他们检查自己的存在,或者至少检查一下CPU供应商。然而,这一推理是有缺陷的;如果游戏试图使用3DNow是真的!指令在AMD芯片上运行的任何时候,如果不检查它们是否受支持,当试图执行非法指令时,游戏很可能会崩溃。此外,快速扫描游戏代码会发现游戏没有检查CPU功能。因此,无论这个问题出了什么问题,它似乎都不是由游戏错误检测CPU功能引起的,因为它似乎从一开始就不关心它们。

当这看起来像是一个无法调试的案例时,Rafael给我带来了一个实现-禁用PSGP(处理器特定图形管道)解决了这个问题,字符被正确地照亮了!PSGP不是记录最好的术语,但简而言之,它是允许Direct3D执行处理器特定优化的遗留功能(仅涉及较旧的DirectX版本):

在早期版本的DirectX中,有一条路径允许执行顶点处理,称为PSGP。应用程序必须考虑到这条路径,并支持处理器和图形核心上的顶点处理路径。

这样说来,为什么禁用PSGP会修复AMD上的伪像-现代AMD处理器所走的道路可能会以某种方式中断。如何禁用它?我脑海中浮现出两种方式:

可以将D3DCREATE_DISABLE_PSGP_THREADING标志传递给IDirect3D9::CreateDevice。它被定义为:将计算限制到主应用程序线程。如果未设置该标志,则运行时可以在工作线程中执行软件顶点处理和其他计算,以提高多处理器系统上的性能。

遗憾的是,设置该标志并不能解决问题。看起来,尽管旗帜上有“PSGP”的名字,但它不是我们要找的。

DirectX指定两个注册表项在D3D中禁用PSGP,并仅为D3DX禁用PSGP-DisablePSGP和DisableD3DXPSGP。这些标志可以在系统范围或进程范围内设置。有关如何仅为特定进程设置它们的信息,请参阅Rafael Rivera关于启用特定于应用程序的Direct3D标志的指南。

DisableD3DXPSGP似乎是解决该问题的可行解决方案。因此,如果您对下载第三方修复/修改有反感,或者您必须在不对游戏进行任何更改的情况下修复此问题,这是一个非常好的方法。只要您只为质量效果设置该标志,而不是在系统范围内设置,就可以了!

与图形问题一样,PIX可能是人们可以用来诊断这些问题的最有用的工具。我们从英特尔和AMD硬件捕获了类似的场景,并对结果进行了比较。其中一个差异立刻就显而易见--与我以前的项目不同,在以前的项目中,捕获的内容没有携带错误,相同的捕获在不同的PC上看起来会不同(表示驱动程序或d3d9.dll错误),这些捕获会携带错误!换句话说,从安装了英特尔硬件的PC上打开的AMD硬件捕获确实会显示该错误。

英特尔上的AMD捕获与获取它的硬件上的AMD捕获看起来没有什么不同:

由于PIX不“截取屏幕”,而是捕获D3D命令序列并在硬件上执行它们,因此我们可以观察到,执行从AMD框捕获的命令在Intel上执行时会导致相同的错误。

这强烈地表明,这种差异不是由命令执行方式的不同(这就是如何获得GPU特定的错误)造成的,而是由执行哪些命令引起的。

换句话说,几乎可以肯定它不是任何类型的驱动程序错误。取而代之的是,GPU的输入准备方式似乎不知何故被破坏了1。这确实是非常罕见的情况!

在这一点上,找到bug就是找出捕获之间的任何不和谐的差异。这很乏味,但这是唯一可行的方法。

在戳了很长一段时间之后,一个全身抽签的呼唤引起了我的注意:

在英特尔捕获的图像中,此绘图输出角色的大部分身体,以及照明和纹理。在AMD捕捉上,它输出纯黑色模型。这条路看起来很不错。

检查的第一个明显的候选对象是绑定纹理,但是它们看起来很好,并且在捕获过程中是一致的。但是,一些像素着色器常量看起来很奇怪。它们不仅有NAN(不是号码),而且似乎只出现在AMD捕获上,而不出现在英特尔捕获上:

这看起来很有希望--导致奇怪视觉效果的NaN值并不是闻所未闻的。有趣的是,PlayStation3版本的“质量效果2”在RPCS3中有一个非常相似的外观问题,这也与nans有关!

但是,在我们过于兴奋之前,这些值可能只是以前绘制的剩余值,它们可能最终不会用于当前绘制。幸运的是,在这种情况下,可以清楚地看到这些NaN被提交到D3D以用于此特定绘制…。

49652 IDirect3DDevice9::SetVertexShaderConstantF(230,0x3017FC90,4)49653 IDirect3DDevice9::SetVertexShaderConstantF(234,0x3017FCD0,3)49654 IDirect3DDevice9::SetPixelShaderConstantF(10,0x3017F9D4,1)//提交常量c1049655 IDirect3DDevice9::SetPixelShaderConstantF(11,0x3017F9C4,1)//提交常量c1149656 IDirect3DDevice9::SetRenderState(D3DRS_FILLMODE,D3DFILL0.000f)49660 IDirect3DDevice9::TestCooperativeLevel()49661 IDirect3DDevice9::SetIndices(0x296A5770)49662 IDirect3DDevice9::DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,2225,0,3484)//绘制角色模型。

这两个常量似乎直接来自虚幻引擎,从名称来看,它们可能会直接影响照明。对啰!。

游戏中的快速测试进一步证实了这一理论-在Intel机器上,4 nA值的向量从未作为像素着色器常量提交;同时,在AMD机器上,只要玩家进入照明中断的区域,nans就会开始显示!

这是否意味着工作已经完成?不,远非如此,因为找到被破坏的常量只是成功的一半。问题仍然存在,它们是从哪里来的,可以被取代吗?游戏中用零代替NaN值的测试部分解决了这个问题-丑陋的黑色斑点消失了,但人物仍然太暗了:

考虑到这些灯光值对场景可能有多么重要,采用这样的解决方法是不可行的。不过,我们知道我们是在正确的轨道上!

遗憾的是,任何追踪这些常量来源的尝试都指向类似于呈现线程的东西,而不是提交的真实位置。虽然不是不可调试的,但很明显,我们需要尝试一种新的方法,然后才可能花费无限的时间跟踪特定于游戏的和/或特定于UE3的结构之间的数据流。

退后一步,我们意识到我们之前忽略了一些东西。回想一下,要“修复”这个问题,必须添加两个注册表项中的一个-DisablePSGP和DisableD3DXPSGP。假设它们的命名没有误导性,那么DisableD3DXPSGP应该是DisablePSGP的子集,前者仅在D3DX中禁用PSGP,后者在D3DX和D3D中都禁用它。根据这个假设,我们将目光转向了D3DX。

看一看列表,如果我在没有从捕获中获得先验知识的情况下接近它,我会认为D3DXPreprocess Shader或D3DXCompileShader可能是罪魁祸首-着色器可能被错误地优化和/或在AMD上编译,但修复这个问题可能非常具有挑战性。

然而,根据我们目前的知识,有一个函数从这个列表中脱颖而出-D3DXMatrixInverse是唯一可以合理地用于准备像素着色器常量的函数。

我是…。不过,做得不太好。快速浏览d3dx9_31.dll会发现,D3DXMatrixInverse没有触及输出参数,并且返回nullptrif矩阵求逆失败(由于输入矩阵是单一的),但是游戏根本不关心这一点。输出矩阵可能没有初始化,嘘!在游戏中确实会出现求逆的奇异矩阵(最常出现在主菜单中),但无论我们做了什么来使游戏更好地处理它们(例如,将输出置零或将其设置为单位矩阵),视觉效果都不会改变。哦,好吧。

揭穿了这一理论之后,我们回到了PSGP-PSGP在D3DX中到底做了什么?拉斐尔·里维拉(Rafael Rivera)对此进行了研究,其背后的逻辑被证明是相当简单的:

AddFunctions(X86)IF(DisablePSGP||DisableD3DXPSGP){//所有优化已关闭}Else{IF(IsProcessorFeaturePresent(PF_3DNOW_Instructions_Available)){IF((GetFeatureFlags()&;mmx)&;&;(GetFeatureFlags()&;3现在!){AddFunctions(amd_MMX_Available。Sse){AddFunctions(Amdsse)}}Else if(IsProcessorFeaturePresent(PF_XMMI64_Instructions_Available/*SSE2*/)){AddFunctions(Intelsse2)}Else if(IsProcessorFeaturePresent(PF_XMMI_Instructions_Available/*sse*/){AddFunctions(Intelsse)}}。

除非禁用PSGP,否则D3DX会选择优化以利用特定指令集的函数。这是有道理的,并与最初的理论联系在一起。事实证明,D3DX具有针对AMD和3DNow而优化的功能!指令集,所以游戏毕竟是间接的利用了这些,用3DNow!删除指令后,现代AMD处理器采用与英特尔处理器相同的代码路径,即intelsse2。

AMD CPU支持3D Now!采用amd_mmx_3dnow或amd3dnow_amdmmx代码路径,而没有3DNow的CPU采用intelsse2代码路径。

有了这些信息,我们提出了一个假设-AMD SSE2指令可能有问题,用intelsse2路径在AMD上计算矩阵求逆的结果要么太不准确,要么完全不正确。

附注:您可能在想-“嗯,游戏使用的是d3dx9_31.dll,但最新的D3DX9库是d3dx9_43.dll,这肯定要在以后的版本中修复?”。我们试图通过“升级”游戏以链接到最新的DLL来验证这一点,但没有任何改变。

我们编写了一个简单的独立程序来验证矩阵求逆的精度。在“窃听”游戏区的一次简短的游戏会话中,我们将D3DXMatrixInverse的每一次输入和输出记录到一个文件中。后来,该文件被独立的测试程序读取,并再次重新计算结果。为了验证正确性,随后将游戏的输出与测试程序计算的输出进行了比较。

根据从英特尔和AMD芯片收集的数据进行多次尝试后,在启用/禁用psgp的情况下,我们对机器之间的结果进行了交叉检查,结果如下:✔️表示成功(结果相等),❌表示失败(结果不相等)。最后一列指示游戏是否处理好此数据或是否出现故障。我们故意不将浮点数学的不精确性带入账户,而是将结果与memcmp进行比较:

没有SSE2的计算被游戏“接受”,尽管与英特尔SSE2的计算不同。

这就提出了一个问题--AMD SSE2的计算到底出了什么问题,最终导致游戏出现故障?我们没有确切的答案,但它似乎是两个因素的产物:

D3DXMatrixInverse的SSE2实现在数值上可能很差-似乎某些SSE2指令在Intel/AMD上给出了不同的结果(可能是不同的舍入模式),并且该函数的编写方式不能帮助减少不准确性。

游戏代码的编写方式对准确性问题过于敏感。

在这一点上,我们准备提出一个修复方案,将D3DXMatrixInverse替换为D3DX函数的x86变体的重写,然后就到此为止。但是,在继续之前,我还有一个随机的想法-D3DX被弃用,并被替换为DirectXMath。我想,既然我们无论如何都要替换那个矩阵函数,我可以尝试用XMMatrixInverse替换它,使其成为D3DXMatrixInverse的“现代”替代。XMMatrixInverse也使用SSE2指令,因此它对D3DX函数应该是同样优化的,但我几乎可以肯定它会以同样的方式中断。

我们肯定是因为SSE2指令的微小差异而产生的问题,毕竟可能是纯粹的数字问题。尽管也使用了SSE2,XMMatrixInverse在Intel和AMD上都给出了完美的结果。因此,我们重新进行了同样的测试,结果至少可以说是令人惊讶的:

不仅游戏运行良好,而且结果完全相同,并且可以跨机器传输!

考虑到这一点,我们修改了这个错误背后的理论-毫无疑问,这款游戏对问题过于敏感是错误的,但随着额外的测试,D3DX似乎在编写时就考虑到了快速数学,而DirectXMath可能更关心精确的计算。这是有道理的-D3DX是21世纪初的产物,它的编写将性能作为主要优先事项是完全合理的。DirectXMath拥有稍后进行工程设计的“奢侈”,因此它可以将更多的注意力放在精确的、确定性的计算上。

我花了一段时间才到这里,所以我希望你还没有无聊到要死。总而言之,这就是我们所经历的:

我们验证了这款游戏没有使用3DNow!直接执行指令(仅系统DLL可以)。

我们模糊了该函数,发现当使用SSE2指令时,它在Intel和AMD CPU之间不能给出一致的结果。

我们意外地发现XMMatrixInverse没有这个缺陷,是一个可行的替代品。

剩下的唯一一件事就是实现一个适当的替换!这就是出现用于质量效果的SilentPatch的地方。我们认为解决此问题的最干净的方法是提供替换的d3dx9_31.dll,它将质量效果导出的所有函数转发到系统DLL,但D3DXMatrixInverse除外。对于此函数,我们使用XMMatrixInverse开发了一个替代函数。

一个替换的DLL使得安装非常干净和防弹,它已经被证实在Origin和Steam版本的游戏中都能很好地工作。它开箱即用,不需要ASI Loader或任何其他第三方软件。

据我们所知,这款游戏现在看起来完全是它应该的样子,没有任何灯光降级:

可以在Mods&;补丁中下载修改。单击此处直接转到游戏页面:

下载SilentPatch以获得质量效果下载后,您需要做的就是将存档解压到游戏的目录中,仅此而已!不确定如何继续?检查设置说明。

对于感兴趣的人,mod的完整源代码已经在giHub上发布,因此可以免费用作参考:请参阅gihub上的源代码。

从理论上讲,这也可能是d3d9.dll内部的错误,这会使事情变得有点复杂。谢天谢地,情况并非如此。--↩。

当然,假设它们有SSE2指令集,但任何没有这些指令的英特尔CPU都低于质量效应的最低系统要求。-↩