[Перевод] Исправляем графический баг Mass Effect, возникающий на современных процессорах AMD

image


Mass Effect — популярная франшиза научно-популярных RPG. Первая часть сначала была выпущена BioWare в конце 2007 года эксклюзивно для Xbox 360 в рамках соглашения с Microsoft. Спустя несколько месяцев, в середине 2008 года, игра получила порт на PC, разработанный Demiurge Studios. Порт был достойным и не имел заметных недостатков, пока в 2011 году AMD не выпустила свои новые процессоры на архитектуре Bulldozer. При запуске игры на PC с современными процессорами AMD в двух локациях игры (Новерия и Илос) возникают серьёзные графические артефакты:

8b72d00e646b040cc526024294b46394.jpg


Да, выглядит некрасиво.

Хоть это и не делает игру неиграбельной, такие артефакты раздражают. К счастью, решение существует, например, можно отключить освещение консольными командами или модифицировать карты игры, удалив поломанные источники освещения, но, похоже, никто никогда так и не понял полностью причины этой проблемы. В некоторых источниках утверждается, что эту проблему позволяет устранить и мод FPS Counter, но мне не удалось найти информацию о нём: исходный код мода, похоже, не выложен онлайн, а документация о том, как мод исправляет ошибку, отсутствует.

Почему эта проблема так интересна? Баги, возникающие только на оборудовании отдельных производителей, встречаются довольно часто, и в играх они встречаются уже много десятилетий. Однако, по моей информации, это единственный случай, когда проблема с графикой вызвана процессором, а не графической картой. В большинстве случаев проблемы возникают у продуктов определённого производителя GPU и никак не касаются CPU, однако в данном случае всё совсем наоборот. Поэтому эта ошибка уникальна, а значит, её стоит исследовать.
Почитав онлайн-обсуждения, я пришёл к выводу, что проблема, похоже, касается чипов AMD FX и Ryzen. В отличие от более старых процессоров AMD, в этих чипах нет набора команд 3DNow!. Возможно, ошибка никак с этим не связана, но в целом у сообщества геймеров сложился консенсус о том, что это причина бага и что обнаружив процессор AMD, игра пытается использовать эти команды. Учитывая то, что случаи возникновения этого бага на процессорах Intel неизвестны, и что команды 3DNow! использовала только AMD, неудивительно, что сообщество посчитала причиной этот набор команд.

Но в них ли проблема, или ошибку вызывает нечто совершенно другое? Давайте выясним!


Прелюдия


Хотя воссоздать эту проблему чрезвычайно просто, мне долгое время не удавалось оценить её по простой причине — у меня не было под рукой PC с процессором AMD! К счастью, на этот раз я занимаюсь исследованиями не в одиночку — Рафаэль Ривера поддержал меня в процессе изучения, предоставив тестовую среду с чипом AMD, а также поделился своими предположениями и мыслями, пока я делал слепые догадки, как это обычно бывает, когда я ищу источники таких неизвестных проблем.

Так как теперь у нас была тестовая среда, первой, разумеется, мы протестировали теорию cpuid — если люди правы, предполагая, что следует винить команды 3DNow!, то в коде игры должно быть место, проверяющее их наличие, или хотя бы определяющее изготовителя CPU. Однако в таких рассуждений есть ошибка; если бы игра действительно пыталась использовать команды 3DNow! на любом чипе AMD без проверки возможности их поддержки, то она, скорее всего, «вылетела» бы при попытке выполнения недопустимой команды. Более того, краткое исследование кода игры показывает, что она не проверяет возможности CPU. Следовательно, что бы ни было причиной ошибки, не похоже, что она вызвана неправильным определением функцональности процессора, потому что игре она вообще не интересна.

Когда случай начал казаться не подлежащим отладке, Рафаэль сообщил мне о своём открытии — отключение PSGP (Processor Specific Graphics Pipeline) устраняет проблему и все персонажи освещаются правильно! PSGP — не самое подробно задокументированное понятие; если вкратце, то это legacy-функция (касающаяся только старых версий DirectX), позволяющая Direct3D выполнять оптимизации под конкретные процессоры:

В предыдущих версиях DirectX существовал путь выполнения кода под названием PSGP, позволявший производить обработку вершин. Приложения должны были учитывать наличие этого пути и поддерживать путь для обработки вершин ядрами процессора и графической карты.


