Noticias

3 años de metal – Roblox Blog

Hace tres años, migramos nuestro renderizador a Metal. No tomó mucho tiempo, fue increíble y funcionó muy bien en iOS. Así que escribimos un artículo explicando cómo tomamos nuestra decisión y cómo resultó (spoilers: ¡muy bien!). La mayor parte de esa retrospectiva original aún se aplica, pero hoy Metal está en mejor forma que nunca, por lo que hemos decidido volver a publicarlo con nuestra actualización de tres años.

Entonces, retrocedamos en el tiempo, supongamos que es diciembre de 2016 y acabamos de lanzar una versión de nuestro renderizador Metal en iOS.

¿Por qué metálico?

Cuando Apple anunció Metal en la WWDC de 2014, mi primera reacción fue ignorarlo. Solo estaba disponible en el hardware más reciente que la mayoría de nuestros usuarios no tenían, y aunque Apple dijo que soluciona los problemas de rendimiento de la CPU, la optimización para el mercado más pequeño significaría que la brecha entre los dispositivos más rápidos y los más lentos se ampliaría aún más. En ese momento, ejecutábamos OpenGL ES 2 solo en Apple y también comenzábamos a migrar a Android.

Avance rápido dos años y medio, así es como se ve la cuota de mercado del metal para nuestros usuarios:

Es mucho más atractivo que antes. Todavía es cierto que la implementación de Metal no ayuda a los dispositivos más antiguos, pero el mercado GL en iOS sigue reduciéndose, y el contenido que ejecutamos en esos dispositivos más antiguos a menudo es diferente del contenido que se ejecuta en los dispositivos más nuevos, por lo que tiene sentido poner esfuerzo para hacerlo más rápido. Dado que su código iOS Metal funcionará en Mac con muy pocas modificaciones, podría ser una buena idea usarlo también en Mac, incluso si está enfocado en dispositivos móviles (actualmente solo enviamos versiones Metal en iOS).

Creo que vale la pena analizar la cuota de mercado con un poco más de detalle. En iOS, admitimos Metal para iOS 8.3+; aunque algunos usuarios no pueden ejecutar Metal debido a las restricciones de la versión del sistema operativo, la mayoría del 25 % que aún ejecuta GL solo usa dispositivos más antiguos con hardware SGX. Tampoco tienen características de OpenGL ES 3, y solo estamos ejecutando una ruta de renderizado de gama baja allí (aunque nos encanta que todos los dispositivos se vuelvan Metal; afortunadamente, la división GL/Metal no solo mejorará). En la Mac, la API de Metal es más nueva y el sistema operativo juega un papel bastante importante: debe usar OSX 10.11+ para usar Metal y la mitad de nuestros usuarios solo tienen un sistema operativo más antiguo, eso es menos hardware que software (95% de nuestro Los usuarios de Mac ejecutan OpenGL 3.2+).

Entonces, dada la participación de mercado, aún tenemos opciones que no involucran la migración a Metal. Una de ellas es simplemente usar MoltenGL, que usaría el código OpenGL que ya tenemos, pero que se supone que es más rápido; otra es portar a Vulkan (para un mejor rendimiento en PC y posiblemente en Android) y usar MoltenVK. Evalué brevemente MoltenGL y no me emocionaron demasiado los resultados: se necesitó un poco de esfuerzo para que nuestro código funcionara y, aunque el rendimiento fue un poco mejor en comparación con OpenGL estándar, esperaba más. En cuanto a MoltenVK, creo que está mal intentar implementar una API de bajo nivel como una capa encima de otra; corre el riesgo de obtener una falta de coincidencia de impedancia que dará como resultado un rendimiento subóptimo; tal vez sea mejor que el alto nivel API que estaba usando antes, pero es poco probable que sea lo más rápido posible, ¡por eso elige primero una API de bajo nivel! Otro aspecto importante es que la implementación de Metal es mucho más simple que Vulkan, más sobre eso más adelante, por lo que, en cierto sentido, preferiría un envoltorio Metal -> Vulkan en lugar de Vulkan -> Metal.

También se debe tener en cuenta que aparentemente en iOS 10 en los últimos iPhones no hay un controlador GL: GL se implementa sobre Metal. Lo que significa que usar OpenGL realmente solo le ahorra un poco de esfuerzo de desarrollo, no tanto, dado que la promesa de "escribir una vez, ejecutar en cualquier lugar" de que OpenGL no funciona realmente en dispositivos móviles.

Portage

