Notizie / Giochi

3 anni di metallo – Blog Roblox

Tre anni fa abbiamo portato il nostro renderer su Metal. Non ci è voluto molto, è stato fantastico e ha funzionato benissimo su iOS. Quindi abbiamo scritto un articolo spiegando come abbiamo preso la nostra decisione e come è andata a finire (spoiler: davvero buono!). La maggior parte di quella retrospettiva originale è ancora valida, ma oggi il Metal è più in forma che mai, quindi abbiamo deciso di ripubblicarlo con il nostro aggiornamento triennale.

Quindi torniamo indietro nel tempo, fingendo che sia dicembre 2016 e che abbiamo appena rilasciato una versione del nostro renderer Metal su iOS.

Perché metallo?

Quando Apple ha annunciato Metal al WWDC nel 2014, la mia prima reazione è stata quella di ignorarlo. Era disponibile solo sull'hardware più recente che la maggior parte dei nostri utenti non aveva, e mentre Apple ha affermato che risolve i problemi di prestazioni della CPU, l'ottimizzazione per il mercato più piccolo significherebbe che il divario tra i dispositivi più veloci e quelli più lenti si amplierebbe ulteriormente. A quel tempo, eseguivamo OpenGL ES 2 solo su Apple e stavamo iniziando a trasferirci anche su Android.

Avanti veloce di due anni e mezzo, ecco come appare la quota di mercato dei metalli per i nostri utenti:

È molto più attraente di prima. È ancora vero che l'implementazione Metal non aiuta i dispositivi più vecchi, ma il mercato GL su iOS continua a ridursi e il contenuto che eseguiamo su quei dispositivi più vecchi è spesso diverso dal contenuto che gira su dispositivi più recenti, quindi ha senso mettere sforzo per renderlo più veloce. Poiché il tuo codice iOS Metal funzionerà su Mac con pochissime modifiche, potrebbe essere una buona idea usarlo anche su Mac anche se sei orientato ai dispositivi mobili (attualmente spediamo solo versioni Metal su iOS).

Penso che valga la pena analizzare la quota di mercato un po' più in dettaglio. Su iOS supportiamo Metal per iOS 8.3+; anche se alcuni utenti non possono eseguire Metal a causa delle restrizioni sulla versione del sistema operativo, la maggior parte del 25% che esegue ancora GL utilizza solo dispositivi meno recenti con hardware SGX. Non hanno nemmeno le funzionalità OpenGL ES 3 e stiamo solo eseguendo un percorso di rendering di fascia bassa lì (anche se amiamo che tutti i dispositivi siano in metallo - fortunatamente la divisione GL/Metal non migliorerà solo). Su Mac l'API Metal è più recente e il sistema operativo gioca un ruolo piuttosto importante: devi utilizzare OSX 10.11+ per utilizzare Metal e metà dei nostri utenti ha solo un sistema operativo più datato: è meno hardware che software (il 95% dei nostri Gli utenti Mac eseguono OpenGL 3.2+).

Quindi, data la quota di mercato, abbiamo ancora opzioni che non prevedono il porting su Metal. Uno di questi è usare semplicemente MoltenGL, che userebbe il codice OpenGL che già abbiamo, ma che dovrebbe essere più veloce; un altro è il porting su Vulkan (per prestazioni migliori su PC e possibilmente Android) e utilizzare MoltenVK. Ho valutato brevemente MoltenGL e non ero molto entusiasta dei risultati: ci è voluto un po' di sforzo per far funzionare il nostro codice e, sebbene le prestazioni fossero un po' migliori rispetto all'OpenGL stock, speravo in di più. Per quanto riguarda MoltenVK, penso che sia sbagliato provare a implementare un'API di basso livello come uno strato sopra l'altro - si rischia di ottenere una mancata corrispondenza di impedenza che si tradurrà in prestazioni non ottimali - forse sarà migliore di quella di alto livello API che stavi utilizzando prima, ma è improbabile che sia il più veloce possibile, ecco perché scegli prima un'API di basso livello! Un altro aspetto importante è che l'implementazione di Metal è molto più semplice di Vulkan - ne parleremo più avanti - quindi in un certo senso preferirei un wrapper Metal -> Vulkan invece di un Vulkan -> Metal.

