Correção de bug de gráficos de Mass Effect em processadores AMD modernos

imagem


Introdução



Mass Effect é uma popular franquia de RPG de ficção científica. A primeira parte foi lançada pela BioWare no final de 2007 exclusivamente para o Xbox 360 sob um acordo com a Microsoft. Poucos meses depois, em meados de 2008, o jogo recebeu uma porta para PC desenvolvida pela Demiurge Studios. A porta era decente e não apresentava falhas perceptíveis até a AMD lançar seus novos processadores de arquitetura Bulldozer em 2011. Ao lançar um jogo em um PC com processadores AMD modernos em dois locais de jogo (Noveria e Ilos), aparecem artefatos gráficos sérios:





Sim, parece feio.



Embora isso não torne o jogo impossível de jogar, esses artefatos são irritantes. Felizmente, existe uma solução, por exemplo, você pode desligar a iluminação com comandos do console ou modificar os mapas do jogo para remover luzes quebradas , mas parece que ninguém nunca entendeu completamente a causa deste problema. Algumas fontes afirmam que o mod FPS Counter também corrige esse problema, mas não consegui encontrar informações sobre ele: o código-fonte do mod não parece ter sido postado online e não há documentação sobre como o mod corrige o erro.



Por que esse problema é tão interessante? Bugs que ocorrem apenas em hardware de certos fabricantes são bastante comuns e são encontrados em jogos há muitas décadas. Porém, de acordo com minhas informações, este é o único caso em que o problema gráfico é causado pelo processador e não pela placa gráfica. Na maioria dos casos, os problemas surgem com os produtos de um determinado fabricante de GPU e não afetam a CPU de forma alguma, mas neste caso, o oposto é verdadeiro. Portanto, esse erro é único e, portanto, vale a pena investigar.



Depois de ler as discussões online, cheguei à conclusão de que o problema parece ser com os chips AMD FX e Ryzen. Ao contrário dos processadores AMD mais antigos, esses chips não possuem o conjunto de instruções 3DNow! ... Talvez o erro não tenha nada a ver com isso, mas em geral, a comunidade de jogadores tem um consenso de que essa é a causa do bug e que, ao encontrar um processador AMD, o jogo tenta usar esses comandos. Considerando que não há casos conhecidos desse bug em processadores Intel, e que o 3DNow! usei apenas AMD, não é de admirar que a comunidade culpasse esse conjunto de instruções como o motivo.



Mas eles são o problema ou algo completamente diferente está causando o erro? Vamos descobrir!



Parte 1 - Pesquisa



Prelúdio



Embora seja extremamente fácil recriar esse problema, por muito tempo não fui capaz de avaliá-lo por um motivo simples - eu não tinha um PC com processador AMD à mão! Felizmente, desta vez não estou fazendo minha pesquisa sozinho - Rafael Rivera me apoiou no processo de aprendizagem ao fornecer um ambiente de teste com um chip AMD e também compartilhou suas suposições e pensamentos enquanto eu fazia suposições cegas, como geralmente é o caso quando procuro fontes de tais problemas desconhecidos.



Como agora tínhamos um ambiente de teste, o primeiro, é claro, testamos a teoriacpuid- se as pessoas estão certas ao assumir que os comandos 3DNow! são os culpados, então deve haver um lugar no código do jogo que verifica sua presença, ou pelo menos identifica o fabricante da CPU. No entanto, há um erro em tal raciocínio; se o jogo realmente tentou usar o 3DNow! em qualquer chip AMD sem verificar a possibilidade de seu suporte, então provavelmente travaria ao tentar executar um comando inválido. Além disso, um breve exame do código do jogo mostra que ele não testa os recursos da CPU. Portanto, seja qual for a causa do erro, ele não parece ser causado por uma identificação incorreta da funcionalidade do processador, porque o jogo não está interessado nele.



Quando o caso começou a parecer impossível de depurar, Raphael me informou sobre sua descoberta - desativando o PSGP(Processor Specific Graphics Pipeline) corrige o problema e todos os caracteres são iluminados corretamente! PSGP não é o conceito mais amplamente documentado; Resumindo, esta é uma função legada (apenas para versões antigas do DirectX) que permite que o Direct3D execute otimizações para processadores específicos:



Nas versões anteriores do DirectX, havia um caminho de execução de código chamado PSGP que permitia o processamento de vértice. Os aplicativos tiveram que levar este caminho em consideração e manter um caminho para o processamento de vértice pelo processador e núcleos gráficos.


