Porting Detroit: Become Human de Playstation 4 a PC

Introducción



En esta serie de publicaciones, cubriremos la migración de Detroit: Become Human  de PlayStation 4 a PC.



Detroit: Become Human se lanzó para PlayStation 4 en mayo de 2018. Comenzamos a trabajar en la versión para PC en julio de 2018 y la lanzamos en diciembre de 2019. Este es un juego de aventuras con tres personajes jugables y muchas historias. Tiene gráficos de muy alta calidad y la mayor parte de la tecnología gráfica fue desarrollada por el propio Quantic Dream.



El motor 3D tiene excelentes características:



  • Representación realista de personajes.
  • Iluminación PBR.
  • Postprocesamiento de alta calidad como profundidad de campo (DOF), desenfoque de movimiento, etc.
  • Anti-aliasing temporal.






Detroit: Become Human



Desde el principio, el motor 3D del juego fue diseñado específicamente para PlayStation, y no teníamos idea de que luego sería compatible con otras plataformas. Por tanto, la versión para PC supuso un reto para nosotros.



  • El líder de 3D Engine, Ronan Marshalot, y los líderes de 3D Engine, Nicholas Viseri y Jonathan Siret de Quantic Dream , hablarán sobre los aspectos de renderización del juego portado. Explicarán qué optimizaciones podrían trasladarse fácilmente de PlayStation 4 a PC y qué dificultades enfrentaron debido a las diferencias entre plataformas.
  • Lou Kramer es ingeniero de desarrollo tecnológico en AMD . Ella nos ayudó a optimizar el juego, por lo que hablará en detalle sobre la indexación heterogénea de recursos en la PC y, en particular, en las tarjetas AMD.


Elegir una API de gráficos



Ya teníamos una versión OpenGL del motor, que usamos en nuestras herramientas de desarrollo.



Pero no queríamos lanzar el juego en OpenGL:



  • Teníamos muchas extensiones propietarias que no estaban abiertas a todos los fabricantes de GPU.
  • El motor tenía un rendimiento muy bajo en OpenGL, aunque, por supuesto, se podía optimizar.
  • En OpenGL, hay muchas formas de implementar diferentes aspectos, por lo que fue una pesadilla implementar diferentes aspectos correctamente en todas las plataformas.
  • OpenGL . , , .


Debido al uso intensivo de recursos no relacionados, no pudimos portar el juego a DirectX11. No tiene suficientes espacios de recursos y sería muy difícil lograr un rendimiento decente si tuviéramos que rehacer los sombreadores para usar menos recursos.



Elegimos entre DirectX 12 y Vulkan, que tienen un conjunto de características muy similar. Vulkan nos permitiría brindar soporte para Linux y teléfonos móviles, y DirectX 12 brindaría soporte para Microsoft Xbox. Sabíamos que eventualmente necesitaríamos implementar soporte para ambas API, pero tendría más sentido que el puerto se enfocara en una sola API.



Vulkan es compatible con Windows 7 y Windows 8. Ya que queríamos hacer Detroit: Become Humanaccesible para tantos jugadores como sea posible, esto se ha convertido en un argumento muy fuerte. Sin embargo, la migración tomó un año, y este argumento ya no es importante, ¡porque Windows 10 ahora se usa ampliamente!



Varios conceptos de API de gráficos



OpenGL y las versiones anteriores de DirectX tienen un modelo de control de GPU muy simple. Estas API son fáciles de entender y muy adecuadas para el aprendizaje. Indican al conductor que haga mucho trabajo que está oculto al desarrollador. Por lo tanto, será muy difícil optimizar un motor 3D completamente funcional en ellos.



Por otro lado, la API de PlayStation 4 es muy ligera y muy cercana al hardware.



Vulkan está en algún punto intermedio. También tiene abstracciones porque se ejecuta en diferentes GPU, pero los desarrolladores tienen más control. Digamos que tenemos una tarea para implementar la administración de memoria o la caché de sombreado. Como queda menos trabajo para el conductor, ¡tenemos que hacerlo! Sin embargo, desarrollamos proyectos en PlayStation, y por eso es más conveniente para nosotros cuando podemos controlarlo todo.



Dificultades



La CPU de PlayStation 4 es un AMD Jaguar con 8 núcleos. Obviamente más lento que el hardware de PC más nuevo; sin embargo, la PlayStation 4 tiene importantes ventajas, en particular, un acceso muy rápido al hardware. Creemos que la API de gráficos de PlayStation 4 es mucho más eficiente que todas las API de PC. Es muy sencillo y desperdicia pocos recursos. Esto significa que podemos lograr una gran cantidad de llamadas de dibujo por cuadro. Sabíamos que las llamadas de alto consumo pueden ser un problema en PC más lentas.



