Actualités / Jeux

3 ans de métal – Roblox Blog

Il y a trois ans, nous avons porté notre moteur de rendu sur Metal. Cela n'a pas pris beaucoup de temps, c'était génial et cela a très bien fonctionné sur iOS. Nous avons donc écrit un article expliquant comment 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 republier avec notre mise à jour de trois ans.

Alors, remontons le temps, faisons comme si c'était décembre 2016 et nous venons de livrer une version de notre rendu Metal 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 disponible que sur le matériel le plus récent que la plupart de nos utilisateurs ne possédaient pas, et bien qu'Apple ait déclaré qu'il résolvait les problèmes de performances du processeur, 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 exécutions OpenGL ES 2 uniquement sur Apple, et commençions également à porter sur Android.

Avance rapide de deux ans et demi, voici à quoi ressemble la part de marché du métal pour nos utilisateurs:

C'est beaucoup plus attrayant qu'auparavant. Il est toujours vrai que l'implémentation de Metal n'aide pas les appareils les plus anciens, mais le marché GL sur iOS ne cesse de se réduire, et le contenu que nous exécutons sur ces anciens appareils est souvent différent du contenu qui s'exécute sur les appareils les plus récents, il est donc logique de consacrer des 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 axé sur les mobiles (nous ne livrons actuellement que des versions Metal sur iOS).

Je pense qu'il vaut la peine d'analyser la part de marché un peu plus en détail. Sur iOS, nous prenons en charge Metal pour iOS 8.3+; même si certains utilisateurs ne peuvent pas exécuter Metal en raison des restrictions de version du système d'exploitation, la plupart des 25% qui exécutent toujours GL utilisent simplement des appareils plus anciens dotés de matériel SGX. Ils n'ont pas non plus de fonctionnalités OpenGL ES 3, et nous nous contentons d'exécuter un chemin de rendu bas de gamme là-bas (bien que nous adorions que tous les appareils deviennent Metal – heureusement, la répartition GL / Metal ne fera que s'améliorer). Sur Mac, l'API Metal est plus récente 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 daté – c'est moins le matériel que le logiciel (95 % de nos utilisateurs Mac exécutent OpenGL 3.2+).

Donc, étant donné la part de marché, nous avons encore des options qui n'impliquent pas le portage vers Metal. L'un d'eux consiste à utiliser simplement MoltenGL, qui utiliserait le code OpenGL que nous avons déjà, mais qui serait censé être plus rapide; une autre consiste à porter sur Vulkan (pour obtenir de meilleures performances sur PC et éventuellement Android) et utiliser MoltenVK. J'ai brièvement évalué MoltenGL et je n'étais pas trop ravi des résultats – cela a pris un certain effort pour faire fonctionner notre code, et bien que les performances soient un peu meilleures par rapport à OpenGL stock, j'espérais plus. Quant à MoltenVK, je pense qu'il est erroné d'essayer d'implémenter une API de bas niveau en tant que couche au-dessus d'une autre – vous risquez d'obtenir une incompatibilité d'impédance qui se traduira par des performances sous-optimales – peut-être que ce sera mieux que le haut niveau API que vous utilisiez auparavant, mais il est peu probable qu'elle soit aussi rapide que possible, c'est pourquoi vous choisissez d'abord une API de bas niveau! Un autre aspect important est que l'implémentation de Metal est beaucoup plus simple que Vulkan – plus à ce sujet plus tard – donc dans un certain sens, je préférerais un wrapper Metal -> Vulkan au lieu d'un Vulkan -> Metal.

Il convient également de noter qu'apparemment sur iOS 10 sur les derniers iPhones, il n'y a pas de pilote GL – GL est implémenté au-dessus de Metal. Ce qui signifie qu'utiliser OpenGL ne vous fait vraiment économiser qu'un peu d'effort de développement – pas tant que cela, étant donné que la promesse «d'écrire une fois, exécuter n'importe où» qu'OpenGL ne fonctionne pas vraiment sur mobile.