Com esta abordagem, é lógico que desabilitar o PSGP elimina artefatos no AMD - o caminho escolhido pelos processadores AMD modernos era de alguma forma falho. Como posso desativá-lo? Duas maneiras vêm à mente:



  • Você pode passar um IDirect3D9::CreateDevicesinalizador para a função D3DCREATE_DISABLE_PSGP_THREADING. É descrito da seguinte forma:



    . , (worker thread), .


    , . , «PSGP», , .
  • DirectX PSGP D3D PSGP D3DX – DisablePSGP DisableD3DXPSGP. . . Direct3D .


Parece ser DisableD3DXPSGPcapaz de resolver este problema. Portanto, se você não gosta de baixar correções / modificações de terceiros ou deseja consertar o problema sem fazer nenhuma alteração no jogo, esta é uma maneira totalmente funcional. Se você definir este sinalizador apenas para Mass Effect, e não para todo o sistema, tudo ficará bem!



PIX



Como de costume, se você tiver problemas com os gráficos, provavelmente ajudará a diagnosticar o PIX. Capturamos cenas semelhantes no hardware Intel e AMD e depois comparamos os resultados. Uma diferença imediatamente chamou minha atenção - ao contrário de meus projetos anteriores, onde as capturas não registravam um bug e a mesma captura podia parecer diferente em PCs diferentes (o que indica um bug de driver ou d3d9.dll), essas capturas escreveu um bug! Em outras palavras, se você abrir uma captura feita no hardware AMD em um PC com processador Intel, o bug será exibido.



A captura da AMD para a Intel é exatamente igual à do hardware de onde foi tirada:





O que isso nos diz?



  • PIX « », D3D , , Intel , AMD .
  • , , ( GPU ), , .


Em outras palavras, isso quase certamente não é um bug de driver. Parece que os dados recebidos que estão sendo preparados para a GPU estão corrompidos de alguma forma 1 . Este é um caso muito raro!



Nesta fase, para encontrar o bug, é necessário encontrar todas as discrepâncias entre as capturas. É um trabalho chato, mas não tem outro jeito.



Após um longo estudo dos dados capturados, minha atenção foi atraída para a chamada para desenhar todo o corpo do personagem:





Na aquisição da Intel, esta chamada produz a maior parte do corpo do personagem junto com iluminação e texturas. Na captura da AMD, ele produz um modelo preto sólido. Parece que pegamos a trilha certa.



O primeiro candidato óbvio para verificação serão as texturas correspondentes, mas elas parecem estar bem e são as mesmas em ambas as capturas. No entanto, algumas constantes de sombreador de pixel parecem estranhas. Eles não contêm apenas NaN (não é um número), mas também são encontrados apenas na captura de AMD:





1. # QO significa NaN.



Parece promissor - os valores NaN costumam causar artefatos gráficos estranhos. Curiosamente , a versão para PlayStation 3 de Mass Effect 2 tinha um problema muito semelhante no emulador RPCS3 , também relacionado ao NaN!



No entanto, não fique muito feliz por agora - esses podem ser valores que sobraram de chamadas anteriores e não foram usados ​​na atual. Felizmente, em nosso caso, é claramente visível que esses NaNs estão sendo passados ​​para o D3D para esta renderização em particular ...



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


... e o sombreador de pixel usado nesta renderização se refere a ambas as constantes:



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


Parece que ambas as constantes vêm diretamente do Unreal Engine e, como o nome sugere, podem afetar a iluminação. Bingo!



O teste do jogo confirma nossa teoria - em uma máquina Intel, um vetor de quatro valores NaN nunca é passado como constantes de pixel shader; no entanto, em uma máquina AMD, os valores NaN começam a aparecer assim que o jogador entra no local onde a iluminação quebra!



Isso significa que o trabalho está feito? Longe disso, porque encontrar constantes quebradas é apenas metade da batalha. A questão ainda permanece - de onde eles vêm e podem ser substituídos? No teste do jogo, alterar os valores NaN corrigiu parcialmente o problema - os pontos pretos feios haviam sumido, mas os personagens ainda parecem muito escuros:





Quase correto ... mas não exatamente.



Dada a importância desses valores de iluminação para uma cena, não podemos parar nesta solução. No entanto, sabemos que estamos no caminho certo!



Infelizmente, todas as tentativas de rastrear as fontes dessas constantes apontavam para algo semelhante a um fluxo de renderização, não o destino real. Embora seja possível depurar isso, é claro que precisamos tentar uma nova abordagem antes de gastar uma quantidade potencialmente infinita de tempo rastreando os fluxos de dados entre as estruturas do jogo e / ou relacionadas ao UE3.



Parte 2 - Olhando mais de perto o D3DX