Va anche notato che apparentemente su iOS 10 sugli ultimi iPhone non c'è il driver GL – GL è implementato su Metal. Ciò significa che l'utilizzo di OpenGL consente di risparmiare solo un po' di sforzo di sviluppo, non così tanto, dato che "scrivi una volta, esegui ovunque" promette che OpenGL non funziona, non proprio sui dispositivi mobili.

Portage

Nel complesso, il passaggio a Metal è stato un gioco da ragazzi. Abbiamo molta esperienza con diverse API grafiche, che vanno da API di alto livello come Direct3D 9/11 ad API di basso livello come PS4 GNM. Ciò offre il vantaggio unico di poter utilizzare comodamente un'API come Metal che è contemporaneamente di livello ragionevolmente alto, ma lascia anche alcune attività come la sincronizzazione CPU-GPU allo sviluppatore dell'applicazione.

L'unico ostacolo era davvero la compilazione dei nostri shader: una volta terminato ed era ora di scrivere il codice, è diventato evidente che l'API è così semplice e autoesplicativa che il codice praticamente si scrive da solo. Ho ottenuto il port che ha reso la maggior parte delle cose in modo non ottimale in circa 10 ore in un solo giorno e ho trascorso altre due settimane a ripulire il codice, risolvere i problemi di convalida, correggere la profilazione, l'ottimizzazione e la rifinitura generale. Ottenere un'implementazione API in quel lasso di tempo dice molto sulla qualità dell'API e del set di strumenti. Credo che ci siano diversi aspetti che contribuiscono:

  • Puoi sviluppare il codice in modo incrementale, con un buon feedback ad ogni passaggio. Il nostro codice è iniziato ignorando tutte le sincronizzazioni CPU-GPU, essendo davvero non ottimale su alcune parti della configurazione dello stato, utilizzando il monitoraggio dei riferimenti integrato per le risorse e non eseguendo mai CPU e GPU in parallelo per evitare problemi di esperienza; la fase di ottimizzazione/lucidatura lo ha poi convertito in qualcosa che potevamo spedire, senza mai perdere la renderizzabilità nel processo.
  • Gli strumenti sono lì per te, funzionano e funzionano bene. Questa non è una grande sorpresa per le persone abituate a Direct3D 11, ma questa è la prima volta su dispositivi mobili in cui ho avuto un profiler CPU, un profiler GPU, un debugger GPU e un livello di convalida API GPU che funzionano tutti bene in tandem, catturando la maggior parte problemi durante lo sviluppo e aiutando a ottimizzare il codice.
  • Sebbene l'API sia un livello leggermente inferiore rispetto a Direct3D 11 e lasci allo sviluppatore alcune decisioni chiave di basso livello (come la configurazione o la tempistica del passaggio di rendering), utilizza ancora un modello di risorsa tradizionale in cui ogni risorsa ha determinati "flag di utilizzo" È stato creato con, ma non richiede barriere di pipeline o transizioni di layout, e un modello di associazione tradizionale in cui ogni fase dello shader ha più slot a cui è possibile allocare liberamente le risorse. Entrambi sono familiari, facili da capire e richiedono una quantità molto limitata di codice per iniziare rapidamente.