При таком подходе логично, что отключение PSGP устраняет артефакты на AMD — путь, выбиравшийся современными процессорами AMD был каким-то образом испорчен. Как его отключить? На ум приходят два способа:

  • Можно передать функции IDirect3D9::CreateDevice флаг D3DCREATE_DISABLE_PSGP_THREADING. Он описывается следующим образом:
    Ограничивает вычисления только основным потоком приложения. Если флаг не установлен, то среда выполнения может выполнять программную обработку вершин и другие вычисления в рабочем потоке (worker thread), чтобы повысить производительность в многопроцессорных системах.

    К сожалению, установка этого флага не решает проблему. Похоже, что несмотря на наличие в названии флага букв «PSGP», это не то, что нам нужно.
  • DirectX задаёт два элемента регистра для отключения PSGP в D3D и для отключения PSGP только для D3DX — DisablePSGP и DisableD3DXPSGP. Эти флаги можно устанавливать для всей системы или только для процесса. Подробности о его установке для конкретного процесса см. в руководстве Рафаэля Риверы по включению флагов Direct3D для отдельных приложений.


Похоже, что DisableD3DXPSGP способен решить эту проблему. Следовательно, если вы не любите скачивать сторонние исправления/модификации или хотите устранить проблему, не внося никаких изменений в игру, то это вполне рабочий способ. Если вы установите этот флаг только для Mass Effect, а не для всей системы, то всё будет в порядке!

PIX


Как обычно, при возникновении проблем с графикой их, скорее всего, поможет диагностировать PIX. Мы выполнили захват схожих сцен на оборудовании Intel и AMD, а затем сравнили результаты. Сразу же бросилось в глаза одно различие — в отличие от моих предыдущих проектов, где захваты не записывали в себя баг и один и тот же захват мог выглядеть на разных PC по-разному (что указывает на баг драйвера или d3d9.dll), эти захваты записывали в себя баг! Другими словами, если открыть сделанный на «железе» AMD захват на PC с процессором Intel, то баг будет отображаться.

Захват с AMD на Intel выглядит точно так же, как выглядел на оборудовании, где был сделан:

5c955c0fa9cde2713d834fa7841dcc7b.jpg


О чём это нам говорит?

  • Так как PIX не «делает скриншоты», а захватывает последовательности команд D3D и выполняет их в оборудовании, мы видим, что при выполнении на компьютере с Intel команд, захваченных в системе с AMD получается тот же баг.
  • Это определённо даёт нам понять, что разница вызвана не отличиями в том, как выполняются команды (а именно так и получаются специфичные для конкретных GPU баги), а в том, какие команды выполняются.


Другими словами, это почти точно не баг драйвера. Похоже, что входящие данные, подготавливаемые для GPU, каким-то образом искажаются 1. Это и в самом деле очень редкий случай!

На этом этапе для нахождения бага необходимо обнаружить все расхождения между захватами. Это скучная работа, но иного пути нет.

После долгого изучения захваченных данных моё внимание привлёк вызов отрисовки целого тела персонажа:

a943d980578fbad48c05f14a960d61da.jpg


В захвате Intel этот вызов выводит бОльшую часть тела персонажа вместе с освещением и текстурами. В захвате AMD он выводит сплошную чёрную модель. Похоже, мы взяли нужный след.

Первым очевидным кандидатом на проверку будут соответствующие текстуры, но с ними, похоже, всё в порядке и в обоих захватах они одинаковы. Однако странно выглядят некоторые константы пиксельных шейдеров. В них не только содержатся NaN (Not a Number), но они также есть только в захвате AMD:

b969321ce40ad7bae7ddfe5dcb2e4767.jpg


1.#QO обозначает NaN

Выглядит многообещающе — часто бывает, что значения NaN вызывают странные графические артефакты. Довольно забавно, что в версии Mass Effect 2 для PlayStation 3 была очень похожая проблема в эмуляторе RPCS3, тоже связанная с NaN!

Однако не стоит пока слишком радоваться — это могут быть значения, оставшиеся от предыдущих вызовов и не используемые в текущем. К счастью, в нашем случае чётко видно, что эти NaN передаются в D3D для этой конкретной отрисовки…

