Actualités / Jeux

3 années de Metal – Blog Roblox

Il y a trois ans, nous avons fait passé notre système sur Metal. Cela n’a pas pris beaucoup de temps, c’était un plaisir et cela a très bien fonctionné sur iOS. Nous avons donc écrit un article commentaire expliquant nous avons pris notre décision et comment elle a abouti (spoilers: vraiment bien!). La plupart de cette rétrospective originale s’applique toujours mais aujourd’hui, Metal est en meilleure forme que jamais – nous avons donc décidé de la républier avec notre mise à jour sur trois ans.

Alors remontons le temps, faisons comme si nous étions en décembre 2016 et que nous venions de livrer une version de notre Metal render sur iOS.

Pourquoi le métal?

Quand Apple a annoncé Metal à la WWDC en 2014, ma première réaction a été de l’ignorer. Il n'était pas disponible que sur le matériel le plus récent dont la plupart de nos utilisateurs n'en disposaient pas et bien qu'Apple ait déclaré qu'il résolvait les problèmes de performance du CPU, l'optimisation pour le plus petit marché signifierait que l'écart entre les appareils les plus rapides et les plus lents se creuserait encore plus. À l’époque, nous utilisons OpenGL ES 2 uniquement sur Apple et nous commençons également à passer sur Android.

Deux ans et demi plus tard, voici comment se présente la part de marché Metal pour nos utilisateurs:

https://blog.roblox.com/

C’est beaucoup plus intéressant d’avoir. Il est toujours vrai que la mise en œuvre de Metal n'aide pas les appareils les plus anciens, mais le marché du GL sur iOS ne cesse de se réduire et le contenu que nous utilisons sur ces anciens appareils est souvent différent de celui qui fonctionne sur les appareils plus récents, il est donc logique de concentrer nos efforts pour le rendre plus rapide. Étant donné que votre code iOS Metal fonctionnera sur Mac avec très peu de modifications, il pourrait être judicieux de l’utiliser également sur Mac même si vous êtes un utilisateur mobile (nous ne livrons actuellement que des Metal builds sur iOS).