Un'altra cosa che ha aiutato è che la nostra interfaccia API era pronta per API simili a Metal: è molto leggera ma espone abbastanza dettagli (come i passaggi di rendering) per scrivere facilmente un'implementazione ad alte prestazioni. In nessun momento della nostra implementazione ho avuto bisogno di salvare/ripristinare lo stato (molte API ne soffrono, soprattutto a causa del trattamento della configurazione di destinazione del rendering come modifiche di stato e risorse/associazione di stato persistente) o prendere decisioni complicate sulla durata/tempo delle risorse. L'unico pezzo di codice "complicato" necessario per il rendering è quello che crea lo stato della pipeline di rendering eseguendo l'hashing dei bit necessari per crearne uno: gli oggetti dello stato della pipeline non fanno parte della nostra astrazione API. Anche questo è abbastanza semplice e veloce. Scriverò di più sulla nostra interfaccia API in un articolo separato.

Quindi, una settimana per compilare gli shader, due settimane per ottenere un'implementazione ottimizzata e raffinata1: quali sono i risultati? I risultati sono eccellenti: il metallo mantiene assolutamente le sue promesse di prestazioni. Per prima cosa, le prestazioni di invio su un singolo thread sono notevolmente migliori rispetto a OpenGL (riducendo la parte di invio del sorteggio del nostro framework di rendering di 2-3 volte a seconda del carico di lavoro), e questo Dato che la nostra implementazione OpenGL è abbastanza ben ottimizzata in termini di ridurre la configurazione dello stato ridondante e giocherellare con il driver utilizzando percorsi veloci. Ma non finisce qui: il multithreading in Metal è semplice da usare a condizione che il codice di rendering sia pronto per questo. Non siamo ancora passati alla distribuzione di stampa in thread, ma stiamo già convertendo altre parti che preparano le risorse a uscire dal thread di rendering, il che, a differenza di OpenGL, è praticamente semplice.