Portage

Dans l'ensemble, le portage vers Metal a été un jeu d'enfant. Nous avons beaucoup d'expérience 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 un avantage unique de pouvoir utiliser confortablement une API comme Metal qui est simultanément de niveau raisonnablement élevé, mais laisse également certaines tâches comme la synchronisation CPU-GPU pour le développeur de l'application.

Le seul obstacle était vraiment de compiler nos shaders – une fois cela fait et il était temps d'écrire le code, il est devenu évident que l'API est si simple et explicite que le code s'est pratiquement écrit lui-même. J'ai obtenu le port qui rendait la plupart des choses d'une manière sous-optimale en environ 10 heures en une seule journée, et j'ai passé deux semaines de plus à nettoyer le code, à résoudre les problèmes de validation, à profiler et à optimiser et à effectuer un polissage général. Obtenir une implémentation d'API dans ce laps de temps en dit long sur la qualité de l'API et du jeu d'outils. Je crois qu'il y a plusieurs aspects qui contribuent:

  • Vous pouvez développer le code de manière incrémentielle, avec de bons commentaires à chaque étape. 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 rencontrer des problèmes; la phase d'optimisation / polissage a ensuite converti cela en quelque chose que nous pourrions expédier, 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 autant une surprise pour les gens habitués à Direct3D 11 – mais c'est la première fois sur mobile où j'avais un profileur CPU, un profileur GPU, un débogueur GPU et une couche de validation API GPU qui fonctionnaient tous bien dans en tandem, détectant la plupart des problèmes pendant le développement et aidant à optimiser le code.
  • Bien que l'API soit un niveau légèrement inférieur à Direct3D 11, et qu'elle laisse certaines décisions de bas niveau clés au développeur (telles que la configuration de la passe de rendu ou la synchronisation), elle utilise toujours un modèle de ressource traditionnel où chaque ressource a certains «indicateurs d'utilisation» ”Il a été créé avec 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 a plusieurs emplacements auxquels vous pouvez librement affecter des ressources. Les deux sont familiers, faciles à comprendre et nécessitent une quantité très limitée de code pour démarrer rapidement.

Une autre chose qui a aidé est que notre interface API était prête pour les API de type Metal – elle est très allégée mais elle expose suffisamment de détails (comme les passes de rendu) pour pouvoir facilement écrire une implémentation performante. À aucun moment de notre implémentation, je n'ai eu besoin d'enregistrer / restaurer l'état (de nombreuses interfaces API en souffrent, en particulier en raison du traitement de la configuration de la cible de rendu comme des changements d'état et des ressources / liaison d'état persistant) ou de prendre des décisions compliquées concernant la durée de vie / la synchronisation des ressources . Le seul morceau de code «compliqué» nécessaire pour le rendu est celui qui crée l'état du pipeline de rendu en hachant les bits qui sont nécessaires pour en créer un – les objets d'état du pipeline ne font pas partie de notre abstraction API. Même cela est assez simple et rapide. J'écrirai plus sur notre interface API dans un article séparé.

Donc, une semaine pour obtenir la compilation des shaders, deux semaines pour obtenir une implémentation optimisée et raffinée1 – quels sont les résultats? Les résultats sont excellents – Metal tient absolument ses promesses de performances. D'une part, les performances de répartition sur un seul thread sont sensiblement meilleures qu'avec OpenGL (en réduisant la partie de répartition de tirage de notre cadre de rendu de 2 à 3 fois en fonction de la charge de travail), et cela est donné que notre implémentation d'OpenGL est assez bien réglée en termes de réduction configuration de l'état redondant et jouer avec le pilote en utilisant des chemins rapides. Mais cela ne s'arrête pas là – le multithreading dans Metal est simple à utiliser à condition que votre code de rendu soit prêt pour cela. Nous ne sommes pas encore passés à la distribution de tirages filetés mais convertissons déjà d'autres parties qui préparent les ressources à se produire hors du fil de rendu, ce qui, contrairement à OpenGL, est à peu près sans effort.