Je pense qu’il est utile d’analyser la part de marché un peu plus en détail. Sur iOS, nous supportons Metal pour iOS 8.3+; alors que certains utilisateurs ne peuvent pas exécuter Metal à cause des restrictions de version de l’OS, la plupart des 25% qui exécutent encore GL utilisent simplement des appareils plus anciens qui ont du matériel SGX. Ils n'ont pas non plus de fonctionnalités OpenGL ES 3 et nous nous contentons d'utiliser un chemin de rendu bas de gamme (bien que nous aimerions que tous les appareils passent en Metal – heureusement la séparation GL / Metal ne fera que s ' améliorer). Sur Mac, l'API de Metal est plus récent et le système d'exploitation joue un rôle assez important – vous devez utiliser OSX 10.11+ pour utiliser Metal et la moitié de nos utilisateurs ont simplement un système d'exploitation plus ancien – c ' est moins une question de matériel que de logiciel (95% de nos utilisateurs Mac utilisent OpenGL 3.2+).

Compte tenu de la part de marché, nous avons donc encore des options qui n'impliquent pas le transfert vers Metal. L’une d’entre elles consiste à se servir simplement de MoltenGL, qui utiliseait le code OpenGL que nous avons déjà, mais qui serait supposé être plus rapide; une autre consiste à transférer sur Vulkan (pour obtenir de meilleures performances sur PC, et sur Android) et à utiliser MoltenVK. J'ai brièvement évalué MoltenGL et je n'étais pas très enthousiaste à propos des résultats – il a fallu un certain effort pour faire fonctionner notre code et même si les performances étaient un peu meilleures par rapport à l'OpenGL d'origine, j «espérais plus. Quant à MoltenVK, je pense qu'il est malavisé d'essayer de mettre en œuvre une API de bas niveau comme une couche au dessus d'une autre – vous obtiendrez forcément un décalage d'impédance qui se traduira par des performances sous- optimales – elle sera peut-être meilleure que l'API de haut niveau que vous utilisiez auparavant, mais il est peu probable qu'elle soit aussi rapide que possible, ce qui est censé expliquer pourquoi vous choisissez une API de bas niveau pour commencer! Un autre aspect important est que la mise en œuvre de Metal est beaucoup plus simple que celle de Vulkan – nous y reviendrons plus tard – donc dans un certain sens, je préférerais un emballage Metal -> Vulkan plutôt qu'un Vulkan-> Metal.

Il convient également de noter qu’apparemment sur l’iOS 10 des derniers iPhones, il n’y a pas de pilote GL – GL est implémenté par-dessus Metal. Cela signifie que l'utilisation d'OpenGL ne vous permet d'économiser qu'un peu de développement – pas tant que ça, si l'on considère que la promesse d'OpenGL “écrire une fois, marche partout” ne fonctionne pas vraiment sur les mobiles.

Portage

Dans l’ensemble, le transfert vers Metal a été un jeu d’enfant. Nous avons une grande expérience de travail avec différentes API graphiques, allant des API de haut niveau comme Direct3D 9/11 aux API de bas niveau comme PS4 GNM. Cela donne l’avantage unique de pouvoir utiliser confortablement une API comme Metal qui est à la fois un niveau raisonnablement élevé mais qui laisse également certaines tâches comme la synchronisation CPU-GPU à la charge du développeur de l’application.

Le seul obstacle était de faire compiler nos shaders – une fois que cela a été fait et qu'il était temps d'écrire le code. Il était devenu évident que l’API était si simple et explicite que le code s’est pratiquement écrit tout seul. J'ai fait fonctionner le port qui rendait la plupart des choses de manière sous-optimale en une dizaine d'heures dans une seule journée puis j'ai passé deux semaines supplémentaires à nettoyer le code, à régler les problèmes de validation, à profiler , à optimiser et à effectuer un polissage général. Le fait d’obtenir une implémentation de l’API dans ce délai en dit long sur la qualité de l’API et de l’ensemble des outils. Je crois qu'il y a plusieurs aspects qui y contribuent:

  • Vous pouvez développer le code de manière progressive, avec un bon retour d’information à chaque étape. Notre code a commencé en ignorant toute synchronisation CPU-GPU, en étant vraiment sous-optimal sur certaines parties de la configuration de l'état, en utilisant un suivi de référence intégré pour les ressources et en ne faisant jamais tourner le CPU et le GPU en parallèle pour éviter de rencontrer des problèmes; la phase d’optimisation / polissage a ensuite converti cela en quelque chose que nous pourrions envoyer, sans jamais perdre la capacité de rendu dans le processus.
  • Les outils sont là pour vous, ils fonctionnent et ils fonctionnent bien. Ce n'est pas vraiment une surprise pour les personnes habituées à Direct3D 11 – mais c'est la première fois sur un mobile où j'avais un profileur CPU, un profileur GPU, un débogueur GPU et une couche de validation de l'API GPU qui ont tous bien fonctionné en tandem, en détectant la plupart des problèmes pendant le développement et en aidant à optimiser le code.
  • Bien que l'API soit un peu moins élaboré que Direct3D 11 et qu'elle laisse certaines décisions clés de niveau moindre au développeur (telles que la configuration des passes de rendu ou la synchronisation), elle utilise toujours un modèle de ressources traditionnel où chaque ressource possède certains «drapeaux d'utilisation» avec lesquels elle a été créée mais ne nécessite pas de barrières de pipeline ou de transitions de mise en page, et un modèle de liaison traditionnel où chaque étape de shader possède plusieurs emplacements liés à vous pouvez librement attribuer des ressources. Ces deux éléments sont familiers, faciles à comprendre et ne nécessitent qu'une quantité très limitée de code pour aller vite.

Une autre chose que nous avons aidée est que notre interface API était prête pour des API de type Metal – elle est très légère mais elle expose assez de détails pour pouvoir écrire facilement une implémentation performante. À aucun moment de notre mise en œuvre, je n'ai eu besoin de sauvegarder / restaurer l'état (de nombreuses interfaces API en souffrent, notamment parce qu'ils traitent la configuration de la cible à rendre comme des changements d'état et que les liaisons ressources / état persistant à travers cela) ou de prendre des décisions compliquées sur la durée de vie / synchronisation des ressources. Le seul morceau de code “compliqué” nécessaire est celui qui crée l’état du pipeline de rendu en hachant les bits nécessaires pour en créer un – les objets de l’état du pipeline ne font pas partie de l’abstraction de notre API. Même si c’est assez simple et rapide. J’en écrirai un peu plus sur l’interface API dans un post séparé.

https://blog.roblox.com/

Donc, une semaine pour compiler les shaders, deux semaines pour obtenir une mise en œuvre optimisée et soignée 1 – quels sont les résultats? Les résultats sont excellents – Metal tient ses promesses en termes de performance. D'une part, les performances de la distribution à fil unique sont nettement meilleures avec OpenGL (en partie la partie “draw dispatch” de notre cadre de rendu de 2 à 3 fois selon la charge de travail) et cela parce que notre implémentation OpenGL est assez bien appliquée en termes de réduction de la redondance de la configuration d'état et de compatibilité avec le pilote en utilisant des chemins rapides. Mais cela ne s’arrête pas là – le multithreading dans Metal est facile à utiliser à condition que votre code de rendu soit prêt pour cela. Nous ne sommes pas encore passés à la répartition des plans mais nous sommes déjà en train de convertir d’autres parties qui préparent les ressources à se produire hors du processus de rendu, ce qui, contrairement à OpenGL, est assez facile.

Au-delà de cela, Metal nous permet de régler certains autres problèmes de performance en donnant des outils facilement accessibles et fiables. Une des parties centrales de notre code de rendu est le système qui calcule les données d'éclairage sur le CPU dans l'espace mondial et les téléchargements dans les régions d'une texture 3D (que nous devons émuler sur le matériel OpenGL ES 2) . Les mises à jour sont partielles, nous ne pouvons donc pas dupliquer la texture entière et devons nous fier à la façon dont le pilote implémente glTexSubImage3D. À un moment donné, nous avons essayé d'utiliser le PBO pour améliorer les performances des mises à jour, mais nous avons été confrontés à des problèmes de stabilité importants dans tous les domaines, tant sur Android que sur iOS Sur Metal, il existe deux méthodes intégrées de télécharger une région – MTLTexture.replaceRegion que vous pouvez utiliser si le GPU ne lit pas actuellement la texture, ou MTLBlitCommandEncoder (copyFromBufferToTexture ou copyFromTextureToTexture) qui peut télécharger la région de manière asynchrone juste à temps pour le GPU commence à utiliser la texture.

https://blog.roblox.com/

Ces deux méthodes étaient plus lentes que je ne l'aurais souhaité – la première n'était pas vraiment disponible car nous devions prendre en charge des mises à jour partielles efficaces, et elle fonctionnait uniquement sur le CPU en utilisant ce qui devrait être une implémentation très lente de la traduction d'adresses. Le second a fonctionné mais également utilisé une série de blits 2D pour remplir la texture 3D qui était à la fois assez coûteux à mettre en place pour les commandes du côté du CPU et avait également un supplément de GPU très élevé pour une raison quelconque. Si c’était OpenGL, ce serait terminé – en fait, la performance de ces deux méthodes correspond à peu près au coût d’une mise à jour similaire à OpenGL. Heureusement, comme il s'agit de Metal, il a facilement accès aux shaders de calcul – et un shader de calcul super simple nous a donné la possibilité de faire tampon -> Le téléchargement des textures 3D a été très rapide sur le CPU et le GPU et a pratiquement résolu nos problèmes de performance dans cette partie du code pour de bon 2:

https://blog.roblox.com/

En guise de commentaire général final, la maintenance du code de Metal est également assez facile – toutes les fonctionnalités supplémentaires que nous avons dû ajouter jusqu'à présent ont été plus faciles à y ajouter que sur toute autre API que nous supportons, et je m 'assiste à ce que cette tendance se poursuitive. On s’inquiétait un peu du fait que l’ajout d’une API supplémentaire nécessiterait une maintenance constante, mais comparé à OpenGL, cela ne demande pas vraiment beaucoup de travail; en fait, comme nous n’aurons plus à supporter OpenGL ES 3 sur iOS, cela signifie que nous pouvons simplifier certains codes OpenGL que nous avons également.

Stabilité

Aujourd’hui iOS Metal est très stable. Je ne suis pas sûr de la situation au lancement en 2014, ni de celle sur Mac aujourd’hui mais tant les drivers que les outils pour iOS me paraissent assez fiables.

Nous avons eu un problème de driver sur iOS 10 qui avait trait au chargement de shaders compilés avec Xcode 7 (que nous avons résolu en passant à Xcode 8) et un crash de driver sur iOS 9 qui s'est avéré être le résultat d ' une mauvaise utilisation de l'API nextDrawable. À part cela, nous n’avons constaté aucun bug de fonctionnement ni aucun avantage – pour une API relativement nouvelle, Metal a été très stable sur toute la ligne.

De plus, les outils que vous obtenez avec Métal sont variés et riches; en particulier, vous pouvez les utiliser pour:

  • Une couche de validation assez complète qui permettrait d’identifier les problèmes communs dans l’utilisation de l’API. C'est en gros comme le débogage de Direct3D – qui est familier pour Direct3D mais pratiquement inconnu dans le monde OpenGL (en théorie ARB_debug_callback est censé résoudre ce problème, en pratique il est la plupart du temps indisponible et quand il l'est, pas terriblement utile)
  • Un débogueur GPU fonctionnel qui affiche toutes les commandes que vous avez envoyé ainsi que leur état, le contenu de la cible rendu, le contenu des textures, etc. Je ne sais pas si le débogueur de shaders fonctionne, car je n'en ai jamais eu besoin et l'inspection de la mémoire tampon pourrait être un peu plus facile, mais il fait surtout le travail.
  • Un profileur GPU fonctionnel qui indique les statistiques de performance par passage (temps, bande passante) et aussi le temps d’exécution par shader. Comme le GPU est un carreleur, vous ne pouvez pas vraiment vous attendre à des délais par appel, bien sûr. Avoir ce niveau de visibilité – surtout si l’on considère l’absence totale d’informations de timing GPU dans les API graphiques sur iOS – est une bonne option.
  • Une trace chronologique CPU / GPU (Metal System Trace) qui montre l’ordonnancement de la charge de travail du CPU et du GPU, similaire à GPUView mais en fait facile à utiliser, modulant quelques particularités de l’interface utilisateur.
  • Un compilateur de shader hors ligne qui valide la syntaxe de votre shader, vous donne occasionnellement des avertissements utiles, convertit votre shader en un blob binaire assez rapide à charger à l'exécution et en outre raisonnablement bien optimisé au préalable, ce qui réduit les temps de chargement puisque le compilateur du driver peut être plus rapide.