En general, la migración a Metal fue pan comido. Tenemos mucha experiencia con diferentes API de gráficos, que van desde API de alto nivel como Direct3D 9/11 hasta API de bajo nivel como PS4 GNM. Esto brinda una ventaja única de poder usar cómodamente una API como Metal que es simultáneamente de un nivel razonablemente alto, pero también deja algunas tareas como la sincronización de CPU-GPU al desarrollador de la aplicación.

El único obstáculo fue realmente compilar nuestros sombreadores: una vez que se hizo y llegó el momento de escribir el código, se hizo evidente que la API es tan simple y se explica por sí misma que el código prácticamente se escribió solo. Obtuve el puerto que representaba la mayoría de las cosas de manera subóptima en aproximadamente 10 horas en un solo día, y pasé dos semanas más limpiando el código, solucionando problemas de validación, perfilando y optimizando y puliendo en general. Obtener una implementación de API en esa cantidad de tiempo dice mucho sobre la calidad de la API y el conjunto de herramientas. Creo que hay varios aspectos que contribuyen:

  • Puede desarrollar el código de forma incremental, con buenos comentarios en cada paso. Notre code a commencé par ignorer toutes les synchronisations CPU-GPU, étant vraiment sous-optimal sur certaines parties de la configuration de l'état, utilisant le suivi de référence intégré pour les ressources et ne jamais exécuter CPU et GPU en parallèle pour éviter de encontrar problemas; la fase de optimización/pulido luego convirtió eso en algo que pudiéramos enviar, sin perder nunca la renderización en el proceso.
  • Las herramientas están ahí para usted, funcionan y funcionan bien. Esto no es una gran sorpresa para las personas acostumbradas a Direct3D 11, pero esta es la primera vez en un dispositivo móvil donde tuve un generador de perfiles de CPU, un generador de perfiles de GPU, un depurador de GPU y una capa de validación de API de GPU, todos funcionando bien en conjunto, capturando la mayoría problemas durante el desarrollo y ayudar a optimizar el código.
  • Aunque la API tiene un nivel ligeramente más bajo que Direct3D 11 y deja algunas decisiones clave de bajo nivel en manos del desarrollador (como la configuración o el tiempo del pase de procesamiento), todavía usa un modelo de recurso tradicional donde cada recurso tiene ciertas "marcas de uso". Se creó con, pero no requiere, barreras de canalización ni transiciones de diseño, y un modelo de vinculación tradicional en el que cada etapa de sombreado tiene varias ranuras en las que puede asignar recursos libremente. Ambos son familiares, fáciles de entender y requieren una cantidad muy limitada de código para comenzar rápidamente.

Otra cosa que ayudó es que nuestra interfaz de API estaba lista para API similares a Metal: es muy liviana pero expone suficientes detalles (como pases de renderizado) para escribir fácilmente una implementación de alto rendimiento. En ningún momento de nuestra implementación necesité guardar/restaurar el estado (muchas API sufren de esto, especialmente debido al tratamiento de la configuración del destino de procesamiento como cambios de estado y recursos/enlace de estado persistente) o tomar decisiones complicadas sobre la vida útil/tiempo de los recursos. La única pieza de código 'complicada' que se necesita para la representación es la que crea el estado de la canalización de representación mediante el hash de los bits que se necesitan para crear objetos de estado de una canalización que no forman parte de nuestra abstracción de API. Incluso eso es bastante simple y rápido. Escribiré más sobre nuestra interfaz API en un artículo separado.

Entonces, una semana para compilar los sombreadores, dos semanas para obtener una implementación optimizada y pulida1: ¿cuáles son los resultados? Los resultados son excelentes: Metal cumple absolutamente sus promesas de rendimiento. Por un lado, el rendimiento de despacho en un solo subproceso es notablemente mejor que con OpenGL (reduciendo la parte de despacho de dibujo de nuestro marco de renderizado de 2 a 3 veces dependiendo de la carga de trabajo), y dado que nuestra implementación de OpenGL está bastante bien ajustada en términos de reducir la configuración de estado redundante y jugar con el controlador utilizando rutas rápidas. Pero no se detiene allí: el subprocesamiento múltiple en Metal es fácil de usar siempre que su código de renderizado esté listo para ello. Todavía no hemos pasado a la distribución de impresión por subprocesos, pero ya estamos convirtiendo otras partes que preparan los activos para salir del subproceso de procesamiento, lo que, a diferencia de OpenGL, es bastante sencillo.