Dando um passo para trás, percebemos que tínhamos perdido algo antes. Lembre-se de que, para "consertar" o jogo, você precisa adicionar uma das duas entradas ao registro - DisablePSGPe DisableD3DXPSGP. Se assumirmos que seus nomes falam sobre sua finalidade, então DisableD3DXPSGPdeve ser um subconjunto DisablePSGP, com o primeiro desabilitando PSGP apenas em D3DX, e o último em D3DX e D3D. Tendo feito essa suposição, vamos voltar nossos olhos para o D3DX.



Mass Effect importa o recurso D3DX definido ao compor d3dx9_31.dll:



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


Se eu vi esta lista sem saber as informações que recebemos dos capturas, eu diria que o provável culpado poderia ser D3DXPreprocessShaderqualquer um D3DXCompileShader- shaders poderia ser otimizado de forma incorrecta e / ou compilado em AMD, mas corrigi-los pode ser insanamente difícil.



No entanto, já temos conhecimento, portanto, uma função é selecionada da lista para nós - D3DXMatrixInverseé a única função que pode ser usada para preparar as constantes de sombreador de pixel.



Esta função só é chamada de um lugar no jogo:



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


No entanto ... não foi muito bem implementado. Um breve estudo d3dx9_31.dllmostra que ele D3DXMatrixInversenão toca nos parâmetros de saída e, se a inversão da matriz for impossível (porque a matriz de entrada está degenerada), ele retorna nullptr, mas o jogo não liga para nada. A matriz de saída pode permanecer não inicializada, ah-yay! Na verdade, a inversão de matrizes degeneradas ocorre no jogo (mais frequentemente no menu principal), mas tudo o que fizemos para melhorar o jogo (por exemplo, zerar a saída ou atribuir uma matriz de identidade), nada mudou graficamente. É assim que é.



Tendo refutado essa teoria, voltamos ao PSGP - o que exatamente o PSGP faz no D3DX? Rafael Rivera examinou essa questão e a lógica por trás desse pipeline acabou sendo muito simples:



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)
  }
}


Se o PSGP não estiver desabilitado, o D3DX seleciona os recursos que são otimizados para um conjunto de instruções específico. Isso é lógico e nos traz de volta à teoria original. Como se viu, o D3DX tem recursos otimizados para AMD e o conjunto de instruções 3DNow !, então o jogo os usa indiretamente. Os processadores AMD modernos sem 3DNow! As instruções seguem o mesmo caminho que os processadores Intel - isto é, por intelsse2.



Resumir:



  • Quando o PSGP está desativado, a Intel e a AMD seguem o caminho normal de execução de código x86.
  • Os processadores Intel sempre seguem pelo caminho de código intelsse22 .
  • Processadores AMD com 3DNow! passar pelo caminho de execução do código amd_mmx_3dnowou amd3dnow_amdmmxe os processadores sem o 3DNow passarem intelsse2.


Depois de receber essas informações, apresentaremos uma hipótese - provavelmenteintelsse2algo errado com os comandos AMD SSE2 e os resultados de inversão de matriz calculados no AMD ao longo do caminho são muito imprecisos ou completamente incorretos .



Como podemos testar essa hipótese? Testes, claro!



PS: Você pode estar pensando "é usado no jogo d3dx9_31.dll, mas a biblioteca D3DX9 mais recente tem uma versão d3dx9_43.dlle, provavelmente, esse bug foi corrigido nas versões mais recentes?" Tentamos "atualizar" o jogo para vincular a DLL mais recente, mas nada mudou.



Parte 3 - testes independentes



Preparamos um programa simples e independente para testar a precisão da inversão da matriz. Durante uma curta sessão de jogo, no local do bug, gravamos todos os dados de entrada e saída D3DXMatrixInverseem um arquivo. Este arquivo é lido por um programa de teste independente e os resultados são recalculados. A saída do jogo é comparada com os dados calculados pelo programa de teste para verificar a exatidão.



Após várias tentativas com base em dados coletados de chips Intel e AMD com PSGP habilitado / desabilitado, comparamos os resultados de máquinas diferentes. Os resultados são mostrados abaixo, indicando sucesso ( , os resultados são iguais) e falhas (, os resultados não são iguais) é executado. A última coluna indica se o jogo está processando os dados corretamente ou tem "bugs". Ignoramos intencionalmente a imprecisão dos cálculos de ponto flutuante e comparamos os resultados usando memcmp:



Fonte de dados Intel SSE2 AMD SSE2 Intel x86 AMD x86 Os dados são aceitos pelo jogo?
Intel SSE2
AMD SSE2
Intel x86
AMD x86