Au-delà de cela, Metal nous permet de résoudre d'autres problèmes de performances en fournissant des outils facilement accessibles et fiables. L'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écharge 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 totalité de la texture et nous devons nous en remettre, cependant, le pilote implémente glTexSubImage3D. À un moment donné, nous avons essayé d'utiliser le PBO pour améliorer les performances de mise à jour, mais nous avons été confrontés à d'importants problèmes de stabilité à tous les niveaux, à la fois sur Android et iOS. Sur Metal, il existe deux façons 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 que le GPU commence à utiliser le GPU texture.

Ces deux méthodes étaient plus lentes que je le souhaiterais – la première n'était pas vraiment disponible car nous devions prendre en charge des mises à jour partielles efficaces, et cela fonctionnait uniquement sur le processeur en utilisant ce qui ressemblait à une implémentation de traduction d'adresse très lente. Le second a fonctionné mais semblait utiliser une série de blits 2D pour remplir la texture 3D qui étaient à la fois assez chers pour configurer des commandes côté CPU et avaient également un surdébit GPU très élevé pour une raison quelconque. S'il s'agissait d'OpenGL, ce serait trop – en fait, les performances de ces deux méthodes correspondaient à peu près au coût observé d'une mise à jour similaire dans OpenGL. Heureusement, étant Metal, il a un accès facile aux shaders de calcul – et un shader de calcul super simple nous a donné la possibilité de faire un tampon -> Téléchargement de texture 3D qui était très rapide sur le CPU et le GPU et a essentiellement résolu nos problèmes de performances dans cette partie du code pour de bon2:

En guise de commentaire général final, la maintenance du code Metal est à peu près aussi facile – toutes les fonctionnalités supplémentaires que nous avons dû ajouter jusqu'à présent étaient plus faciles à ajouter que sur toute autre API que nous prenons en charge, et je m'attends à ce que cette tendance se poursuive. On craignait un peu que l'ajout d'une API supplémentaire nécessite une maintenance constante, mais par rapport à OpenGL, cela ne nécessite pas vraiment beaucoup de travail; en fait, comme nous n'aurons plus à prendre en charge OpenGL ES 3 sur iOS, cela signifie que nous pouvons également simplifier le code OpenGL que nous avons.

La stabilité

Aujourd'hui, sur iOS, Metal se sent très stable. Je ne sais pas à quoi ressemblait la situation au lancement en 2014, ni à quoi elle ressemble aujourd'hui sur Mac, mais les pilotes et les outils pour iOS semblent assez solides.

Nous avons eu un problème de pilote sur iOS 10 lié au chargement de shaders compilés avec Xcode 7 (que nous avons corrigé en passant à Xcode 8), et un crash de pilote sur iOS 9 qui s'est avéré être le résultat d'une mauvaise utilisation de l'API NextDrawable. En dehors de cela, nous n'avons vu aucun bogue comportemental ou crash – pour un API Metal relativement nouveau, il a été très solide dans tous les domaines.

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

  • Une couche de validation assez complète qui identifiera les problèmes courants liés à l'utilisation de l'API. C'est essentiellement comme le débogage Direct3D – qui est familier pour Direct3D mais quasiment inconnu en terrain OpenGL (en théorie, ARB_debug_callback est censé résoudre ce problème, dans la pratique, il est principalement indisponible et lorsqu'il n'est pas très utile)
  • Un débogueur GPU fonctionnel qui montre toutes les commandes que vous avez envoyées avec leur état, le contenu cible du rendu, le contenu de la texture, etc. Je ne sais pas s'il a un débogueur de shader fonctionnel parce que je n'en ai jamais eu besoin, et l'inspection du tampon pourrait être un peu plus facile, mais ça fait surtout le boulot.
  • Un profileur GPU fonctionnel qui affiche les statistiques de performance par passe (temps, bande passante) et également le temps d'exécution par shader. Étant donné que le GPU est un carreleur, vous ne pouvez pas vraiment vous attendre à des synchronisations par appel nul, bien sûr. Avoir ce niveau de visibilité – en particulier compte tenu du manque total d'informations de synchronisation GPU dans les API graphiques sur iOS – est formidable.
  • Une trace chronologique CPU / GPU fonctionnelle (Metal System Trace) qui montre la planification de la charge de travail de rendu CPU et GPU, similaire à GPUView mais en fait facile à utiliser, modulo certaines idiosyncrasies de l'interface utilisateur.
  • Un compilateur de shaders hors ligne qui valide votre syntaxe de shader, vous donne parfois des avertissements utiles, convertit votre shader en un blob binaire qui est assez rapide à charger au moment de l'exécution et en outre raisonnablement bien optimisé au préalable, réduisant les temps de chargement car le compilateur de pilotes peut être plus rapide.