Más allá de eso, Metal nos permite abordar otros problemas de rendimiento al proporcionar herramientas confiables y de fácil acceso. Una de las partes centrales de nuestro código de renderizado es el sistema que calcula los datos de iluminación en la CPU en el espacio mundial y los carga en regiones de una textura 3D (que debemos emular en el hardware OpenGL ES 2). Las actualizaciones son parciales, por lo que no podemos duplicar toda la textura y depender de ella; sin embargo, el controlador implementa glTexSubImage3D. En un momento, intentamos usar PBO para mejorar el rendimiento de las actualizaciones, pero nos encontramos con importantes problemas de estabilidad en todos los ámbitos, tanto en Android como en iOS. En Metal, hay dos formas integradas de descargar una región: MTLTexture.replaceRegion, que puede usar si la GPU no está leyendo la textura actualmente, o MTLBlitCommandEncoder (copyFromBufferToTexture o copyFromTextureToTexture), que puede descargar la región de forma asíncrona justo a tiempo para la GPU comienza a usar la GPU de textura.

Ambos métodos eran más lentos de lo que me gustaría: el primero no estaba realmente disponible porque necesitábamos admitir actualizaciones parciales eficientes, y funcionaba en la CPU solo usando lo que parecía una implementación de traducción de dirección muy lenta. El segundo funcionó, pero parecía usar una serie de blits 2D para llenar la textura 3D que eran bastante costosos para configurar los controles laterales de la CPU y también tenían una sobrecarga de GPU muy alta por alguna razón. Si fuera OpenGL, sería demasiado; de hecho, el rendimiento de ambos métodos coincidió aproximadamente con el costo observado de una actualización similar en OpenGL. Afortunadamente, al ser Metal, tiene fácil acceso a los sombreadores de cómputo, y un sombreador de cómputo súper simple nos dio la capacidad de hacer Buffer -> 3D Texture Download, que fue muy rápido en la CPU y GPU y básicamente solucionó nuestros problemas de rendimiento en esta parte del código. para bien2:

Como comentario general final, mantener el código de Metal es igual de fácil: todas las características adicionales que tuvimos que agregar hasta ahora fueron más fáciles de agregar que en cualquier otra API que admitamos, y espero que esta tendencia continúe. Hubo un poco de preocupación de que agregar una API adicional requeriría un mantenimiento constante, pero en comparación con OpenGL, en realidad no requiere mucho trabajo; de hecho, dado que ya no tendremos que admitir OpenGL ES 3 en iOS, eso significa que también podemos simplificar el código OpenGL que tenemos.

Estabilidad

Hoy en iOS, Metal se siente muy estable. No sé cómo era en el lanzamiento en 2014, o cómo se ve hoy en Mac, pero los controladores y las herramientas para iOS parecen bastante sólidos.

Tuvimos un problema con el controlador en iOS 10 relacionado con la carga de sombreadores compilados con Xcode 7 (que solucionamos actualizando a Xcode 8) y un bloqueo del controlador en iOS 9 que resultó ser el resultado de un uso inadecuado de la API NextDrawable. Aparte de eso, no hemos visto ningún error de comportamiento o bloqueo: para una API de Metal relativamente nueva, ha sido muy sólida en todos los ámbitos.

Además, las herramientas que obtienes con Metal son variadas y ricas; en particular, puede utilizar:

  • Una capa de validación bastante completa que identificará problemas comunes con el uso de la API. Es básicamente como la depuración de Direct3D, que es familiar para Direct3D pero bastante desconocido en el terreno de OpenGL (en teoría, se supone que ARB_debug_callback resuelve este problema, en la práctica, en su mayoría no está disponible y cuando no es muy útil)
  • Un depurador de GPU funcional que muestra todos los comandos que ha enviado con su estado, contenido de destino de procesamiento, contenido de textura, etc. No sé si tiene un depurador de sombreado que funcione porque nunca lo necesité, e inspeccionar el búfer podría ser un poco más fácil, pero en su mayoría hace el trabajo.
  • Un perfilador de GPU funcional que muestra estadísticas de rendimiento por pasada (tiempo, ancho de banda) y también tiempo de ejecución por sombreador. Dado que la GPU es un mosaico, no puede esperar tiempos de llamada cero, por supuesto. Tener este nivel de visibilidad, especialmente dada la falta total de información de tiempo de GPU en las API de gráficos en iOS, es excelente.
  • Un seguimiento funcional de la línea de tiempo de CPU/GPU (Metal System Trace) que muestra la programación de la carga de trabajo de procesamiento de CPU y GPU, similar a GPUView pero realmente fácil de usar, módulo algunas idiosincrasias de interfaz de usuario.
  • Un compilador de sombreadores fuera de línea que valida la sintaxis de su sombreador, a veces le brinda advertencias útiles, convierte su sombreador en un blob binario que es bastante rápido de cargar en tiempo de ejecución y, además, razonablemente bien optimizado de antemano, lo que reduce el tiempo de carga porque el compilador del controlador puede ser más rápido.