Resultados do teste D3DXMatrixInverse



Curiosamente, os resultados mostram que:



  • A computação com SSE2 não é portátil entre máquinas Intel e AMD.
  • Computação sem SSE2 é transferida entre máquinas.
  • Computações sem SSE2 são "aceitas" pelo jogo, apesar do fato de serem diferentes das computações no Intel SSE2.


Portanto, surge a pergunta: o que exatamente há de errado em computar com AMD SSE2, por causa do que eles levam a falhas no jogo? Não temos uma resposta exata, mas parece que é o resultado de dois fatores:



  • D3DXMatrixInverse SSE2 — , SSE2 Intel/AMD (, - ), , .
  • , .


Neste estágio, estamos prontos para criar uma correção que substituirá D3DXMatrixInversea variação x86 reescrita da função D3DX, e é isso. No entanto, eu tive outro pensamento aleatório - D3DX é obsoleto e foi substituído pelo DirectXMath . Decidi que, se quisermos substituir essa função de matriz de qualquer maneira, podemos alterá-la para XMMatrixInverse, que é uma substituição "moderna" para a função D3DXMatrixInverse. Ele XMMatrixInversetambém usa comandos SSE2, ou seja, será tão ótimo quanto com a função do D3DX, mas eu tinha quase certeza que os erros seriam os mesmos.



Eu rapidamente escrevi o código, enviei para o Raphael e ...



Funcionou muito bem! (?)



Em última análise, o que consideramos um problema devido às pequenas diferenças nas equipes SSE2 pode ser um problema extremamente numérico. Embora XMMatrixInversetambém use SSE2, deu resultados perfeitos tanto na Intel quanto na AMD. Portanto, executamos os mesmos testes novamente e os resultados foram inesperados, para dizer o mínimo:



Fonte de dados Intel AMD Os dados são aceitos pelo jogo?
Intel
AMD


Resultados de benchmark com XMMatrixInverse



Não só o jogo funciona bem, mas os resultados são perfeitamente combinados e transportados entre as máquinas!



Com isso em mente, revisamos nossa teoria sobre as causas do bug - sem dúvida, um jogo muito sensível a problemas é o culpado; no entanto, depois de realizar testes adicionais, pareceu-nos que o D3DX foi escrito para cálculos rápidos e o DirectXMath está mais preocupado com a precisão dos cálculos. Isso parece lógico, uma vez que o D3DX é um produto dos anos 2000 e faz sentido que a velocidade seja sua prioridade. O DirectXMath foi desenvolvido posteriormente, para que os autores pudessem prestar mais atenção a cálculos determinísticos precisos.



Parte 4 - Juntando tudo



O artigo acabou por ser bastante longo, espero que você não esteja cansado. Vamos resumir o que fizemos:



  • , 3DNow! ( DLL).
  • , PSGP AMD.
  • PIX — NaN .
  • D3DXMatrixInverse.
  • , Intel AMD, SSE2.
  • , XMMatrixInverse .


A única coisa que resta para implementarmos é a substituição correta! É aqui que o SilentPatch para Mass Effect entra em jogo . Decidimos que a solução mais limpa para esse problema era criar um spoofer d3dx9_31.dllque redirecionaria todas as funções do Mass Effect exportadas para a DLL do sistema, exceto a função D3DXMatrixInverse. Para esse recurso, desenvolvemos um XMMatrixInverse.



A DLL de substituição fornece uma instalação muito limpa e confiável e funciona muito bem com as versões Origin e Steam do jogo. Ele pode ser usado imediatamente, sem a necessidade de ASI Loader ou qualquer outro software de terceiros.



Pelo que entendemos, o jogo agora parece como deveria, sem a menor degradação na iluminação:



imagem


imagem


Noveria



imagem


imagem


Ilos



Transferências



A modificação pode ser baixada de Mods & Patches . Clique aqui para ir diretamente para a página do jogo:



Baixe SilentPatch para Mass Effect



Após o download, basta extrair o arquivo para a pasta do jogo e pronto ! Se você não tiver certeza do que fazer a seguir, leia as instruções de configuração .






O código-fonte completo do mod é publicado no GitHub e pode ser usado gratuitamente como ponto de partida:



Código-fonte no GitHub



Notas



  1. Em teoria, também poderia ser um bug dentro do d3d9.dll, o que complicaria um pouco as coisas. Felizmente, não foi esse o caso.
  2. Supondo que eles tenham o conjunto de instruções SSE2, é claro, mas qualquer processador Intel sem essas instruções é muito mais fraco do que os requisitos mínimos do sistema para Mass Effect.


Veja também:






All Articles