Si vous venez de Direct3D ou du monde de la console, vous pouvez prendre chacun d'eux pour acquis – croyez-moi, dans OpenGL, chacun d'eux est inhabituel et suscite de l'excitation, en particulier sur mobile où vous avez l'habitude de faire face à des bris occasionnels pilotes, pas de validation, pas de débogueur GPU, pas de profileur GPU utile, pas de possibilité de rassembler des données de planification GPU et d'être obligé de travailler avec un langage de shader 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, il a des performances prévisibles, il a des pilotes robustes et un ensemble d'outils solide. Il bat OpenGL dans tous ses aspects, sauf pour la portabilité, mais la réalité avec OpenGL est que vous ne devriez vraiment l'avoir utilisé que sur trois plates-formes (iOS, Android et Mac), et deux d'entre elles prennent désormais en charge Metal; en outre, la promesse de portabilité d'OpenGL n'est en général pas remplie car le code que vous écrivez sur une plate-forme 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à pris en charge; si vous n'êtes pas et que vous aimez la programmation graphique ou que vous vous souciez profondément des performances et que vous prenez iOS ou Mac au sérieux, je vous invite fortement à essayer Metal. Vous ne serez pas déçu.

Metal Now

Les plus grands changements qui sont arrivés à Metal de notre point de vue au cours des trois dernières années concernent l'adoption à grande échelle.

Il y a trois ans, un quart des appareils devaient utiliser OpenGL. Aujourd'hui, pour notre public, ce nombre est d'environ 2% – ce qui signifie que notre backend OpenGL n'a plus vraiment d'importance. Nous le maintenons toujours mais cela ne durera pas longtemps.

Les pilotes sont également meilleurs que jamais – de manière générale, nous ne voyons pas de problèmes de pilotes sur iOS, et lorsque nous le faisons, ils se produisent souvent sur les premiers prototypes, et au moment où les prototypes arrivent en production, les problèmes sont généralement résolus.

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

Retravailler la chaîne d'outils de compilation de shader

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 Vulkan a donné à la communauté de rendu un ensemble fantastique d'outils open source qui, lorsqu'ils sont combinés, aboutissent à un ensemble d'outils de compilation de qualité de production facile à utiliser.

Nous avons utilisé les bibliothèques pour créer une chaîne d'outils de compilation qui peut prendre le code source HLSL (en utilisant diverses fonctionnalités 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 chaîne d'outils précédente qui ne pouvait utiliser que la source DX9 HLSL comme entrée et présentait divers problèmes de correction pour les shaders complexes.

Il est quelque peu ironique qu’Apple n’ait rien à voir avec cela, mais nous y voilà. Un grand merci aux contributeurs et aux 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 recibler nos shaders vers les API Vulkan, Metal et OpenGL.

Prise en charge macOS