Si vienes de Direct3D o del mundo de las consolas, puedes dar por sentado cada uno de ellos. Créeme, en OpenGL cada uno de ellos es inusual y emocionante, especialmente en dispositivos móviles donde solías lidiar con interrupciones ocasionales del conductor, no. validación, sin depurador de GPU, sin generador de perfiles de GPU útil, sin capacidad para recopilar datos de planificación de GPU y obligado a trabajar con un lenguaje de sombreado basado en texto para el que cada proveedor tiene un analizador ligeramente diferente.

Metal es una excelente API para escribir código y enviar aplicaciones. Es fácil de usar, tiene un rendimiento predecible, tiene controladores robustos y un conjunto de herramientas sólido. Supera a OpenGL en todos los aspectos excepto en la portabilidad, pero la realidad con OpenGL es que realmente solo deberías haberlo usado en tres plataformas (iOS, Android y Mac), y dos de ellas ahora son compatibles con Metal; además, la promesa de portabilidad de OpenGL generalmente no se cumple porque el código que escribes en una plataforma muy a menudo termina sin funcionar en otra por varias razones.

Si está utilizando un motor de terceros como Unity o UE4, Metal ya es compatible allí; Si no lo eres y te encanta la programación gráfica o te preocupas mucho por el rendimiento y te tomas en serio iOS o Mac, te recomiendo encarecidamente que pruebes Metal. Usted no será decepcionado.

metal ahora

Los mayores cambios que le han ocurrido al Metal desde nuestra perspectiva en los últimos tres años están en la adopción a gran escala.

Hace tres años, una cuarta parte de los dispositivos tenían que usar OpenGL. Hoy, para nuestra audiencia, ese número es de alrededor del 2 %, lo que significa que nuestro backend OpenGL ya no importa. Todavía lo estamos manteniendo, pero no durará mucho.

Los controladores también son mejores que nunca: en términos generales, no vemos problemas con los controladores en iOS, y cuando los vemos, a menudo ocurren en los primeros prototipos, y cuando los prototipos llegan a la producción, los problemas generalmente se resuelven.

También dedicamos tiempo a mejorar nuestro backend Metal, centrándonos en tres áreas:

Cadena de herramientas de compilación de sombreador de reelaboración

Otra cosa que ha sucedido en los últimos tres años es el lanzamiento y desarrollo de Vulkan. Aunque parece que las API son completamente diferentes (y lo son), el ecosistema Vulkan le ha brindado a la comunidad de renderizado un fantástico conjunto de herramientas de código abierto que, cuando se combinan, dan como resultado un conjunto de herramientas de construcción de calidad de producción fáciles de usar.

Usamos las bibliotecas para crear una cadena de herramientas de compilación que puede tomar el código fuente de HLSL (usando varias funciones de DX11, incluidos los sombreadores de cómputo), compilarlo en SPIRV, optimizar dicho SPIRV y convertir el SPIRV resultante en MSL (lenguaje de sombreado de metal). Reemplaza nuestra cadena de herramientas anterior que solo podía usar la fuente DX9 HLSL como entrada y tenía varios problemas de corrección para sombreadores complejos.

