Soporte certificado por conformidad para OpenGL 4.6 en el M1
(rosenzweig.io)- Durante años, el M1 se quedó en OpenGL 4.1, pero los drivers más recientes para M1/M2 de Fedora Asahi Remix ahora ofrecen soporte completo para OpenGL 4.6 y OpenGL ES 3.2
- El driver Linux de código abierto de Asahi, a diferencia del driver 4.1 no conforme del proveedor, aparece en la lista de Khronos como driver certificado por conformidad, ampliando la compatibilidad con cargas de trabajo modernas de OpenGL como Blender
- OpenGL 4.6 exige robustness, SPIR-V, clip control, cull distance, compute shaders y transform feedback mejorado, pero el hardware M1 no encaja de forma nativa con los estándares gráficos más recientes
- Las funciones ausentes en el hardware se resuelven con técnicas de driver y compilador: geometry shaders, tessellation y transform feedback se manejan con compute shaders, mientras que cull distance y clip control se procesan con transformaciones de shader
- Para pasar más de 100,000 pruebas de conformidad, se implementó por software la robustness de buffers e imágenes, y se redujo el costo adicional con optimizaciones de clamp, preamble y un bypass para mipmap
OpenGL 4.6/ES 3.2 disponible en Fedora Asahi Remix
- El M1 antes solo soportaba hasta OpenGL 4.1, pero ahora ya soporta OpenGL 4.6 y OpenGL ES 3.2
- Los drivers más recientes de la serie M1/M2 pueden usarse instalando Fedora Asahi Remix
- Quienes ya lo tengan instalado pueden actualizar con el siguiente comando
dnf upgrade --refresh
- El driver Mesa de código abierto de Asahi, a diferencia del driver 4.1 no conforme del proveedor, cumple con la conformidad (conformance) para versiones recientes de OpenGL
- Un driver conforme para 4.6/3.2 debe pasar más de 100,000 pruebas para garantizar exactitud
- La lista oficial de Khronos incluye los envíos de Asahi para OpenGL 4.6 y OpenGL ES 3.2
- Hace 6 meses apareció el primer driver conforme del M1 para una API gráfica estándar con OpenGL ES 3.1, y ahora también se completó OpenGL 4.6
- El soporte para Vulkan también está en desarrollo
Implementación de funciones para superar la barrera de OpenGL 4.1
- OpenGL 4.6 añade varias funciones obligatorias frente a 4.1
- Robustness
- SPIR-V
- Clip control
- Cull distance
- Compute shaders
- Transform feedback mejorado
- El M1 no se ajusta bien a estándares gráficos más nuevos que OpenGL ES 3.1
- Vulkan vuelve opcionales algunas funciones, pero para montar DirectX y OpenGL encima siguen haciendo falta las funciones ausentes
- La solución existente del M1 no podía ir más allá del conjunto de funciones de OpenGL 4.1
- Las nuevas funciones sin soporte de hardware se implementaron con técnicas del driver
- Geometry shaders, tessellation y transform feedback se resuelven con compute shaders
- Cull distance se maneja con valores interpolados transformados
- Clip control se implementa con un epílogo del vertex shader
Robustness de buffers y corrección por software en el M1
- Tradicionalmente, los GPU priorizan el rendimiento por encima de la seguridad, así que un shader con código incorrecto que lea fuera del rango de un buffer puede provocar comportamiento indefinido
- Para aplicaciones que manejan shaders no confiables, como los navegadores web, este compromiso no es deseable
- La API gráfica en sí no es un límite de seguridad, así que sigue haciendo falta cierta sanitización
- Reducir el comportamiento indefinido de la API ayuda a la defensa en profundidad
- Al activar robustness, la aplicación puede elegir comportamiento definido para accesos fuera de rango a cambio de renunciar a algo de rendimiento
- El resultado de una carga fuera de rango en un buffer varía según la API
- Direct3D y Vulkan
robustBufferAccess2: devuelven 0 - OpenGL y Vulkan
robustBufferAccess: devuelven 0 o algunos datos dentro del buffer - OpenGL ES: puede devolver un valor arbitrario, pero no debe causar un crash
- Direct3D y Vulkan
- Como OpenGL exige devolver 0 o datos dentro del buffer en accesos fuera de rango, se puede cargar con un índice seguro calculando el mínimo sin signo entre el último índice válido y el índice de acceso
- carga de uniform buffer sin robustness:
load.i32 result, buffer, index - después de aplicar robustness:
umin idx, index, lasty luegoload.i32 result, buffer, idx
- carga de uniform buffer sin robustness:
- El preamble del M1 calcula una vez y reutiliza valores en lugar de repetir el mismo cálculo en todos los threads
- Como el tamaño del uniform buffer es fijo, la aritmética adicional de robustness también puede moverse al preamble
- Incluso en un storage buffer robusto, aunque la carga/almacenamiento en sí no se pueda mover, el cálculo del clamp sí puede ir al preamble
Implementación de robustness para vertex buffer
- En las API gráficas, la aplicación configura la dirección base GPU del vertex buffer y el layout de atributos
- Cada atributo tiene offset y formato
- El buffer tiene un stride que representa los bytes por vértice
- El vertex shader lee atributos indexándolos implícitamente con base en el vértice
- Algunos hardwares implementan de forma nativa el robust vertex fetch o aceleran el fetch por software con buffers con verificación de límites, pero el M1 no tiene ninguno de los dos
- Las cargas de memoria del GPU M1 reciben una dirección base de 64 bits y un offset en unidades de elemento, y también ofrecen la instrucción
imadde multiplicación-suma entera- Una carga de atributo de 32 bits puede implementarse con dos instrucciones:
imad idx, stride/4, vertex, offset/4yload.i32 result, base, idx - Un atributo vectorial con 4 valores de 32 bits empaquetados de forma densa puede cargarse con una sola instrucción:
load.v4i32 result, base, vertex << 2
- Una carga de atributo de 32 bits puede implementarse con dos instrucciones:
- La robustness requiere clamp, pero el tamaño del vertex buffer está en bytes y la carga optimizada usa un índice por vértice
- El problema se resuelve reinterpretando múltiples atributos y offsets dentro de un mismo buffer como si fueran direcciones base separadas por atributo
- En vez de sumar el offset en el shader, se pasa una base por atributo
- El tamaño del buffer en bytes puede convertirse a un tamaño por atributo en unidades de vértice
- Se hace clamp sobre el índice de vértice, no sobre el offset
- El driver precalcula el último índice de vértice válido usando el tamaño del formato de cada atributo y se lo pasa al shader
- Si el buffer es tan pequeño que no puede cargar nada, no basta con clamp, así que el buffer de ese atributo se reemplaza por un pequeño zero buffer
- Como se usan direcciones base por atributo, esta decisión también puede tomarse por atributo
- Al final, se implementa un robust vertex buffer con un poco de cálculo en el driver y el costo de un solo
umin
Robustness de imágenes y bypass de mipmap
- Además de la robustness de buffers, también hace falta image robustness, donde una carga de imagen fuera de rango debe devolver 0
- Una imagen con mipmaps contiene múltiples niveles de detalle
- El nivel base es la imagen original
- Cada nivel siguiente es una versión reducida del nivel anterior
- Al renderizar, el hardware selecciona el nivel más cercano al tamaño en pantalla para mejorar eficiencia y calidad visual
- La especificación exige que, con robustness, una image load devuelva 0 en los siguientes casos
- Cuando la coordenada X o Y está fuera de rango
- Cuando el nivel está fuera de rango
- El comportamiento de image load en el GPU M1 difiere de ese requisito
- Si X o Y están fuera de rango, devuelve 0
- Si el nivel está fuera de rango, devuelve el valor del último nivel
- Como el proveedor no publica documentación del hardware, no se sabe si ese comportamiento es intencional o un bug de hardware, y para pasar la conformidad hacía falta un bypass
- Un bypass simple sería cargar solo cuando el nivel sea válido y, si no, devolver 0 con una rama, pero las ramas son ineficientes
- Una mejor opción es aprovechar que una carga fuera de rango no causa crash: primero se hace la carga y luego se elige 0 con compare-select
- Pero el conjunto de instrucciones del GPU M1 es escalar, y image load devuelve un vector de 4 componentes: red/green/blue/alpha
- Haría falta un
uleselpor componente, haciendo crecer el ensamblado
- El bypass final aprovecha que el hardware devuelve 0 cuando X o Y quedan fuera de rango
- Como el ancho máximo de imagen es 16384 px, cambiar X a un valor como 20000 lo deja fuera de rango
- Si el nivel es válido, se usa la X original; si no, se cambia X a 20000 para que image load devuelva 0
- Este método compila a un ensamblado compacto porque solo cambia un escalar y no selecciona el vector completo
- Si la constante se carga de antemano en un registro uniforme, el costo del bypass es una sola instrucción
- Con este método se logró pasar la conformidad
1 comentarios
Opiniones de Hacker News
Alyssa Rosenzweig parece un gran regalo que sigue aportando a la comunidad.
En cada entrada del blog termino aprendiendo algo que no sabía sobre la estructura interna del hardware gráfico moderno.
Es un trabajo que demuestra una y otra vez que la capacidad le gana a las palabras.
Solo leer el blog ya te calienta la cabeza de tantas cosas que hay para desentrañar; y aunque la conclusión no está en la última oración sino en la segunda, al final uno termina siguiendo cada manipulación de bits hasta meterse en la madriguera.
Si existiera un benchmark de revelaciones por párrafo, creo que Alyssa saldría primera en todos.
Tal vez algún día, si Apple elimina OpenGL 3.3 core, todos terminen eliminándolo también.
En general he escuchado que OpenGL es más fácil de usar que Vulkan, pero una API demasiado compleja puede hacer que a los desarrolladores con poca experiencia les cueste aprovechar la GPU, convertirse en una barrera de entrada y expulsar a desarrolladores indie.
Hoy en día todos usan Unity y Unreal, así que hacer algo desde cero o usar otro motor se ve raro; y aunque es interesante ver cómo el desarrollo de juegos despertó después de que Unity intentara cerrarse más, también resulta irritante.
El open source en desarrollo de juegos siempre ha estado ajustado, y aunque existe Godot, parece difícil que compita en serio contra Unity y Unreal.
Aunque Godot fuera lo suficientemente capaz, es probable que los desarrolladores indie estén más acostumbrados a Unity y Unreal y se queden ahí.
El estado del open source en el desarrollo de juegos a veces se siente desesperanzador, y la llegada de las API gráficas de nueva generación no facilita las cosas.
El ejemplo de renderizado de un triángulo en OpenGL tiene unas 200 líneas, y el ejemplo de renderizado de un triángulo en Vulkan unas 1000.
Vulkan está diseñado para ser muy flexible, pero a cambio no ofrece muchas comodidades.
En cualquier caso, OpenGL era de demasiado alto nivel para exponerse como API directa del driver; encaja mejor con la forma en que funciona el hardware de GPU tener una API de bajo nivel como Vulkan como capa base y montar encima algo como OpenGL.
Además, no todo el mundo usa Unity y Unreal.
Los seis nominados a juego del año en The Game Awards 2023 se hicieron todos con motores propios, y en el ámbito indie todavía hay desarrolladores que crean su propio motor, como en el caso de Hades.
Dicho eso, es cierto que la mayoría usa motores ya hechos.
Si cubre todas las funciones que necesitas y puedes tolerar un pipeline de renderizado basado en estado, también es una buena opción para proyectos nuevos.
Todavía funciona y hoy se ejecuta como una capa sobre Metal, pero al compilar código GL para macOS o iOS aparecen advertencias de obsolescencia constantemente.
Se pueden desactivar con un define.
El problema de OpenGL es que está demasiado alejado de cómo funcionan las GPU, lo que dificulta obtener buen rendimiento.
Me da curiosidad cuánto de este trabajo está ligado al código de la GPU M1 y cuántas implementaciones de funciones sobre otras funciones podrían reutilizarse en otros lugares.
Se parece mucho a la forma en que Zink ejecuta funciones complejas de OpenGL sobre un Vulkan más primitivo, pero el M1 todavía no tiene un backend Vulkan al cual apuntar.
Al final es una cuestión de cantidad de trabajo, y puede reutilizarse en hardware diverso.
También puede ayudar a hardware antiguo y bien entendido, pero difícil de usar por sí solo con cargas de trabajo modernas.
Tengo muchísima curiosidad por el impacto en rendimiento que esto implica, especialmente comparado con usar Metal directamente en macOS.
La respuesta seguramente sea “depende”, pero igual me intriga.
Quizá la respuesta esté en el artículo, pero no entendí la mayor parte.
El “soporte de hardware” también suele implementarse con microcódigo de la GPU y muchas veces pasa por el mismo silicio.
Cualquier función puede convertirse en cuello de botella de rendimiento, y es difícil saber dónde se va a trabar hasta probarlo realmente.
Es cierto que la GPU de Apple no soporta shaders de geometría de forma nativa, pero los shaders de geometría están mal diseñados y no encajan bien con el hardware de GPU.
Incluso en hardware que supuestamente los soporta, se sabe que son lentos, y hay una razón por la que Nvidia diseñó mesh shading.
También se menciona mucho el transform feedback, pero la GPU de Apple puede escribir en ubicaciones arbitrarias de memoria desde cualquier etapa de shader, así que el transform feedback en la práctica es innecesario.
El punto clave es que Apple implementó una arquitectura de cómputo concisa y recortó muchas reliquias antiguas y funciones conocidas por no funcionar bien.
La expresión “el M1 está atrapado en OpenGL 4.1” no me parece adecuada.
Dejé de seguir OpenGL hace mucho, así que no sé a qué funciones posteriores a 4.1 se refiere, pero me sorprendería muchísimo que hubiera algo posible en OpenGL que no se pueda hacer en Metal.
En cambio, hay muchas cosas posibles en Metal que son totalmente imposibles en OpenGL, empezando por el hecho de que el lenguaje de sombreado de Metal tiene punteros completos.
Esto es para Fedora en M1
Sería sorprendente si también se volviera posible en macOS, pero me pregunto qué haría falta para construir algo así
El driver inicial de Mesa para la GPU M1 también se arrancó enviando buffers de comandos al driver AGX de macOS mediante IOKit
https://rosenzweig.io/blog/asahi-gpu-part-2.html
https://github.com/AsahiLinux/gpu/blob/main/demo/iokit.c
Así que haría falta un poco más de código de integración del lado de Mesa para pasar la surface de la GPU a algo que pueda componerse en la pantalla de macOS
Entiendo que, para crear una implementación adecuada de Vulkan u OpenGL, se necesita una contraparte del lado del kernel que se encargue del procesamiento de la GPU
Tal vez por eso no parece haber nadie intentando implementar Vulkan nativo para macOS
Aunque no sé si sería posible encima del driver de Apple
Pero no sé si tendría sentido gastar tantos recursos en una API legacy no óptima
Es bastante gracioso llamar robustez a cambiar los accesos fuera de rango de un trap a devolver datos arbitrarios
La programación gráfica definitivamente es rara
Una parte central de los drivers de GPU es hacer que aplicaciones rotas se ejecuten, o que lo hagan más rápido
Poner valores predeterminados estrictos no va a arreglar los problemas estructurales de la industria de los videojuegos, que lanza código roto; solo hará que los usuarios se vayan
En hardware donde las ramificaciones suelen ser muy caras, parece útil tener una bandera que le indique al sistema que maneje silenciosamente los casos límite de la forma más eficiente posible
También parece haber muchos usos válidos en los que el programador puede estar razonablemente seguro de que esos casos límite casi no afectarán el frame renderizado final
Si se combina con el hecho de que, en general, a las GPU no les gustan mucho los traps, tiene sentido
Carmack también dijo alguna vez que, al diseñar megatexture, fue doloroso lograr que los fabricantes aceptaran la idea de la memoria virtual
Hablar de otros lenguajes de programación en campos dominados por una cultura de “el rendimiento está por encima de la seguridad” es parecido a hablarle a una pared
Sin duda es algo muy interesante, pero me pregunto por qué no apuntaron primero a Vulkan
Hoy parece un objetivo más importante, y encima ya existe una implementación de OpenGL
Para soportar una función específica de OpenGL, el driver Vulkan tiene que soportar la función correspondiente, y normalmente se necesita una extensión
Es decir, implementar solo un driver Vulkan básico no te da gratis soporte para OGL 4.6; para que Mesa traduzca OGL 4.6 a Vulkan, hay que implementar en el driver Vulkan todas las funciones de OGL 4.6
Además, Alyssa ya tiene experiencia en ingeniería inversa y en proyectos de drivers OpenGL
No conozco los detalles, pero es muy probable que crear un driver para una API familiar sea mucho más fácil y rápido que crear uno para una API que no lo es
Es muy probable que subir desde ahí hasta un OpenGL más reciente haya sido menos trabajo que una implementación completa de Vulkan, y seguramente también aprendieron mucho de lo necesario para Vulkan
Así que parece que eligieron empezar por una versión baja de OpenGL para obtener más rápido algo que funcionara
Es bastante sorprendente pensar que, si John Carmack no se hubiera obsesionado en los 90 con usar OpenGL en Quake II, quizá OpenGL no habría tenido presencia en los juegos 3D
Lo que terminó convirtiendo a OpenGL en lo que fue fueron SGI y el enorme esfuerzo por crear implementaciones compatibles en múltiples sistemas y arquitecturas
https://web.archive.org/web/19970707113513/http://www.opengl.org/
Muchas cosas relacionadas con los juegos 3D se deben a Doom y Quake
Consideraba que Microsoft había tenido el valor de seguir haciendo cambios importantes incompatibles para mejorar la API, mientras que OpenGL se vio frenado por preocupaciones de compatibilidad
Dijo que Direct3D manejaba mejor el multithreading y que las versiones más recientes también tenían una mejor gestión de estado
Aun así, afirmó que id Software seguía en OpenGL por inercia y que, pese a sus ventajas, no tenía planes de migrar a Direct3D
Fuente: https://www.bit-tech.net/news/gaming/pc/carmack-directx-better-opengl/1/