Un portage macOS a toujours été une possibilité, mais n'était pas une grande préoccupation pour nous jusqu'à ce que nous commencions à manquer certaines 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 du tout. La plupart de l'API est exactement la même; à part la gestion des fenêtres, le seul domaine qui nécessitait des ajustements importants était l'allocation de mémoire. Sur mobile, il y a un espace de mémoire partagé pour les tampons et les textures tandis que sur le bureau, l'API suppose un GPU dédié avec sa propre mémoire vidéo.

Il est possible de contourner rapidement cela 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é l'implémentation pour copier plus explicitement les données des ressources à l'aide de tampons de travail afin de minimiser la surcharge de la mémoire système.

La plus grande différence entre macOS et iOS était la stabilité. Sur iOS, nous avions affaire à un seul fournisseur de pilotes sur une architecture, tandis que sur macOS, nous devions prendre en charge les trois fournisseurs (Intel, AMD, NVidia). De plus, sur iOS, nous – heureusement! – ignoré la * première * version d'iOS où Metal était disponible, iOS 8, et sur macOS, cela n'était pas pratique car 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 de pilotes dans les zones relativement inoffensives et relativement obscures de l'API sur macOS.

Nous prenons toujours en charge toutes les versions de macOS Metal (10.11+), bien que nous ayons commencé à supprimer la prise en charge et à passer au backend OpenGL hérité pour certaines versions avec des bogues de compilateur de shader connus 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 part de marché, nous sommes aujourd'hui à ~ 25% d'OpenGL et ~ 75% d'utilisateurs de Metal sur la plate-forme macOS, ce qui est une répartition assez saine. Cela signifie qu'à un certain moment dans l'avenir, il peut être pratique pour nous d'arrêter de prendre en charge OpenGL de bureau, car aucune autre plate-forme que nous prenons en charge ne l'utilise, ce qui est génial en termes de pouvoir se concentrer sur des API plus faciles à prendre en charge et obtenir de bonnes performances avec.

Itération sur les performances et la consommation de mémoire

Nous sommes historiquement assez conservateurs avec les fonctionnalités de l'API graphique que nous utilisons, et Metal ne fait pas exception. Il y a plusieurs grandes mises à jour de fonctionnalités acquises par Metal au fil des ans, notamment des API d'allocation des ressources améliorées avec des tas explicites, des shaders de tuiles 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 à tous les niveaux, donc quelque chose comme les shaders de tuiles, qui nous obligent à implémenter un support très spécial dans tout le rendu et n'est accessible que sur du matériel plus récent , est moins intéressant.

Cela dit, nous passons un certain temps à régler différentes parties du backend pour qu'elles s'exécutent * plus rapidement * – en utilisant des téléchargements de texture complètement asynchrones pour réduire le bégaiement lors des chargements de niveau, ce qui était complètement indolore, en faisant les optimisations de mémoire susmentionnées sur macOS, en optimisant le processeur répartissez à divers endroits du backend en réduisant les erreurs de cache, etc., et – l'une des seules fonctionnalités les plus récentes pour lesquelles nous avons un support explicite – en utilisant le stockage de texture sans mémoire lorsqu'il est disponible pour réduire considérablement la mémoire requise pour notre nouveau système fantôme.

L'avenir

Dans l'ensemble, le fait que nous n'ayons pas passé 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 gros, fonctionne et est rapide et stable, ce qui est un excellent signe de API mature. Le portage vers le métal a été un excellent investissement, étant donné le temps qu'il a fallu et les avantages continus qu'il nous apporte ainsi qu'à nos utilisateurs.

Nous réévaluons constamment l'équilibre entre la quantité de travail que nous faisons pour différentes API – il est très probable que nous devrons approfondir les parties plus modernes de l'API Metal pour certains des futurs projets de rendu; si cela se produit, nous nous assurerons d'écrire un autre article à ce sujet!


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