49652	IDirect3DDevice9::SetVertexShaderConstantF(230, 0x3017FC90, 4)
49653	IDirect3DDevice9::SetVertexShaderConstantF(234, 0x3017FCD0, 3)
49654	IDirect3DDevice9::SetPixelShaderConstantF(10, 0x3017F9D4, 1) // Submits constant c10
49655	IDirect3DDevice9::SetPixelShaderConstantF(11, 0x3017F9C4, 1) // Submits constant c11
49656	IDirect3DDevice9::SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID)
49657	IDirect3DDevice9::SetRenderState(D3DRS_CULLMODE, D3DCULL_CW)
49658	IDirect3DDevice9::SetRenderState(D3DRS_DEPTHBIAS, 0.000f)
49659	IDirect3DDevice9::SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, 0.000f)
49660	IDirect3DDevice9::TestCooperativeLevel()
49661	IDirect3DDevice9::SetIndices(0x296A5770)
49662	IDirect3DDevice9::DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 2225, 0, 3484) // Draws the character model


…и используемый в этой отрисовке пиксельный шейдер ссылается на обе константы:

// Registers:
//
//   Name                     Reg   Size
//   ------------------------ ----- ----
//   UpperSkyColor            c10      1
//   LowerSkyColor            c11      1


Похоже, обе константы берутся напрямую из Unreal Engine и, судя по их названию, они могут влиять на освещение. Бинго!

Тест в игре подтверждает нашу теорию — на машине с Intel вектор из четырёх значений NaN никогда не передаётся как константы пиксельного шейдера; однако на машине с AMD значения NaN начинают появляться сразу, как игрок входит в место, где ломается освещение!

Значит ли это, что работа сделана? Далеко нет, потому что обнаружение сломанных констант — только половина успеха. По-прежнему остаётся вопрос — откуда они берутся и можно ли их заменить? Во внутриигровом тесте замена значений NaN частично устранила проблему — уродливые чёрные пятна пропали, но персонажи всё равно выглядят слишком тёмными:

c45db6f7909a9e066f7fb9c625e98d5c.jpg


Почти правильно…, но не совсем.

Учитывая то, насколько важными эти значения освещения могут быть для сцены, мы не можем остановиться на таком решении. Однако мы знаем, что на верном пути!

Увы, любые попытки отследить источники этих констант указывали на нечто, напоминающее поток рендера, а не истинное место передачи. Хоть отладка этого и возможна, очевидно, что нам нужно попробовать новый подход, прежде чем тратить потенциально бесконечное количество времени на отслеживание потоков данных между внутриигровыми и/или относящимися к UE3 структурами.


Сделав шаг назад, мы поняли, что ранее кое-что упустили. Вспомним, что для «исправления» игры нужно добавить в реестр одну из двух записей — DisablePSGP и DisableD3DXPSGP. Если предположить, что их названия говорят об их назначении, то DisableD3DXPSGP должен быть подмножеством DisablePSGP, причём первый отключает PSGP только в D3DX, а последний — и в D3DX, и в D3D. Сделав такое предположение, обратим своё взгляд на D3DX.

Mass Effect импортирует набор функций D3DX, компонуя d3dx9_31.dll:

D3DXUVAtlasCreate
D3DXMatrixInverse
D3DXWeldVertices
D3DXSimplifyMesh
D3DXDebugMute
D3DXCleanMesh
D3DXDisassembleShader
D3DXCompileShader
D3DXAssembleShader
D3DXLoadSurfaceFromMemory
D3DXPreprocessShader
D3DXCreateMesh


Если бы я увидел этот список, не зная информации, полученной нами из захватов, то предположил бы, что вероятными виновниками могут быть D3DXPreprocessShader или D3DXCompileShader — шейдеры могли быть неверно оптимизированы и/или скомпилированы на AMD, но их починка может быть безумно сложной задачей.

Однако у нас уже есть знания, поэтому для нас из списка выделяется одна функция — D3DXMatrixInverse является единственной функцией, которую можно использовать для подготовки констант пиксельного шейдера.

Эта функция вызывается только из одного места в игре:

int __thiscall InvertMatrix(void *this, int a2)
{
  D3DXMatrixInverse(a2, 0, this);
  return a2;
}