Oltre a ciò, Metal ci consente di affrontare altri problemi di prestazioni fornendo strumenti facilmente accessibili e affidabili. Una delle parti centrali del nostro codice di rendering è il sistema che calcola i dati di illuminazione sulla CPU nello spazio mondiale e li carica in regioni di una trama 3D (che dobbiamo emulare sull'hardware OpenGL ES 2). Gli aggiornamenti sono parziali, quindi non possiamo duplicare l'intera texture e dobbiamo fare affidamento su di essa, tuttavia, il driver implementa glTexSubImage3D. A un certo punto, abbiamo provato a utilizzare PBO per migliorare le prestazioni di aggiornamento, ma abbiamo riscontrato notevoli problemi di stabilità su tutta la linea, sia su Android che su iOS. Su Metal, ci sono due modi integrati per scaricare una regione: MTLTexture.replaceRegion che puoi usare se la GPU non sta attualmente leggendo la texture, o MTLBlitCommandEncoder (copyFromBufferToTexture o copyFromTextureToTexture) che può scaricare la regione in modo asincrono appena in tempo per il La GPU inizia a utilizzare la GPU Texture.

Entrambi questi metodi erano più lenti di quanto avrei voluto: il primo non era realmente disponibile poiché dovevamo supportare aggiornamenti parziali efficienti e funzionava solo sulla CPU utilizzando quella che sembrava un'implementazione di traduzione di indirizzi molto lenti. Il secondo ha funzionato ma sembrava utilizzare una serie di blit 2D per riempire la trama 3D che erano entrambi piuttosto costosi per impostare i controlli laterali della CPU e avevano anche un sovraccarico della GPU molto elevato per qualche motivo. Se fosse OpenGL, sarebbe troppo, infatti, le prestazioni di entrambi i metodi corrispondevano all'incirca al costo osservato di un aggiornamento simile in OpenGL. Fortunatamente essendo Metal ha un facile accesso agli shader di calcolo e uno shader di calcolo super semplice ci ha dato la possibilità di eseguire Buffer -> 3D Texture Download che era molto veloce su CPU e GPU e sostanzialmente ha risolto i nostri problemi di prestazioni in questa parte del codice per sempre2:

Come ultimo commento generale, mantenere il codice Metal è altrettanto facile: tutte le funzionalità extra che abbiamo dovuto aggiungere finora erano più facili da aggiungere rispetto a qualsiasi altra API che supportiamo e mi aspetto che questa tendenza continui. C'era un po' di preoccupazione sul fatto che l'aggiunta di un'API aggiuntiva richiedesse una manutenzione costante, ma rispetto a OpenGL, non richiede molto lavoro; infatti, dal momento che non dovremo più supportare OpenGL ES 3 su iOS, ciò significa che possiamo anche semplificare il codice OpenGL che abbiamo.

Stabilità

Oggi su iOS, Metal sembra molto stabile. Non so com'era al lancio nel 2014, o come appare oggi su Mac, ma i driver e gli strumenti per iOS sembrano piuttosto solidi.

Abbiamo riscontrato un problema di driver su iOS 10 relativo al caricamento degli shader compilati con Xcode 7 (che abbiamo risolto aggiornando a Xcode 8) e un crash del driver su iOS 9 che si è rivelato essere il risultato di un uso improprio dell'API NextDrawable. A parte questo, non abbiamo riscontrato bug comportamentali o arresti anomali: per un'API Metal relativamente nuova, è stata molto solida su tutta la linea.

Inoltre, gli strumenti che ottieni con Metal sono vari e ricchi; in particolare, puoi utilizzare:

  • Un livello di convalida abbastanza completo che identificherà i problemi comuni con l'utilizzo dell'API. È fondamentalmente come il debug di Direct3D, che è familiare a Direct3D ma praticamente sconosciuto nel terreno OpenGL (in teoria ARB_debug_callback dovrebbe risolvere questo problema, in pratica è per lo più non disponibile e quando non molto utile)
  • Un debugger GPU funzionale che mostra tutti i comandi che hai inviato con il loro stato, contenuto di destinazione di rendering, contenuto di texture, ecc. Non so se ha un debugger dello shader funzionante perché non ne ho mai avuto bisogno e l'ispezione del buffer potrebbe essere un po' più semplice, ma per lo più fa il suo lavoro.
  • Un profiler GPU funzionale che mostra le statistiche sulle prestazioni per passaggio (tempo, larghezza di banda) e anche il runtime per shader. Poiché la GPU è un piastrellista, ovviamente non puoi aspettarti tempi di chiamata zero. Avere questo livello di visibilità, soprattutto data la completa mancanza di informazioni sui tempi della GPU nelle API grafiche su iOS, è fantastico.
  • Una traccia funzionale della sequenza temporale di CPU/GPU (Metal System Trace) che mostra la pianificazione del carico di lavoro di rendering di CPU e GPU, simile a GPUView ma in realtà facile da usare, modulo alcune idiosincrasie dell'interfaccia utente.
  • Un compilatore di shader offline che convalida la sintassi dello shader, a volte fornisce utili avvisi, converte lo shader in un BLOB binario che è abbastanza veloce da caricare in fase di esecuzione e inoltre ragionevolmente ben ottimizzato in anticipo, riducendo i tempi di caricamento perché il compilatore del driver può essere più veloce.

Se provieni da Direct3D o dal mondo delle console, puoi darli tutti per scontati – credimi, in OpenGL ognuno di loro è insolito ed eccitante, specialmente sui dispositivi mobili dove sei abituato a fare i conti con interruzioni occasionali del conducente, no validazione, nessun debugger GPU, nessun profilatore GPU utile, nessuna capacità di raccogliere dati di pianificazione GPU ed essere costretto a lavorare con un linguaggio shader basato su testo per il quale ogni fornitore ha un parser leggermente diverso.

Metal è un'ottima API per scrivere codice e spedire app. È facile da usare, ha prestazioni prevedibili, ha driver robusti e un solido set di strumenti. Batte OpenGL in ogni aspetto tranne la portabilità, ma la realtà con OpenGL è che avresti dovuto usarlo solo su tre piattaforme (iOS, Android e Mac) e due di esse ora supportano il supporto Metal; inoltre, la promessa di portabilità di OpenGL generalmente non viene mantenuta perché il codice che scrivi su una piattaforma molto spesso finisce per non funzionare su un'altra per vari motivi.

Se stai utilizzando un motore di terze parti come Unity o UE4, Metal è già supportato lì; Se non lo sei e ami la programmazione grafica o ti preoccupi profondamente delle prestazioni e prendi sul serio iOS o Mac, ti esorto vivamente a provare Metal. Non rimarrete delusi.

Metallo ora

I più grandi cambiamenti che sono accaduti al Metal dal nostro punto di vista negli ultimi tre anni sono stati adottati su larga scala.

Tre anni fa, un quarto dei dispositivi doveva utilizzare OpenGL. Oggi, per il nostro pubblico, quel numero è di circa il 2%, il che significa che il nostro backend OpenGL non ha più importanza. Lo stiamo ancora mantenendo ma non durerà a lungo.

Anche i driver sono migliori che mai: in generale, non vediamo problemi di driver su iOS, e quando lo facciamo spesso si verificano sui primi prototipi, e quando i prototipi entrano in produzione i problemi di solito vengono risolti.

Abbiamo anche dedicato del tempo a migliorare il nostro backend Metal, concentrandoci su tre aree:

Rielabora la toolchain di compilazione degli shader

Un'altra cosa che è accaduta negli ultimi tre anni è il rilascio e lo sviluppo di Vulkan. Sebbene sembri che le API siano completamente diverse (e lo sono), l'ecosistema Vulkan ha fornito alla comunità di rendering un fantastico set di strumenti open source che, se combinati, danno come risultato un set di strumenti di costruzione di qualità di produzione facili da usare.

Abbiamo utilizzato le librerie per creare una toolchain di compilazione in grado di prendere il codice sorgente HLSL (utilizzando varie funzionalità DX11, inclusi gli shader di calcolo), compilarlo in SPIRV, ottimizzare detto SPIRV e convertire lo SPIRV risultante in MSL (Metal Shading Language). Sostituisce la nostra precedente toolchain che poteva utilizzare solo la sorgente HLSL DX9 come input e presentava vari problemi di correzione per shader complessi.

È un po' ironico che Apple non abbia nulla a che fare con questo, ma eccoci qui. Mille grazie ai contributori e ai manutentori di glslang (https://github.com/KhronosGroup/glslang), spirv-opt (https://github.com/KhronosGroup/SPIRV-Tools) e SPIRV-Cross (https: / / github.com/KhronosGroup/SPIRV-Cross). Abbiamo fornito una serie di correzioni a queste librerie per aiutarci a spedire anche la nuova toolchain e usarla per reindirizzare i nostri shader alle API Vulkan, Metal e OpenGL.

supporto per macOS

Una porta macOS è sempre stata una possibilità, ma non è stata una grande preoccupazione per noi fino a quando non abbiamo iniziato a perdere alcune funzionalità. Quindi abbiamo deciso di investire in Metal su macOS per ottenere un rendering più veloce e sbloccare alcune possibilità per il futuro.

Dal punto di vista dell'attuazione, non è stato affatto difficile. La maggior parte delle API è esattamente la stessa; a parte la gestione delle finestre, l'unica area che necessitava di importanti modifiche era l'allocazione della memoria. Sui dispositivi mobili c'è uno spazio di memoria condiviso per buffer e texture mentre su desktop l'API presuppone una GPU dedicata con una propria memoria video.

C'è un modo rapido per aggirare questo problema utilizzando le risorse gestite, in cui il runtime Metal si occupa di copiare i dati per te. Questo è il modo in cui abbiamo distribuito la nostra prima versione, ma poi abbiamo rielaborato l'implementazione per copiare in modo più esplicito i dati delle risorse utilizzando buffer di memoria per ridurre al minimo l'overhead della memoria di sistema.

La più grande differenza tra macOS e iOS era la stabilità. Su iOS avevamo a che fare con un unico fornitore di driver su un'architettura, mentre su macOS dovevamo supportare tutti e tre i fornitori (Intel, AMD, NVidia). Inoltre, su iOS, noi – fortunatamente! – ignorato la *prima* versione di iOS in cui era disponibile Metal, iOS 8, e su macOS questo non era pratico in quanto avremmo avuto troppo pochi utenti per usare Metal in quel momento. A causa della combinazione di questi problemi, abbiamo riscontrato molti più problemi di driver nelle aree relativamente innocue e relativamente oscure dell'API su macOS.

Supportiamo ancora tutte le versioni di macOS Metal (10.11+), anche se abbiamo iniziato a rimuovere il supporto e passare al backend OpenGL legacy per alcune versioni con bug noti del compilatore shader che sono difficili da aggirare, ad esempio su 10.11, ora abbiamo bisogno di macOS 10.11.6 affinché il metallo funzioni.

I vantaggi in termini di prestazioni sono stati in linea con le nostre aspettative; in termini di quota di mercato, ora siamo circa il 25% di OpenGL e circa il 75% di utenti Metal sulla piattaforma macOS, il che è una divisione piuttosto sana. Ciò significa che a un certo punto in futuro potrebbe essere conveniente per noi smettere di supportare OpenGL desktop, poiché nessun'altra piattaforma che supportiamo lo utilizza, il che è ottimo in termini di poterci concentrare su API più facili da supportare e ottenere buone prestazioni con esso.

Iterare su prestazioni e consumo di memoria

Storicamente siamo stati piuttosto prudenti con le funzionalità dell'API grafica che utilizziamo e Metal non fa eccezione. Ci sono molti grandi aggiornamenti delle funzionalità che Metal ha acquisito nel corso degli anni, tra cui API di allocazione delle risorse migliorate con heap espliciti, tile shader con Metal 2, buffer di argomenti e lato GPU per la generazione di comandi, ecc.

La maggior parte delle volte non utilizziamo nessuna delle nuove funzionalità. Le prestazioni finora sono state ragionevoli e vorremmo concentrarci sui miglioramenti che si applicano su tutta la linea, quindi qualcosa come i tile shader, che richiedono l'implementazione di un supporto molto speciale durante il rendering ed è accessibile solo su hardware più recente, è meno interessante.

Detto questo, stiamo dedicando un po' di tempo a mettere a punto diverse parti del backend per funzionare *più velocemente*, utilizzando download di texture completamente asincroni per ridurre la balbuzie sui carichi di livello, il che è stato completamente indolore, eseguendo le suddette ottimizzazioni della memoria su macOS, ottimizzando la diffusione della CPU in vari punti del back-end riducendo le perdite di cache, ecc. e, una delle uniche funzionalità più recenti per cui abbiamo un supporto esplicito, utilizzando l'archiviazione delle texture senza memoria quando disponibile per ridurre significativamente la memoria richiesta per il nostro nuovo sistema shadow.

Il futuro

Nel complesso, il fatto che non abbiamo dedicato troppo tempo ai miglioramenti di Metal è in realtà una buona cosa: il codice che è stato scritto 3 anni fa funziona sostanzialmente ed è veloce e stabile, il che è un ottimo segno di un'API matura. Il port to metal è stato un ottimo investimento, visto il tempo impiegato e i continui benefici che porta a noi e ai nostri utenti.

Stiamo costantemente rivalutando l'equilibrio tra la quantità di lavoro che svolgiamo per le diverse API: è molto probabile che dovremo scavare più a fondo nelle parti più moderne dell'API Metal per alcuni dei futuri progetti di rendering; se ciò accade, saremo sicuri di scrivere un altro articolo a riguardo!


  1. Sì, ok, e forse una settimana per correggere alcuni bug scoperti durante i test ↩
  2. Le cifre corrispondono a 128 KB di dati aggiornati per frame (due regioni 32x16x32 RGBA8) su A10 ↩