Es algo irónico que Apple no haya tenido nada que ver con eso, pero aquí estamos. Muchas gracias a los colaboradores y mantenedores de glslang (https://github.com/KhronosGroup/glslang), spirv-opt (https://github.com/KhronosGroup/SPIRV-Tools) y SPIRV-Cross (https: / / github.com/KhronosGroup/SPIRV-Cross). Hemos proporcionado un conjunto de correcciones a estas bibliotecas para ayudarnos a enviar la nueva cadena de herramientas también y usarla para redirigir nuestros sombreadores a las API de Vulkan, Metal y OpenGL.

compatibilidad con macOS

Un puerto macOS siempre fue una posibilidad, pero no fue una gran preocupación para nosotros hasta que comenzamos a perder algunas funciones. Así que decidimos invertir en Metal en macOS para obtener un renderizado más rápido y desbloquear algunas posibilidades para el futuro.

Desde el punto de vista de la implementación, no fue muy difícil en absoluto. La mayor parte de la API es exactamente igual; Además de la administración de ventanas, el área que necesitaba ajustes importantes era la asignación de memoria. En dispositivos móviles hay un espacio de memoria compartida para búferes y texturas, mientras que en computadoras de escritorio la API asume una GPU dedicada con su propia memoria de video.

Hay una forma rápida de evitar esto mediante el uso de recursos administrados, donde el tiempo de ejecución de Metal se encarga de copiar los datos por usted. Así es como enviamos nuestra primera versión, pero luego modificamos la implementación para copiar de manera más explícita los datos de recursos utilizando búferes temporales para minimizar la sobrecarga de la memoria del sistema.

La mayor diferencia entre macOS e iOS fue la estabilidad. En iOS, trabajábamos con un solo proveedor de controladores en una arquitectura, mientras que en macOS teníamos que admitir los tres proveedores (Intel, AMD, NVidia). Además, en iOS, ¡afortunadamente! – ignoró la *primera* versión de iOS donde Metal estaba disponible, iOS 8, y en macOS esto no era práctico ya que tendríamos muy pocos usuarios para usar Metal en ese momento. Debido a la combinación de estos problemas, encontramos muchos más problemas con los controladores en las áreas relativamente inocuas y relativamente oscuras de la API en macOS.

Todavía admitimos todas las versiones de macOS Metal (10.11+), aunque comenzamos a eliminar la compatibilidad y pasamos al backend heredado de OpenGL para algunas versiones con errores conocidos del compilador de sombreadores que son difíciles de solucionar, por ejemplo, en 10.11, ahora necesitamos macOS 10.11.6 para Metal a trabajar.

Los beneficios de rendimiento estuvieron en línea con nuestras expectativas; en términos de participación de mercado, ahora somos aproximadamente un 25 % de usuarios de OpenGL y aproximadamente un 75 % de usuarios de Metal en la plataforma macOS, que es una división bastante saludable. Esto significa que en algún momento en el futuro puede ser conveniente para nosotros dejar de admitir OpenGL de escritorio, ya que ninguna otra plataforma que admitimos lo usa, lo cual es excelente en términos de poder centrarnos en API más fáciles de admitir y obtener un buen rendimiento con eso.

Iterar sobre el rendimiento y el consumo de memoria

Históricamente, hemos sido bastante conservadores con las características de la API de gráficos que usamos, y Metal no es una excepción. Hay varias actualizaciones de funciones importantes que Metal ha adquirido a lo largo de los años, incluidas API de asignación de recursos mejoradas con montones explícitos, sombreadores de mosaicos con Metal 2, búferes de argumentos y lado de GPU de generación de comandos, etc.

La mayoría de las veces no usamos ninguna de las nuevas características. Hasta ahora, el rendimiento ha sido razonable y nos gustaría centrarnos en las mejoras que se aplican en todos los ámbitos, por lo que algo como los sombreadores de mosaicos, que requieren que implementemos un soporte muy especial durante el renderizado y solo es accesible en hardware más nuevo, es menos interesante.

Dicho esto, dedicamos un tiempo a ajustar diferentes partes del backend para que se ejecuten *más rápido*, utilizando descargas de texturas completamente asincrónicas para reducir el tartamudeo en las cargas de nivel, lo cual fue completamente sencillo, haciendo las optimizaciones de memoria antes mencionadas en macOS, optimizando la distribución de la CPU. en varios lugares en el backend, reduciendo las fallas de caché, etc., y, una de las únicas características más nuevas para las que tenemos soporte explícito, mediante el uso de almacenamiento de texturas sin memoria cuando esté disponible para reducir significativamente la memoria requerida para nuestro nuevo sistema de sombra.

L'avenir

En general, el hecho de que no hayamos dedicado demasiado tiempo a las mejoras de Metal es en realidad algo bueno: el código que se escribió hace 3 años básicamente funciona y es rápido y estable, lo cual es una gran señal de una API madura. El puerto al metal ha sido una gran inversión, dado el tiempo que tomó y los beneficios continuos que nos brinda a nosotros y a nuestros usuarios.

Constantemente reevaluamos el equilibrio entre la cantidad de trabajo que hacemos para diferentes API; es muy probable que tengamos que profundizar en las partes más modernas de Metal API para algunos de los futuros proyectos de renderizado; si eso sucede, ¡nos aseguraremos de escribir otro artículo al respecto!


  1. Sí, está bien, y tal vez una semana para corregir algunos errores descubiertos durante las pruebas ↩
  2. Las cifras corresponden a 128 KB de datos actualizados por cuadro (dos regiones RGBA32 de 16x32x8) en A10 ↩