Otra ventaja importante era que todos los sombreadores de PlayStation 4 podían compilarse por adelantado, lo que significaba que se cargaban casi al instante. En una PC, el controlador debe compilar sombreadores en el momento del arranque: debido a la gran cantidad de configuraciones de controlador y GPU admitidas, este proceso no se puede realizar por adelantado.



Durante el desarrollo de Detroit: Become Human en PlayStation 4, los artistas pudieron crear árboles de sombreado únicos para todos los materiales. Esto produjo una cantidad increíble de sombreadores de vértices y píxeles, por lo que sabíamos desde el principio del puerto que esto sería un gran problema.



Tuberías de sombreado



Como sabemos por nuestro motor OpenGL, la compilación de sombreadores puede llevar mucho tiempo en una PC. Durante la producción del juego, generamos un caché de sombreado basado en el modelo de GPU de nuestras estaciones de trabajo. ¡La generación de un caché de sombreado completo para Detroit: Become Human tomó toda una noche! Todos los empleados obtuvieron acceso a este caché de sombreado por la mañana. Pero el juego aún se ralentizó, porque el controlador necesitaba convertir este código en el código ensamblador nativo del sombreador de GPU.



Resultó que Vulkan maneja este problema mucho mejor que OpenGL.



Primero, Vulkan no usa directamente un lenguaje de sombreado de alto nivel como HLSL, sino que usa un lenguaje de sombreado intermedio llamado SPIR-V. SPIR-V acelera la compilación de sombreadores y facilita la optimización para el compilador de sombreadores de controladores. De hecho, en términos de rendimiento, es comparable al sistema de caché de sombreado de OpenGL.



En Vulkan, los sombreadores deben estar vinculados a la forma VkPipeline. Por ejemplo, VkPipelinepuede crear a partir de un sombreador de vértices y píxeles. También contiene información de estado de renderizado (pruebas de profundidad, esténcil, fusión, etc.) y formatos de destino de render. Esta información es importante para el controlador para que pueda compilar sombreadores de la manera más eficiente posible.



En OpenGL, la compilación de sombreadores no conoce el contexto del uso de sombreadores. El controlador necesita esperar una llamada de dibujo para generar el binario de la GPU, por lo que la primera llamada de dibujo con un nuevo sombreador puede llevar mucho tiempo en la CPU.



En Vulkan, la canalización VkPipelineproporciona un contexto de uso, por lo que el controlador tiene toda la información que necesita para generar el binario de GPU, y la primera llamada de dibujo no desperdicia ningún recurso. Además, podemos actualizar VkPipelineCachesobre la creación VkPipeline.



Inicialmente, intentamos crear VkPipelinesla primera vez que lo necesitamos. Esto provocó una ralentización similar a la situación con los controladores OpenGL. Luego se VkPipelineCacheactualizó y el frenado desapareció hasta la próxima llamada de sorteo.



Entonces predijimos que podríamos crear VkPipelinesen el momento del arranque, pero cuando VkPipelineCacheera irrelevante, era tan lento que la estrategia de cargar en segundo plano no se podía implementar.



Al final, decidimos generar todo VkPipelinedurante el primer lanzamiento del juego. Esto eliminó por completo los problemas de frenado, pero ahora nos enfrentamos a una nueva dificultad: la generación VkPipelineCachetardó mucho tiempo.



Detroit: Become Human contiene aproximadamente 99.500 VkPipeline! El juego usa renderizado hacia adelante, por lo que los sombreadores de material contienen todo el código de iluminación. Por lo tanto, la compilación de cada sombreador puede llevar mucho tiempo.



Se nos ocurrieron varias ideas para optimizar el proceso:



  • , SPIR-V.
  • SPIR-V SPIR-V.
  • , CPU 100% VkPipeline.


Además, Jeff Boltz de NVIDIA sugirió una optimización importante, y en nuestro caso resultó ser muy efectiva.



Muchos son VkPipelinemuy similares. Por ejemplo, algunos VkPipelinepueden tener los mismos sombreadores de vértices y píxeles, difiriendo solo en unos pocos estados de renderizado, como los parámetros de la plantilla. En este caso, el controlador puede tratarlos como una canalización. Pero si los creamos al mismo tiempo, uno de los hilos simplemente quedará inactivo, esperando que el otro complete la tarea. Por su naturaleza, nuestro proceso transmitió todos los similares VkPipelineal mismo tiempo. Para resolver este problema, simplemente cambiamos el orden de clasificación VkPipeline. Los "clones" se colocaron al final y, como resultado, su creación tomó mucho menos tiempo.