Si vous venez du monde de Direct3D ou de la console, vous pouvez prendre chacun d'entre eux pour acquis – croyez-moi, en OpenGL, chacun d'entre eux est habituel et suscite l'enthousiasme en particulier sur mobile où vous avez l 'habitude de vous occuper de drivers occasionnellement défectueux, pas de validation, pas de débogueur GPU, pas de profileur GPU utile, pas de capacité à rassembler des données de planification GPU et être obligé de travailler avec un langage de shaders basé sur du texte pour lequel chaque fournisseur a un analyseur légèrement différent.

Metal est une excellente API pour écrire du code et expédier des applications. Il est facile à utiliser, ses performances sont prévisibles, ses drivers sont robustes et ses outils solides. Il bat OpenGL sur tous les plans sauf celui de la portabilité, mais la réalité d'OpenGL est que vous n'auriez dû utiliser que sur trois plateformes (iOS, Android et Mac), et deux d'entre elles supportent maintenant le métal ; de plus, la promesse de portabilité d’OpenGL n’est pas vraiment tenue car le code que vous écrivez sur une plateforme finit très souvent par ne pas fonctionner sur une autre pour différentes raisons.

Si vous utilisez un moteur tiers comme Unity ou UE4, Metal y est déjà supporté; si ce n’est pas le cas et que vous aimez la programmation graphique ou que vous vous souciez profondément des performances et prenez iOS ou Mac au sérieux, je vous conseille vivement d’essayer Metal. Vous ne serez pas déçu.