Однако… оно реализовано не очень хорошо. Краткое изучение d3dx9_31.dll показывает, что D3DXMatrixInverse не трогает выходные параметры и в случае невозможности инверсии матрицы (потому что входящая матрица вырожденная) возвращает nullptr, однако игру это совершенно не волнует. Выходная матрица может остаться неинициализированной, ай-яй! На самом деле инвертирование вырожденных матриц происходит в игре (чаще всего в главном меню), но что бы мы ни делали для того, чтобы игра обрабатывала их лучше (например, обнуляли выходные данные или присваивали им единичную матрицу), графически ничего не менялось. Вот так дела.

Опровергнув эту теорию, мы вернулись к PSGP — что же конкретно PSGP делает в D3DX? Рафаэль Ривера изучил этот вопрос, и логика этого конвейера оказалась довольно простой:

AddFunctions(x86)
if(DisablePSGP || DisableD3DXPSGP) {
  // All optimizations turned off
} else {
  if(IsProcessorFeaturePresent(PF_3DNOW_INSTRUCTIONS_AVAILABLE)) {
    if((GetFeatureFlags() & MMX) && (GetFeatureFlags() & 3DNow!)) {
      AddFunctions(amd_mmx_3dnow)
      if(GetFeatureFlags() & Amd3DNowExtensions) {
        AddFunctions(amd3dnow_amdmmx)
      }
    }
    if(GetFeatureFlags() & 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!, поэтому игра, в конечном итоге, всё-таки косвенно их использует. Современные процессоры AMD, в которых отсутствуют команды 3DNow!, идут по тому же пути, что и процессоры Intel — то есть, по intelsse2.

Подведём итог:

  • При отключении PSGP и Intel, и AMD проходят по обычному пути выполнения кода x86.
  • Процессоры Intel всегда проходят по пути кода intelsse22.
  • Процессоры AMD с поддержкой 3DNow! проходят по пути выполнения кода amd_mmx_3dnow или amd3dnow_amdmmx, а процессоры без 3DNow проходят по intelsse2.


Получив эту информацию, мы выдвинем гипотезу — вероятно, что-то не так с командами AMD SSE2, и результаты инвертирования матрицы, вычисляемые на AMD по пути intelsse2, или слишком неточны, или полностью неверны.

Как нам проверить эту гипотезу? Тестами, естественно!

P.S.: Вы можете подумать «в игре используется d3dx9_31.dll, но последняя библиотека D3DX9 имеет версию d3dx9_43.dll, и, скорее всего, эту ошибку устранили в более новых версиях?». Мы попробовали «проапгрейдить» игру, чтобы она компоновала самую новую DLL, но ничего не изменилось.


Мы подготовили простую независимую программу для проверки точности инвертирования матриц. На протяжении короткой игровой сессии в месте возникновения бага мы записали все входящие и выходящие данные D3DXMatrixInverse в файл. Этот файл считывается независимой тестовой программой, а результаты пересчитываются заново. Для проверки правильности выходные данные игры сравниваются с данными, вычисленными тестовой программой.

После нескольких попыток на основании данных, собранных с чипов Intel и AMD со включенным/отключенным PSGP мы сравнили результаты разных машин. Результаты показаны ниже с указанием успешных (rhlc8urh-ms63o1mrghl8cki-8a.png, результаты равны) и ошибочных (1unxqi1xcuq-cxbldydowfo-0aa.png, результаты не равны) прогонов. В последнем столбце указано, обрабатывает ли игра данные правильно, или «глючит». Мы намеренно не учитываем неточность вычислений с плавающей запятой и сравниваем результаты при помощи memcmp:


Результаты тестов D3DXMatrixInverse

Любопытно, результаты демонстрируют, что:

  • Вычисления с SSE2 не переносятся между машинами с Intel и AMD.
  • Вычисления без SSE2 переносятся между машинами.
  • Вычисления без SSE2 «принимаются» игрой, несмотря на то, что отличаются от вычислений на Intel SSE2.


Поэтому встаёт вопрос: что же конкретно не так с вычислениями с AMD SSE2, из-за чего они приводят к глитчам в игре? У нас нет на него точного ответа, но похоже, что это результат двух факторов:

  • Реализация D3DXMatrixInverse на SSE2 может быть плохой численно — похоже, некоторые команды SSE2 дают разные результаты на Intel/AMD (вероятно, из-за разных режимов округления), а функция написана так, что не может устранять эти неточности.
  • Код написан таким образом, что он слишком чувствителен к проблемам с неточностью.


На данном этапе мы уже готовы создать исправление, которое заменит D3DXMatrixInverse на переписанную x86-вариацию функции D3DX, и на этом закончить. Однако у меня возникла ещё одна случайная мысль — D3DX устарел и был заменён на DirectXMath. Я решил, что если уж мы всё равно хотим заменить эту матричную функцию, то можно поменять её на XMMatrixInverse, которая является «современной» заменой функции D3DXMatrixInverse. В XMMatrixInverse тоже используются команды SSE2, то есть она будет такой же оптимальной, как и с функцией из D3DX, но я был почти уверен, что ошибки в ней будут такие же.

Я быстренько написал код, отправил его Рафаэлю, и…

Он отлично заработал! (?)

В конечном итоге, то, что мы считали проблемой, возникающей из-за небольших отличий команд SSE2, может быть исключительно численной проблемой. Несмотря на то, что XMMatrixInverse тоже использует SSE2, она дала идеальные результаты и на Intel, и на AMD. Поэтому мы заново прогнали те же тесты и результаты оказались неожиданными, если не сказать больше:


Результаты тестов с XMMatrixInverse

Игра не только хорошо работает, но и результаты идеально совпадают и переносятся между машинами!

Учтя это, мы пересмотрели свою теорию о причинах бага — без всяких сомнений, в нём виновата игра, слишком чувствительная к проблемам; однако после проведения дополнительных тестов нам показалось, что D3DX писался под быстрые вычисления, а DirectXMath больше волнует точность вычислений. Это выглядит логично, ведь D3DX — продукт 2000-х и вполне разумно, что его основным приоритетом была скорость. DirectXMath был разработан позже, поэтому авторы могли уделить больше внимания точным, детерминированным вычислениям.


Статья оказалась довольно длинной, надеюсь, вы не устали. Подведём итог тому, что мы сделали:

  • Мы убедились, что игра не использует команды 3DNow! напрямую (их используют только системные DLL).
  • Мы выяснили, что отключение PSGP устраняет проблему на процессорах AMD.
  • При помощи PIX мы нашли виновника — значения NaN в константах пиксельного шейдера.
  • Мы нашли источник этих значений — D3DXMatrixInverse.
  • Мы изучили эту функцию и выяснили, что она не даёт одинаковых результатов на процессорах Intel и AMD, когда используются команды SSE2.
  • Мы случайно обнаружили, что XMMatrixInverse не имеет этого недостатка и является вполне достойной заменой.


Единственное, что нам осталось реализовать — правильную замену! Здесь на сцену выходит SilentPatch for Mass Effect. Мы решили, что самым чистым решением этой проблемы будет создание подменной d3dx9_31.dll, которая будет перенаправлять все экспортированные Mass Effect функции на системную DLL, за исключением функции D3DXMatrixInverse. Для этой функции мы разработали замену на основе XMMatrixInverse.

Заменная DLL обеспечивает очень чистую и надёжную установку, она отлично работает с версиями игры с Origin и Steam. Её можно использовать сразу, без необходимости ASI Loader или любого другого стороннего ПО.

Насколько мы понимаем, теперь игра выглядит так, как должна, без малейшего ухудшения освещения:

image


image


Новерия

image


image


Илос

Загрузки


Модификацию можно скачать в Mods & Patches. Нажмите сюда, чтобы сразу перейти к странице игры:

Скачать SilentPatch for Mass Effect

После скачивания достаточно извлечь архив в папку игры, и на этом всё! Если не знаете, что делать дальше, то прочитайте инструкции по настройке.


Полный исходный код мода опубликован на GitHub и его можно свободно использовать как отправную точку:

Исходники на GitHub

Примечания


  1. Теоретически, это также мог быть баг внутри d3d9.dll, что немного усложнило бы ситуацию. К счастью, это было не так.
  2. Разумеется, если предположить, что у них есть набор команд SSE2, но любой процессор Intel без этих команд намного слабее, чем минимальные системные требования Mass Effect.

© Habrahabr.ru