Rendimiento de creaciónVkPipelinesvaría mucho. En particular, depende en gran medida del número de subprocesos de hardware disponibles. En AMD Ryzen Threadripper con 64 subprocesos de hardware, puede tardar tan solo dos minutos. Desafortunadamente, en PC débiles, este proceso puede tardar más de 20 minutos.



Este último fue demasiado largo para nosotros. Desafortunadamente, la única forma de reducir aún más este tiempo era reducir el número de sombreadores. Necesitaríamos cambiar la forma en que creamos los materiales para que se compartan la mayor cantidad posible de ellos. Para Detroit: Become Human, esto era imposible, porque los artistas tendrían que rehacer todos los materiales. Planeamos implementar la creación de instancias de material adecuada en el próximo juego, pero ya era demasiado tarde para Detroit: Become Human .



Descriptores de indexación



Para optimizar la velocidad de las llamadas de sorteo en la PC, usamos una extensión de indexación de los descriptores VK_EXT_descriptor_indexing. Su principio es simple: podemos crear un conjunto de descriptores que contengan todos los búferes y texturas utilizados en el marco. Luego podemos acceder a los búferes y texturas a través de índices. La principal ventaja de esto es que los recursos se vinculan solo una vez por cuadro, incluso si se usan en múltiples llamadas de dibujo. Esto es muy similar al uso de recursos no vinculados en OpenGL.



Creamos matrices de recursos para todo tipo de recursos utilizados:



  • Una matriz para todas las texturas 2D.
  • Una matriz para todas las texturas 3D.
  • Una matriz para todas las texturas cúbicas.
  • Una matriz para todos los tampones de material.


Solo tenemos un búfer principal que cambia entre llamadas de dibujo (implementado como un búfer circular) que contiene un índice de descriptor que se refiere al búfer de material deseado y las matrices requeridas. Cada búfer de material contiene índices de las texturas utilizadas.





Gracias a esta estrategia, pudimos mantener una pequeña cantidad de conjuntos de descriptores comunes a todas las llamadas de dibujo y que contenían toda la información necesaria para dibujar el marco.



Optimización de actualizaciones de conjuntos de descriptores



Incluso con una pequeña cantidad de conjuntos de descriptores, actualizarlos seguía siendo un cuello de botella. Actualizar un conjunto de descriptores puede resultar muy costoso si contiene muchos recursos. Por ejemplo, en un cuadro de Detroit: Become Human puede haber más de cuatro mil texturas.



Hemos implementado actualizaciones incrementales para los conjuntos de descriptores, realizando un seguimiento de los recursos que se vuelven visibles e invisibles en el marco actual. Además, esto limita el tamaño de las matrices de descriptores, porque tienen suficiente capacidad para manejar los recursos visibles en el momento actual. El seguimiento de la visibilidad desperdicia pocos recursos porque no utilizamos un algoritmo costoso para calcular las intersecciones conO(n.log(n))... En su lugar, usamos dos listas, una para el marco actual y otra para el anterior. Mover los recursos visibles restantes de una lista a otra y examinar los recursos que quedan en la primera lista ayuda a determinar qué recursos entran y desaparecen de la pirámide.



Los deltas obtenidos durante estos cálculos se almacenan para cuatro fotogramas; utilizamos el almacenamiento en búfer triple y para calcular los vectores de movimiento de los objetos con piel, se requiere un fotograma más. El conjunto de descriptores debe permanecer sin cambios durante al menos cuatro fotogramas antes de que se pueda modificar de nuevo, porque todavía puede ser útil para la GPU. Por tanto, aplicamos deltas a grupos de cuatro fotogramas.



En última instancia, esta optimización redujo el tiempo de actualización de los conjuntos de descriptores de uno a dos órdenes de magnitud.



Butching primitivos



El uso de la indexación de descriptores nos permite agrupar por lotes múltiples primitivas en una sola llamada de extracción usando vkCmdDrawIndexedIndirect. Usamos gl_InstanceIDpara acceder a los índices deseados en el búfer principal. Las primitivas se pueden agrupar en lotes si tienen el mismo conjunto de descriptores, la misma canalización de sombreadores y el mismo búfer de vértice. Esto es muy efectivo, especialmente durante los pases de profundidad y sombra. El número total de llamadas de sorteo se reduce en un 60%.



Con esto concluye la primera parte de la serie de artículos. En la parte 2, el ingeniero tecnológico Lou Kramer hablará sobre la indexación de recursos heterogéneos en PC y tarjetas AMD en particular.



All Articles