Introduzione alle DirectX12 4110 Visite) DirectX 12
Benvenuti al primo tutorial per le Direct3D12, la componente grafica delle DirectX12 in cui cercherò di illustrare le principali differenze con la precedente versione, le DirectX11
Dopo 6 anni dall’uscita delle DirectX11 si presenta la successiva evoluzione delle librerie grafiche dedicate a giochi ed applicazioni: le DirectX 12 (o per meglio dire Direct3D12, essendo la componente grafica l’unica ad essere stata evoluta).
Per la prima volta nella storia di queste API l’uscita di una nuova versione DirectX non corrisponde alla necessità di nuove schede video e questo è un primo indizio di ciò che è in realtà Direct3D12. Il nuovo set di librerie non rinnovano le funzionalità offerte ma come queste vengono utilizzate.
Un’applicazione grafica può essere vista come un set di comandi che la CPU comunica alla GPU (la scheda grafica). Un gioco che gira a 60 fotogrammi al secondo richiede che in 1/60 di secondo il nostro programma esegua tutte le istruzioni della logica di gioco e le istruzioni grafiche.
Un’applicazione 3D si compone di 2 fasi:
-
Preparazione dei buffer:
Il caricamento di tutti le informazioni della scena all’interno di buffer di memoria (quello che succede durante le schermate di caricamento nei giochi). -
Rendering:
Un ciclo da eseguire continuamente che, per ogni oggetto, indica i buffer da utilizzare e ne ordina il Draw (disegno).
In un gioco la preparazione dei buffer è ciò che accade durante il caricamento dei livelli. Queste contengono gli oggetti 3D (suddivisi nei Vertex Buffer e Index Buffer), le Texture (le immagini che ricoprono i modelli 3D), i Constant Buffer (tutte le informazioni di ogni oggetto come colori, posizione e proprietà varie) e gli Shader (codice che la scheda video eseguirà per trasformare le informazioni contenute nei buffer e generare il modello finale che si vedrà a video).
A parte i Constant Buffer, queste risorse non vengono quasi mai toccate in quanto possono richiedere diversi secondi per essere create (a volte anche minuti se occorre caricare e preparare tanti dati). Fortunatamente questo può essere fatto una tantum nella maggior parte delle situazioni.
In questa schema si capisce subito che è il Rendering il punto più importante, un ciclo continuo che dura per tutta la durate del gioco/applicazione e dove vengono eseguite tutte le istruzioni di disegno. Per ogni oggetto ci sono un certo numero di istruzioni da eseguire e spesso è necessario preparare più Rendering da fondere insieme per realizzare molti degli effetti presenti nei giochi moderni. Se il nostro obiettivo sono i 60 fotogrammi al secondo, abbiamo 16 millisecondi per fare tutto.
Molte di queste istruzioni sono l’impostazione dei vari buffer. DirectX ha un certo numero di slot di memoria in cui inserire i vari buffer. Quando tutti i buffer sono inseriti nei giusti slot si esegue l’operazione di Draw e l’immagine compare sul backbuffer, una sorta di bozza della scena. Per ogni oggetto si dovranno quindi cambiare i buffer negli slot (alternare i vertex buffer per cambiare modello, o cambiare shader per avere un differente effetto) ed eseguire di nuovo il Draw finché il backbuffer non conterrà l’immagine finale da mandare al monitor. Tutto questo in pochi millisecondi.
Le DirectX, fino alla versione 11, ha un limite: ogni singola istruzione viaggia dalla CPU alla GPU restituendone poi alla CPU l’esito. In una scena con centinaia di oggetti inizia a diventare un costo non trascurare che obbliga chi sviluppa a decine di strategie per limitare il numero di chiamate. Senza contare che durante l’attesa tra una chiamata e l’altra la scheda video non può fare nulla per avvantaggiarsi il lavoro non sapendo cosa deve fare.
Direct3D12 cambia definitivamente questo approccio tramite le Command List che, come dice il nome, sono un elenco di istruzioni che noi imposteremo e che Direct3D eseguirà quando noi ne avremo bisogno.
Queste istruzioni risiederanno nella memoria della scheda video e, una volta pronte, verranno eseguite in un’unica chiamata praticamente azzerando il costo delle singole.
L’utilità delle Command List però non si limita a questo, è infatti possibile:
-
Avere più liste e conservarle per tutto il tempo necessario in modo da non doverle preparare ad ogni fotogramma
-
Preparare le liste in un thread a parte sfruttando i core della CPU
-
Lanciare più CommandList in parallelo sulla nostra GPU
In questo modo si può lavorare veramente in MultiThreading sia noi, che possiamo con facilità suddividere il nostro rendering su più Thread, sia lo stesso sistema che può parallelizzare ogni processo ed addirittura avvantaggiarsi un rendering mentre sta finendo il precedente.
In questa immagine (presa dalla presentazione Microsoft) viene mostrata il differente comportamento su un sistema con CPU a 4 Core.
Nel caso delle Direct3D11 (le 4 barre in alto) il processore riesce a distribuire solo la logica dell’applicazione (il lavoro che fa la CPU per gestire gli oggetti da renderizzare) mentre la maggior parte del lavoro grafico viene svolto solo dal primo Core.
Nel caso delle Direct3D12 (le 4 barre in basso) anche il carico relativo alla grafica è equamente distribuito oltre ad essere ridotto (notare che è sparito il lavoro del Kernel e del Kernel Model Driver).
La durata di ogni Frame (il fotogramma) viene ridotta con conseguente possibilità di creare più frame.
Heap
La pipeline grafica è rimasta la stessa delle Direct3D11, quindi gli Shader, i Buffer e tutto il resto, sono rimasti concettualmente gli stessi, con la differenza che questi step vanno gestiti nelle Command List. Per chi si avvicina solo ora al mondo DirectX consiglio di leggere i primi paragrafi del PDF De Rerum Shader scaricabile in fondo all’articolo.
Sempre nell’ottica di minimizzare lo scambio di informazioni tra la CPU e la GPU è cambiato il modo con cui vengono passate le risorse a DirectX.
Nelle Direct3D11 chiamavamo continuamente il DeviceContext (il controller delle Direct3D) per passargli continuamente i nostri Buffer, Shader e proprietà. Anche questo ora avviene a livello più basso. Nasce così il concetto di Pipeline State, un contenitore che contiene gli Shader, l’Input Assembler (la struttura dei vertici delle mesh) e tutte le proprietà di rendering. Per ogni effetto si creerà un Pipeline Stage. Le variabili di ogni shader (chiamate Registri) saranno passate anche loro in modo ottimizzato tramite gli Heap ed i Root Parameter.
Un Heap è una zona di memoria contigua che contiene i puntatori alle risorse. Ogni volta che creiamo una qualunque risorsa la aggiungiamo all’Heap e generiamo una Vista su quella risorsa che ci dà la posizione dell’oggetto all’interno dell’Heap. Alla Command List passeremo solo gli Heap e la posizione in cui si trova ogni risorsa di cui abbiamo bisogno. Quest’ultima associazione viene fatta tramite le RootDescriptor che associano ogni registro presente negli Shaders con una Texture o un Costant Buffer presenti nell’Heap (operazione ancor più performante se si utilizzano le Root Descriptor Table che raccolgono più descrittori in uno).
Se il tutto è opportunamente configurato basteranno poche operazioni per impostare il Pipeline State e le sue variabili.
Nell’immagine sopra viene mostrato come i registri associati ad ogni Shader (blocchi arancioni) vengano associate alle risorse presenti in memoria.
Utilizzare le Direct3D12
Le Direct3D12 hanno subito un notevole snellimento, cosa già iniziata dalla versione 10. Le tecnologie offerte dalle 12 sono le stesse offerte dalle 11 (salvo l’introduzione di alcune ma utilissime nuove feature) ma come avete già leggere sono state stravolte dal punto di vista dell’utilizzo. Le Direct3D12 non sono state fatte per semplificare la vita al programmatore, ma al contrario, per dargli completo controllo di ciò che avviene nella scheda video in modo da spremere il sistema fino all’ultima risorsa. In mano ad un programmatore esperto le Direct3D promettono notevoli miglioramenti in termini di performance fino al 30% su PC.
Unico limite è che le librerie sono presenti solo sui sistemi Windows 10 (PC e a breve Mobile ed Xbox One) e quindi senza un PC con questo sistema operativo non potrete utilizzarle (fortunatamente Microsoft ci permette di aggiornare il nostro vecchio Windows a questo sistema).
Per programmare Direct3D12 avete 2 scelte: C++ utilizzando le API ufficiali presenti nell’ultima SDK (qui il link) o .Net utilizzando il wrapper SharpDx, una libreria che permette di utilizzare tutti i componenti DirectX dalla versione 9 alla 12 (comprese anche le componenti audio e di input). Qui si trova il link. Scaricate l’ultimo Dev Package (attualmente in fase beta) per poter utilizzare le Direct3D12. All’interno del sorgente ufficiale è possibile trovare alcuni esempi Direct3D12 realizzati da me per contribuire all’ottimo lavoro fatto da Alexandre Mutel, autore di SharpDx che ricalcano quelli realizzati da Microsoft per le API C++.
Dato che ho intenzione di aggiungere ulteriori demo ed esempi, gli stessi sorgenti sono presenti anche nel mio repository personale (link) che faranno da base per i primissimi tutorial.
Buon inizio
- De Rerum Shader.pdf 1065 KB) Download File