Métal maintenant

De notre point de vue, les plus grands changements qui sont arrivés sur Metal au cours des trois dernières années concernent son adoption à grande échelle.

Il y a trois ans, un quart des appareils utilisent OpenGL. Aujourd’hui, pour notre public, ce chiffre est d’environ 2% – ce qui signifie que notre backend OpenGL n’a plus guère d’importance. Nous la maintenons encore, mais cela ne durera pas longtemps.

Les drivers sont également meilleurs que jamais – en général, nous ne voyons pas de problèmes de drivers sur iOS, et quand nous en voyons, ils surviennent souvent sur les premiers prototypes, et lorsque les prototypes sont mis en production, les problèmes sont encore résolus .

Nous avons également passé un certain temps à améliorer notre backend Metal, en nous concentrant sur trois domaines:

Refonte de la chaîne de compilation des shaders

Une autre chose qui s’est produite au cours des trois dernières années est la sortie et le développement de Vulkan. Bien qu'il semble que les API soient complètement différentes (et elles le sont), l'écosystème de Vulkan a donné à la communauté de rendu un ensemble fantastique d'outils open-source qui, qui sont combinés, donnent un ensemble outils de compilation de qualité de production facile à utiliser.

Nous avons utilisé les bibliothèques pour construire une chaîne d'outils de compilation qui peut prendre le code source HLSL (en utilisant diverses fonctions DX11, y compris les shaders de calcul), le compiler en SPIRV, optimiser ledit SPIRV et convertir le SPIRV résultant en MSL (Metal Shading Language). Il remplace notre précédente chaîne d’outils qui ne pouvait pas utiliser que la source HLSL DX9 en entrée et présentait divers problèmes de correction pour les shaders compliqués.

