Introducción
Mass Effect es una popular franquicia de juegos de rol de ciencia ficción. La primera parte fue lanzada por primera vez por BioWare a finales de 2007 exclusivamente para Xbox 360 en virtud de un acuerdo con Microsoft. Unos meses más tarde, a mediados de 2008, el juego recibió un puerto para PC desarrollado por Demiurge Studios. El puerto era decente y no tenía fallas notables hasta que AMD lanzó sus nuevos procesadores de arquitectura Bulldozer en 2011. Al iniciar un juego en una PC con procesadores AMD modernos en dos ubicaciones de juego (Noveria e Ilos), aparecen graves artefactos gráficos:
Sí, se ve feo.
Si bien esto no hace que el juego sea injugable, estos artefactos son molestos. Afortunadamente, existe una solución, como apagar la iluminación con comandos de consola o modificar los mapas del juego para eliminar las luces rotas , pero parece que nadie ha entendido nunca del todo la causa de este problema. Algunas fuentes afirman que el mod FPS Counter también soluciona este problema, pero no pude encontrar información al respecto: el código fuente del mod no parece estar publicado en línea y no hay documentación sobre cómo el mod corrige el error.
¿Por qué es tan interesante este problema? Los errores que ocurren solo en hardware de ciertos fabricantes son bastante comunes y se han encontrado en los juegos durante muchas décadas. Sin embargo, según mi información, este es el único caso en el que el problema de los gráficos es causado por el procesador y no por la tarjeta gráfica. En la mayoría de los casos, los problemas surgen con productos de un determinado fabricante de GPU y no afectan a la CPU de ninguna manera, pero en este caso ocurre lo contrario. Por lo tanto, este error es único y, por lo tanto, vale la pena investigarlo.
Después de leer las discusiones en línea, he llegado a la conclusión de que el problema parece estar en los chips AMD FX y Ryzen. A diferencia de los procesadores AMD más antiguos, estos chips carecen del conjunto de instrucciones 3DNow! ... Quizás el error no tenga nada que ver con esto, pero en general, existe un consenso en la comunidad de jugadores de que esta es la causa del error y que cuando encuentra un procesador AMD, el juego intenta usar estos comandos. Teniendo en cuenta que no hay casos conocidos de este error en los procesadores Intel, y que 3DNow! solo usó AMD, no es de extrañar que la comunidad culpara a este conjunto de instrucciones como la razón.
Pero, ¿son ellos el problema o es algo completamente diferente la causa del error? ¡Vamos a averiguar!
Parte 1 - Investigación
Preludio
Aunque es extremadamente fácil recrear este problema, durante mucho tiempo no pude evaluarlo por una simple razón: ¡no tenía una PC con un procesador AMD a mano! Afortunadamente, esta vez no estoy investigando solo: Rafael Rivera me apoyó en el proceso de aprendizaje al brindarme un entorno de prueba con un chip AMD y también compartió sus suposiciones y pensamientos mientras yo hacía conjeturas a ciegas, como suele ser el caso cuando busco fuentes de tales problemas desconocidos.
Como ahora teníamos un entorno de prueba, el primero, por supuesto, probamos la teoría
cpuid
- si la gente tiene razón al asumir que los equipos 3DNow! son los culpables, entonces debería haber un lugar en el código del juego que verifique su presencia, o al menos identifique al fabricante de la CPU. Sin embargo, hay un error en tal razonamiento; si el juego realmente intentara usar 3DNow! en cualquier chip AMD sin verificar la posibilidad de su soporte, lo más probable es que se bloquee al intentar ejecutar un comando no válido. Además, un breve examen del código del juego muestra que no prueba las capacidades de la CPU. Por lo tanto, sea cual sea la causa del error, no parece deberse a una identificación errónea de la funcionalidad del procesador, porque el juego no está interesado en él en absoluto.
Cuando el caso comenzó a parecer imposible de depurar, Raphael me informó sobre su descubrimiento: deshabilitar PSGP(Canalización de gráficos específicos del procesador) soluciona el problema y todos los caracteres se iluminan correctamente. PSGP no es el concepto más ampliamente documentado; En resumen, esta es una función heredada (solo para versiones anteriores de DirectX) que permite a Direct3D realizar optimizaciones para procesadores específicos:
En versiones anteriores de DirectX, había una ruta de ejecución de código llamada PSGP que permitía el procesamiento de vértices. Las aplicaciones tenían que tener en cuenta esta ruta y mantener una ruta para el procesamiento de vértices por parte del procesador y los núcleos gráficos.
Con este enfoque, es lógico que deshabilitar PSGP elimine los artefactos en AMD; la ruta elegida por los procesadores AMD modernos fue de alguna manera defectuosa. ¿Cómo puedo desactivarlo? Me vienen a la mente dos formas:
- Puede pasar una
IDirect3D9::CreateDevice
bandera a la funciónD3DCREATE_DISABLE_PSGP_THREADING
. Se describe como sigue:
. , (worker thread), .
, . , «PSGP», , . - DirectX PSGP D3D PSGP D3DX –
DisablePSGP
DisableD3DXPSGP
. . . Direct3D .
Parece
DisableD3DXPSGP
poder resolver este problema. Por lo tanto, si no te gusta descargar correcciones / modificaciones de terceros o quieres solucionar el problema sin realizar ningún cambio en el juego, este es un método completamente funcional. Si configura esta bandera solo para Mass Effect, y no para todo el sistema, ¡todo estará bien!
PIX
Como de costumbre, si tiene problemas con los gráficos, lo más probable es que ayude a diagnosticar el PIX. Capturamos escenas similares en hardware Intel y AMD y luego comparamos los resultados. Una diferencia me llamó la atención de inmediato: a diferencia de mis proyectos anteriores, donde las capturas no registraban un error y la misma captura podía verse diferente en diferentes PC (lo que indica un error de controlador o d3d9.dll), estas capturas escribió un error! En otras palabras, si abre una captura realizada en hardware AMD en una PC con un procesador Intel, se mostrará el error .
La captura de AMD a Intel se ve exactamente igual que en el hardware donde se tomó:
¿Qué nos dice esto?
- PIX « », D3D , , Intel , AMD .
- , , ( GPU ), , .
En otras palabras, es casi seguro que esto no sea un error del controlador. Parece que los datos entrantes preparados por la GPU están dañados de alguna manera 1 . ¡Este es un caso muy raro!
En esta etapa, para encontrar el error, es necesario encontrar todas las discrepancias entre las capturas. Es un trabajo aburrido, pero no hay otra forma.
Después de un largo estudio de los datos capturados, me llamó la atención la llamada para dibujar todo el cuerpo del personaje:
En la toma de control de Intel, esta llamada genera la mayor parte del cuerpo del personaje junto con la iluminación y las texturas. En el agarre de AMD, genera un modelo negro sólido. Parece que tenemos el camino correcto.
El primer candidato obvio para la comprobación serán las texturas correspondientes, pero parecen estar bien y son iguales en ambas capturas. Sin embargo, algunas constantes de sombreado de píxeles se ven extrañas. No solo contienen NaN (no un número), sino que también solo se encuentran en la captura de AMD:
1. # QO significa NaN
Parece prometedor: los valores de NaN a menudo causan artefactos gráficos extraños. Curiosamente , la versión para PlayStation 3 de Mass Effect 2 tenía un problema muy similar en el emulador RPCS3 , ¡también relacionado con NaN!
Sin embargo, no debería estar muy contento por ahora; estos pueden ser valores que quedaron de llamadas anteriores y no se usaron en la actual. Afortunadamente, en nuestro caso, es claramente visible que estos NaN se pasan a D3D para esta representación en 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
... y el sombreador de píxeles utilizado en esta representación se refiere a ambas constantes:
// Registers:
//
// Name Reg Size
// ------------------------ ----- ----
// UpperSkyColor c10 1
// LowerSkyColor c11 1
Parece que ambas constantes provienen directamente de Unreal Engine y, como sugiere su nombre, pueden afectar la iluminación. ¡Bingo!
La prueba en el juego confirma nuestra teoría: en una máquina Intel, un vector de cuatro valores de NaN nunca se pasa como constantes de sombreado de píxeles; sin embargo, en una máquina AMD, los valores NaN comienzan a aparecer tan pronto como el jugador ingresa al lugar donde se rompe la iluminación.
¿Significa esto que el trabajo está hecho? Lejos de eso, porque encontrar constantes rotas es solo la mitad de la batalla. La pregunta sigue siendo: ¿de dónde vienen y se pueden reemplazar? En la prueba del juego, cambiar los valores de NaN solucionó parcialmente el problema: los horribles puntos negros habían desaparecido, pero los personajes todavía se ven demasiado oscuros:
Casi correcto ... pero no del todo.
Dado lo importantes que pueden ser estos valores de iluminación para una escena, no podemos detenernos allí. Sin embargo, sabemos que estamos en el camino correcto.
Por desgracia, cualquier intento de rastrear las fuentes de estas constantes apuntaba a algo parecido a un flujo de procesamiento, no al destino real. Si bien es posible depurar esto, está claro que debemos probar un nuevo enfoque antes de pasar una cantidad de tiempo potencialmente infinita rastreando los flujos de datos entre las estructuras del juego y / o relacionadas con UE3.
Parte 2 - Echando un vistazo más de cerca a D3DX
Dando un paso atrás, nos dimos cuenta de que nos habíamos perdido algo antes. Recuerde que para "arreglar" el juego, debe agregar una de dos entradas al registro,
DisablePSGP
y DisableD3DXPSGP
. Si asumimos que sus nombres indican su propósito, entonces DisableD3DXPSGP
debería ser un subconjunto DisablePSGP
, con el primero deshabilitando PSGP solo en D3DX, y el último en D3DX y D3D. Habiendo hecho esta suposición, volvamos nuestros ojos a D3DX.
Mass Effect importa el conjunto de características D3DX componiendo
d3dx9_31.dll
:
D3DXUVAtlasCreate
D3DXMatrixInverse
D3DXWeldVertices
D3DXSimplifyMesh
D3DXDebugMute
D3DXCleanMesh
D3DXDisassembleShader
D3DXCompileShader
D3DXAssembleShader
D3DXLoadSurfaceFromMemory
D3DXPreprocessShader
D3DXCreateMesh
Si vi esta lista sin conocer la información que recibimos de las capturas, quiero suponer que la causa más probable podría ser
D3DXPreprocessShader
cualquiera D3DXCompileShader
- shaders se podrían optimizar de forma incorrecta y / o compilados en AMD, pero la fijación de ellos puede ser increíblemente difícil.
Sin embargo, ya tenemos conocimiento, por lo que para nosotros se destaca una función de la lista:
D3DXMatrixInverse
es la única función que se puede usar para preparar las constantes del sombreador de píxeles.
Esta función solo se llama desde un lugar en el juego:
int __thiscall InvertMatrix(void *this, int a2)
{
D3DXMatrixInverse(a2, 0, this);
return a2;
}
Sin embargo ... no se ha implementado muy bien. Un breve estudio
d3dx9_31.dll
muestra que D3DXMatrixInverse
no toca los parámetros de salida y, si la inversión de la matriz es imposible (porque la matriz de entrada está degenerada), regresa nullptr
, pero al juego no le importa en absoluto. La matriz de salida puede permanecer sin inicializar, ¡ah-yay! De hecho, la inversión de matrices degeneradas ocurre en el juego (con mayor frecuencia en el menú principal), pero cualquier cosa que hicimos para que el juego las manejara mejor (por ejemplo, poner a cero la salida o asignarle una matriz de identidad), nada cambió gráficamente. Así es como va.
Habiendo refutado esta teoría, volvimos a PSGP: ¿qué hace exactamente PSGP en D3DX? Rafael Rivera analizó esta pregunta y la lógica detrás de esta tubería resultó ser bastante simple:
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)
}
}
Si PSGP no está deshabilitado, entonces D3DX selecciona características que están optimizadas para un conjunto de instrucciones específico. Esto es lógico y nos devuelve a la teoría original. Al final resultó que, D3DX tiene características optimizadas para AMD y el conjunto de instrucciones 3DNow !, por lo que el juego finalmente las usa indirectamente. Los procesadores AMD modernos que carecen de instrucciones 3DNow! Siguen el mismo camino que los procesadores Intel, es decir, por
intelsse2
.
Resumir:
- Cuando PSGP está deshabilitado, tanto Intel como AMD siguen la ruta de ejecución de código normal
x86
. - Los procesadores Intel siempre pasan por la ruta de código
intelsse2
2 . - Procesadores AMD con 3DNow! pasan por la ruta de ejecución del código
amd_mmx_3dnow
oamd3dnow_amdmmx
, y los procesadores sin 3DNow pasanintelsse2
.
Habiendo recibido esta información, presentaremos una hipótesis: probablemente haya algo mal con los comandos AMD SSE2, y los resultados de inversión de matriz calculados en AMD a lo largo del camino
intelsse2
son demasiado inexactos o completamente incorrectos .
¿Cómo podemos probar esta hipótesis? ¡Pruebas, por supuesto!
PD: Puede que
d3dx9_31.dll
estés pensando "se usa en el juego , pero la última biblioteca D3DX9 tiene una versión d3dx9_43.dll
y, lo más probable, este error se corrigió en versiones más nuevas". Intentamos "actualizar" el juego para vincular la DLL más reciente, pero nada cambió.
Parte 3 - Pruebas independientes
Hemos preparado un programa simple e independiente para probar la precisión de la inversión de la matriz. Durante una sesión de juego corta, en la ubicación del error, registramos todos los datos entrantes y salientes
D3DXMatrixInverse
en un archivo. Este archivo es leído por un programa de prueba independiente y los resultados se vuelven a calcular. La salida del juego se compara con los datos calculados por el programa de prueba para verificar la exactitud.
Después de varios intentos basados en datos recopilados de chips Intel y AMD con PSGP habilitado / deshabilitado, comparamos los resultados de diferentes máquinas. Los resultados se muestran a continuación, indicando el éxito ( , los resultados son iguales) y los fracasos (, los resultados no son iguales) se ejecuta. La última columna indica si el juego está procesando los datos correctamente o tiene "errores". Intencionalmente ignoramos la inexactitud de los cálculos de punto flotante y comparamos los resultados usando
memcmp
:
Fuente de datos | Intel SSE2 | AMD SSE2 | Intel x86 | AMD x86 | ¿Los datos son aceptados por el juego? |
---|---|---|---|---|---|
Intel SSE2 | ️ | ️ | |||
AMD SSE2 | ️ | ||||
Intel x86 | ️ | ️ | ️ | ||
AMD x86 | ️ | ️ | ️ |
Resultados de la prueba D3DXMatrixInverse
Curiosamente, los resultados muestran que:
- La informática con SSE2 no es portátil entre máquinas Intel y AMD.
- La computación sin SSE2 se transfiere entre máquinas.
- Los cálculos sin SSE2 son "aceptados" por el juego, a pesar de que difieren de los cálculos en Intel SSE2.
Por lo tanto, surge la pregunta: ¿qué es exactamente lo que está mal con los cálculos con AMD SSE2, debido a lo que conducen a fallas en el juego? No tenemos una respuesta exacta, pero parece que es el resultado de dos factores:
-
D3DXMatrixInverse
SSE2 — , SSE2 Intel/AMD (, - ), , . - , .
En esta etapa, estamos listos para crear una solución que reemplazará
D3DXMatrixInverse
la variación x86 reescrita de la función D3DX, y eso es todo. Sin embargo, tuve otro pensamiento al azar: D3DX es obsoleto y ha sido reemplazado por DirectXMath . Decidí que si queremos reemplazar esta función de matriz de todos modos, podemos cambiarla a XMMatrixInverse
, que es un reemplazo "moderno" de la función D3DXMatrixInverse
. Se XMMatrixInverse
utiliza también comandos SSE2, que es, va a ser tan óptima como con la función de D3DX, pero estaba casi seguro de que los errores en ella serían los mismos.
Rápidamente escribí el código, se lo envié a Raphael y ... ¡
Funcionó muy bien! (?)
En última instancia, lo que percibimos como un problema debido a pequeñas diferencias en los equipos SSE2 puede ser un problema extremadamente numérico. Aunque
XMMatrixInverse
también usa SSE2, dio resultados perfectos tanto en Intel como en AMD. Por lo tanto, volvimos a ejecutar las mismas pruebas y los resultados fueron inesperados, por decir lo menos:
Fuente de datos | Intel | AMD | ¿Los datos son aceptados por el juego? |
---|---|---|---|
Intel | ️ | ️ | ️ |
AMD | ️ | ️ | ️ |
Resultados de referencia con XMMatrixInverse
No solo el juego funciona bien, sino que los resultados se combinan perfectamente y se transfieren de una máquina a otra.
Con esto en mente, revisamos nuestra teoría sobre las causas del error; sin duda, la culpa es de un juego demasiado sensible a los problemas; sin embargo, después de realizar pruebas adicionales, nos pareció que D3DX estaba escrito para cálculos rápidos y DirectXMath está más preocupado por la precisión de los cálculos. Esto parece lógico, ya que D3DX es un producto de la década de 2000 y tiene sentido que la velocidad sea su máxima prioridad. DirectXMath se desarrolló más tarde, por lo que los autores pudieron prestar más atención a cálculos precisos y deterministas.
Parte 4 - Poniéndolo todo junto
El artículo resultó ser bastante largo, espero que no esté cansado. Resumamos lo que hemos hecho:
- , 3DNow! ( DLL).
- , PSGP AMD.
- PIX — NaN .
- —
D3DXMatrixInverse
. - , Intel AMD, SSE2.
- ,
XMMatrixInverse
.
¡Lo único que nos queda por implementar es el reemplazo correcto! Aquí es donde entra en juego SilentPatch para Mass Effect . Decidimos que la solución más limpia a este problema era crear un spoofer
d3dx9_31.dll
que redirigiría todas las funciones de Mass Effect exportadas a la DLL del sistema, excepto la función D3DXMatrixInverse
. Para esta función, hemos desarrollado un XMMatrixInverse
.
La DLL de reemplazo proporciona una instalación muy limpia y confiable y funciona muy bien con las versiones Origin y Steam del juego. Se puede usar de inmediato, sin necesidad de ASI Loader o cualquier otro software de terceros.
Por lo que entendemos, el juego ahora se ve como debería, sin la menor degradación en la iluminación:
Noveria
Ilos
Descargas
La modificación se puede descargar desde Mods & Patches . Haz clic aquí para ir directamente a la página del juego:
Descarga SilentPatch para Mass Effect
Después de descargar, simplemente extrae el archivo en la carpeta del juego, ¡y listo ! Si no está seguro de qué hacer a continuación, lea las instrucciones de configuración .
El código fuente completo del mod se publica en GitHub y se puede usar libremente como punto de partida:
Fuente en GitHub
Notas
- En teoría, también podría ser un error dentro de d3d9.dll, lo que complicaría un poco las cosas. Afortunadamente, este no fue el caso.
- Suponiendo que tengan el conjunto de instrucciones SSE2, por supuesto, pero cualquier procesador Intel sin estas instrucciones es mucho más débil que los requisitos mínimos del sistema para Mass Effect.
Ver también: