修复《远哭》(2018)中的Direct3D9错误
Fixing a Direct3D9 bug in Far Cry (2018)

原始链接: https://houssemnasri.github.io/2018/07/07/farcry-d3d9-bug/

## SilentPatch 修复《远哭》中的破碎反射 经典游戏《远哭》以其突破性的图形效果而闻名,但在现代系统中,其水面反射出现了问题——陆地无法正确反射,从而降低了视觉冲击力。虽然可以通过 WineD3D 解决,但会严重影响性能(高达 75% 的帧率损失)。 一位开发者进行了调查,发现问题源于 DirectX 9 实现中的回归,很可能是在 Windows Vista 的显示驱动模型中引入的。《远哭》独特地使用了用户自定义的裁剪平面来处理水面反射,而现代硬件现在会模拟这一特性。这种模拟导致水下几何体被错误地裁剪,从而导致反射出现缺陷。 解决方案是识别并重新应用每个绘制调用的裁剪平面数据。此修复恢复了正确的反射,*且*没有任何性能损失。源代码已在 GitHub 上公开。 **要快速修复,请从 [链接至变更日志/补丁] 下载 SilentPatch,并将其简单地解压到您的《远哭》游戏目录中。**

一个 Hacker News 的讨论围绕着一篇 2018 年的博客文章,详细描述了修复初代《Far Cry》中 Direct3D9 错误的方案。 这篇最初由 CookiePLMonster 撰写的文章,似乎被另一位用户镜像到了另一个 GitHub.io 子域名下,引发了关于动机的疑问——可能是在利用现有的网站模板。 用户注意到复制的网站链接指向了镜像用户的 Patreon 页面。 虽然该修复方案在当时意义重大,但评论者指出,如今像 DXVK(Direct3D 到 Vulkan 的包装器)和 WineD3D 这样的现代解决方案,能提供更好的游戏运行性能。 原始文章强调了使用 WineD3D 时性能下降的问题(高达 75%!)。 讨论还深入探讨了该错误的具体技术细节,涉及自定义裁剪平面和 API 规范,吸引了对游戏保护和底层图形编程感兴趣的人。 一些评论员指出,这个错误很微妙,不太可能困扰到普通玩家。
相关文章

原文

TL;DR - if you are not interested in an in-depth overview of what was wrong with the game and how it was fixed, just follow the link to check out a concise changelog and grab SilentPatch for Far Cry:
Download SilentPatch for Far Cry
Upon downloading, all you need to do is to extract the archive to game’s directory and that’s it!

Far Cry (developed by Crytek) once a game considered an example of visual fidelity and de facto a benchmark of then-modern PCs, turns out not to be free of issues. The main issue bothering people for years were broken water reflections - landmass would not reflect on water if the game is played on anything newer than Windows XP:

You can see trees and rocks are reflecting fine - but bigger chunks of land are not, so reflections look far less impressive than they do on XP!

Community has since found a way to fix this issue - it is possible to use WineD3D, a Direct3D to OpenGL wrapper for Windows, and then everything looks fine. However, it comes at a price – performance can be lowered by as much as 75%! That can result in unacceptable framerates even on modern PCs.

Can we do better? Of course!

Of course, as with any graphical related issues, usage of PIX graphics debugger could not be overstated. For starters, I captured several frames on both Windows 10 and Windows XP virtual machine. Nothing seemed out of place – XP frames indeed display correctly, while Win10 frames display with a bug.

However, one test proved this issue to be much weirder than anyone could have guessed. I attempted to capture a frame on a XP virtual machine, and then preview it on Windows 10. This didn’t have a very high chance of succeeding, since PIX frames are strictly bound to user’s current GPU and drivers – so previewing captures from different PCs is not always possible. However, in this case it worked and revealed something bizarre…

That’s how a texture for water reflections displays when previewed in PIX on Windows XP:

Compare it with the very same texture previewed on Windows 10:

What gives!? This is exactly the same capture, exactly the same sequence of D3D calls leading to a different result! What does that mean?

With almost absolute certainty, what is being observed here is a regression in D3D9 implementation – it is very possible that it was introduced with Windows Vista display driver model (WDDM), a replacement for older Windows XP display driver model (XPDM). This should not have happened, but for some reason it did. Why? Since it was never noticed by Microsoft nor any game developers, it hints that the feature which broke is so obscure and minor so very little applications (Far Cry could be one of the few, or even the only game to rely on it) so nobody ever cared.

With this in mind, I could start looking through PIX capture looking for anything which I wasn’t familiar with.

Turns out, Far Cry indeed uses something which I wasn’t even aware of, until now – Clip Planes.

Normally, only front (near) and back (far) planes are used when rendering to cut off too close or too distant geometry:

However, user clip planes allow developers to further customize the shape of the view frustum. Sounds handy, but turns out almost no one ever needed it! The feature was underused so much so it is not natively supported by modern hardware anymore – instead, it’s emulated.

Let’s see how the game looks if we completely disable this feature:

That’s… better. Landmass is reflecting, but we can also spot artifacts on the water. If we then preview water reflection map, it becomes obvious what those are:

Turns out those are underwater models being drawn as a part of water reflection! It makes perfect sense – developers set up a clip plane adjacent to water surface, so anything below it gets clipped and not included in the reflection map. However, if clip planes are broken, we can assume more geometry is clipped than it was needed.

You might ask – why is this feature broken? MSDN may have the answer – notice the fragment from SetClipPlane documentation page I emphasised:

The coefficients that this method sets take the form of the general plane equation. If the values in the array at pPlane were labeled A, B, C, and D in the order that they appear in the array, they would fit into the general plane equation so that Ax + By + Cz + Dw = 0. A point with homogeneous coordinates (x, y, z, w) is visible in the half space of the plane if Ax + By + Cz + Dw >= 0. Points that exist behind the clipping plane are clipped from the scene.

When the fixed function pipeline is used the plane equations are assumed to be in world space. When the programmable pipeline is used the plane equations are assumed to be in the clipping space (the same space as output vertices).

That is a fundamental difference, because same set of coordinates has a completely different meaning depending on context. Now, if shader-based (programmable) pipeline assumes coordinates to be in clipping space (and thus change space for each rendered geometry), what if we assume they somehow invalidate and re-apply them before each draw?

It works! It was a very long shot, but turned out to be accurate enough! Saving requested clip planes and re-applying them for each draw (if they are enabled, of course) seems to solve the issue completely. Awesome!

At this point, I decided it’s enough to call it fixed and release a patch. Unlike WineD3D, this does not affect performance whatsoever – and the amount of added D3D calls is negligible, because clip planes are really not used this often.

However, one question remains unanswered – what really invalidates clip planes? They are not invalidated after each draw, so it can be any other D3D call setting geometry up – shaders, shader constants, some render state… As much as I’d like to know, figuring it out would take way too much time to be feasible, especially since the current fix has proven to have absolutely no disadvantages.


For those interested, full source code of the patch has been published on GitHub, so it can be freely used as a point of reference:
See source on GitHub

联系我们 contact @ memedata.com