Il est quelque peu ironique qu'Apple n'était rien à voir avec, mais nous y voilà. Un grand merci aux contributeurs et mainteneurs de glslang (https://github.com/KhronosGroup/glslang), spirv-opt (https://github.com/KhronosGroup/SPIRV-Tools) et SPIRV-Cross (https: // github.com/KhronosGroup/SPIRV-Cross). Nous avons fourni un ensemble de correctifs à ces bibliothèques pour nous aider à expédier également la nouvelle chaîne d’outils, et l’utiliser pour recevoir nos shaders sur les API Vulkan, Metal et OpenGL.

Soutenir macOS

Un transfert sur MacOS a toujours été une possibilité, mais n’était pas une priorité pour nous jusqu’à ce qu’il nous manque quelques fonctionnalités. Nous avons donc décidé d’investir dans Metal sur macOS pour obtenir un rendu plus rapide et débloquer certaines possibilités pour l’avenir.

Du point de vue de la mise en œuvre, cela n’a pas été très difficile. La plupart des API sont exactement les mêmes; à part la gestion des fenêtres, le seul domaine qui a besoin des ajustements importants est l’allocation de la mémoire. Sur le mobile, il y a un espace mémoire partagé pour les tampons et les textures alors que sur le bureau, l’API suppose un GPU dédié avec sa propre mémoire vidéo.

Il est possible de contourner rapidement ce problème en utilisant des ressources gérées où le runtime Metal se charge de copier les données pour vous. C’est ainsi que nous avons livré notre première version, mais nous avons ensuite retravaillé la mise en œuvre pour copier plus explicitement les données de ressources en utilisant des tampons de grattage afin de minimiser la surcharge de mémoire du système

La plus grande différence entre macOS et iOS était la stabilité. Sur iOS, nous avions affaire à un seul fournisseur de drivers sur une architecture, alors que sur macOS, nous devions soutenir les trois fournisseurs (Intel, AMD, NVidia). De plus, sur iOS – fort heureusement! – a sauté la * première * version d’iOS où Metal était disponible, iOS 8, et sur macOS ce n’était pas pratique parce que nous aurions trop peu d’utilisateurs pour utiliser Metal à l’époque. En raison de la combinaison de ces problèmes, nous avons rencontré beaucoup plus de problèmes liés aux drivers dans les domaines relativement inoffensifs et relativement obscurs de l’API sur macOS.

Nous supportons toujours toutes les versions de macOS Metal (10.11+), bien que nous ayons commencé à supprimer le support et à passer à l'ancien backend OpenGL pour certaines versions avec des bugs connus du compilateur de shaders qui sont difficiles à contourner, par exemple sur 10.11 nous avons maintenant besoin de macOS 10.11.6 pour que Metal fonctionne.

Les avantages en termes de performances étaient conformes à nos attentes; en termes de parts de marché, nous sommes aujourd’hui à environ 25% d’utilisateurs OpenGL et à environ 75% d’utilisateurs Metal sur la plate-forme macOS, ce qui est une répartition plutôt saine. Cela signifie qu'à un moment donné dans le futur, il pourrait être pratique pour nous d'arrêter de supporter OpenGL, car aucune autre plateforme que nous supportons ne l'utilise, ce qui est bien en termes de pouvoir se concentrer sur des API qui sont plus faciles à supporter et à obtenir de bonnes performances.

Itération sur la performance et la consommation de mémoire

Nous sommes historiquement assez conservateurs en ce qui concerne les fonctionnalités de l’API graphique que nous utilisons, et Metal ne fait pas exception. Il existe plusieurs grandes mises à jour à jour de fonctionnalités que Metal a acquises au fil des ans, notamment des API d'allocation de ressources améliorées avec des montages explicites, des tile shaders avec Metal 2, des tampons d'arguments et la génération de commandes côté GPU, etc.

La plupart du temps, nous n'utilisons aucune des nouvelles fonctionnalités. Jusqu'à présent, les performances ont été raisonnables, et nous aimerions nous concentrer sur les améliorations qui s'appliquent à l'ensemble du système, donc quelque chose comme les shaders de tuiles, qui nous oblige à mettre en place un support très spécial pour cela dans tout le dispositif et qui n'est accessible que sur du matériel récent, est moins intéressant.

Cela dit, nous passons un certain temps à régler diverses parties du backend pour qu'il fonctionne plus rapidement – en utilisant des chargements de textures complètement asynchrones pour réduire le saut d'images pendant les chargements de niveaux, ce qui était très facile, en effectuant les optimisations de mémoire susmentionnées sur macOS, en optimisant la répartition du CPU à divers endroits du backend en intégrant les erreurs de cache, etc. et – l'une des seules nouvelles fonctionnalités que nous prenons explicitement en charge – en utilisant le stockage de textures sans mémoire lorsqu'il est disponible pour réduire la mémoire requise pour notre nouveau système d'ombre.

L’avenir

Dans l'ensemble, le fait que nous n'ayons pas eu à passer trop de temps sur les améliorations de Metal est en fait une bonne chose – le code qui a été écrit il y a 3 ans, en grande partie, fonctionne et est rapide et stable, ce qui est un bon signe d'une API mature. Le passage sur métal a été un grand investissement, étant donné que le temps est tombé et les avantages continus qu'il nous apporte, à nous et à nos utilisateurs.

Nous réévaluons constamment l’équilibre entre la quantité de travail que nous faisons pour les différentes API – il est très probable que nous devrons plonger plus profondément dans des parties plus modernes de Metal API pour certains des futurs projets de rendu; si cela se produit, nous ne manquerons pas d’écrire un autre billet à ce sujet!


  1. Oui, d’accord, et peut-être une semaine pour corriger quelques bugs découverts lors des tests ↩
  2. Les chiffres correspondant à 128 Ko de données mises à jour par trame (deux régions 32x16x32 RGBA8) sur l’A10 ↩

Ni Roblox Corporation ni ce blog ne sont prudents ni ne soutiennent aucune entreprise ou service. En outre, aucune garantie ou promesse n’est faite quant à l’exactitude, la fiabilité ou l’exhaustivité des informations contenues dans ce blog.

Cet article de blog a été publié à l’origine sur Roblox Tech Blog.