Ottimizzazioni delle prestazioni (Direct3D 9)

Ogni sviluppatore che crea applicazioni in tempo reale che usano grafica 3D è preoccupata per l'ottimizzazione delle prestazioni. Questa sezione fornisce linee guida per ottenere le migliori prestazioni dal codice.

Suggerimenti generali sulle prestazioni

  • Cancella solo quando devi.
  • Ridurre al minimo le modifiche allo stato e raggruppare le modifiche dello stato rimanenti.
  • Usare trame più piccole, se è possibile farlo.
  • Disegna oggetti nella scena dall'inizio al retro.
  • Usare strisce triangoli anziché elenchi e fan. Per prestazioni ottimali della cache dei vertici, disporre strisce per riutilizzare i vertici del triangolo prima, anziché in un secondo momento.
  • Degrada in modo normale effetti speciali che richiedono una condivisione sproporzionata delle risorse di sistema.
  • Testare costantemente le prestazioni dell'applicazione.
  • Ridurre al minimo le opzioni del buffer dei vertici.
  • Usare i buffer dei vertici statici, se possibile.
  • Usare un buffer di vertice statico di grandi dimensioni per FVF per oggetti statici, anziché uno per oggetto.
  • Se l'applicazione richiede l'accesso casuale nel buffer dei vertici nella memoria AGP, scegliere una dimensione del formato vertice che è un multiplo di 32 byte. In caso contrario, selezionare il formato più piccolo appropriato.
  • Disegnare usando le primitive indicizzate. Ciò può consentire una memorizzazione nella cache dei vertici più efficiente all'interno dell'hardware.
  • Se il formato del buffer di profondità contiene un canale stencil, cancellare sempre la profondità e i canali stencil contemporaneamente.
  • Combinare l'istruzione shader e l'output dei dati, se possibile. Ad esempio:
    // Rather than doing a multiply and add, and then output the data with 
    //   two instructions:
    mad r2, r1, v0, c0
    mov oD0, r2
    
    // Combine both in a single instruction, because this eliminates an  
    //   additional register copy.
    mad oD0, r1, v0, c0 
    

Database e culling

La creazione di un database affidabile degli oggetti nel mondo è fondamentale per prestazioni eccellenti in Direct3D. È più importante dei miglioramenti alla rasterizzazione o all'hardware.

È consigliabile mantenere il numero più basso di poligoni che è possibile gestire. Progettare un conteggio poligono basso creando modelli a basso poligono dall'inizio. Aggiungere poligoni se è possibile farlo senza sacrificare le prestazioni più avanti nel processo di sviluppo. Ricorda, i poligoni più veloci sono quelli che non disegnare.

Batching Primitives

Per ottenere le migliori prestazioni di rendering durante l'esecuzione, provare a usare le primitive in batch e mantenere il numero di modifiche dello stato di rendering il più basso possibile. Ad esempio, se si dispone di un oggetto con due trame, raggruppare i triangoli che usano la prima trama e seguirli con lo stato di rendering necessario per modificare la trama. Raggruppare quindi tutti i triangoli che usano la seconda trama. Il supporto hardware più semplice per Direct3D viene chiamato con batch di stati di rendering e batch di primitive tramite il livello di astrazione hardware (HAL). Le istruzioni vengono eseguite in batch, meno chiamate HAL vengono eseguite durante l'esecuzione.

Suggerimenti per l'illuminazione

Poiché le luci aggiungono un costo per vertice a ogni frame di rendering, è possibile migliorare notevolmente le prestazioni facendo attenzione a come usarli nell'applicazione. La maggior parte dei suggerimenti seguenti deriva dalla massima, "il codice più veloce è il codice che non viene mai chiamato".

  • Usare il minor numero possibile di fonti di luce. Per aumentare il livello di illuminazione complessivo, ad esempio, usare la luce ambientale anziché aggiungere una nuova fonte di luce.
  • Le luci direzionali sono più efficienti rispetto alle luci del punto o ai riflettori. Per le luci direzionali, la direzione della luce è fissa e non deve essere calcolata su base per vertice.
  • I riflettori possono essere più efficienti rispetto alle luci del punto, perché l'area esterna al cono di luce viene calcolata rapidamente. Se i riflettori sono più efficienti o meno dipendono dalla quantità di luce della scena.
  • Usare il parametro range per limitare le luci solo alle parti della scena che è necessario illuminare. Tutti i tipi di luce escono abbastanza presto quando sono fuori intervallo.
  • Evidenzia quasi il doppio del costo di una luce. Usarli solo quando è necessario. Impostare lo stato di rendering D3DRS_SPECULARENABLE su 0, il valore predefinito, ogni volta che è possibile. Quando si definiscono i materiali, è necessario impostare il valore di potenza speculare su zero per disattivare le evidenziazioni speculari per tale materiale; solo impostando il colore speculare su 0.0.0 non è sufficiente.

Dimensioni trama

Le prestazioni del mapping delle trame dipendono fortemente dalla velocità della memoria. Esistono diversi modi per ottimizzare le prestazioni della cache delle trame dell'applicazione.

  • Mantieni le trame piccole. Le trame più piccole sono, la migliore probabilità che siano mantenute nella cache secondaria della CPU principale.
  • Non modificare le trame su base primitiva. Provare a mantenere i poligoni raggruppati in ordine delle trame usate.
  • Usare trame quadrate ogni volta che è possibile. Le trame le cui dimensioni sono 256x256 sono più veloci. Se l'applicazione usa quattro trame 128x128, ad esempio, provare a garantire che usino la stessa tavolozza e li inseriscano tutti in una trama di 256x256. Questa tecnica riduce anche la quantità di scambio di trame. Naturalmente, non è consigliabile usare trame 256x256 a meno che l'applicazione non richieda che gran parte del testo perché, come accennato, le trame devono essere mantenute il più piccolo possibile.

Trasformazioni con matrice

Direct3D usa le matrici di visualizzazione e del mondo impostate per configurare diverse strutture di dati interne. Ogni volta che si imposta una nuova matrice di mondo o visualizzazione, il sistema ricalcolate le strutture interne associate. L'impostazione di queste matrici spesso, ad esempio migliaia di volte per fotogramma, richiede tempo di calcolo. È possibile ridurre al minimo il numero di calcoli necessari concatenando le matrici del mondo e di visualizzazione in una matrice di visualizzazione mondiale impostata come matrice mondiale e quindi impostando la matrice di visualizzazione sull'identità. Mantenere memorizzate nella cache copie di singoli mondi e matrici di visualizzazione in modo da poter modificare, concatenare e reimpostare la matrice mondiale in base alle esigenze. Per maggiore chiarezza in questa documentazione, gli esempi Direct3D usano raramente questa ottimizzazione.

Uso di trame dinamiche

Per scoprire se il driver supporta trame dinamiche, controllare il flag D3DCAPS2_DYNAMICTEXTURES della struttura D3DCAPS9 .

Tenere presente quanto segue quando si lavora con trame dinamiche.

  • Non possono essere gestiti. Ad esempio, il pool non può essere D3DPOOL_MANAGED.
  • Le trame dinamiche possono essere bloccate, anche se vengono create in D3DPOOL_DEFAULT.
  • D3DLOCK_DISCARD è un flag di blocco valido per trame dinamiche.

È consigliabile creare una sola trama dinamica per formato e possibilmente per dimensione. Le mipmap dinamiche, i cubi e i volumi non sono consigliati a causa del sovraccarico aggiuntivo nel blocco di ogni livello. Per mipmap, D3DLOCK_DISCARD è consentito solo al livello superiore. Tutti i livelli vengono rimossi bloccando solo il livello superiore. Questo comportamento è lo stesso per volumi e cubi. Per i cubi, il livello superiore e il viso 0 sono bloccati.

Lo pseudococo seguente mostra un esempio di uso di una trama dinamica.

DrawProceduralTexture(pTex)
{
    // pTex should not be very small because overhead of 
    //   calling driver every D3DLOCK_DISCARD will not 
    //   justify the performance gain. Experimentation is encouraged.
    pTex->Lock(D3DLOCK_DISCARD);
    <Overwrite *entire* texture>
    pTex->Unlock();
    pDev->SetTexture();
    pDev->DrawPrimitive();
}

Uso di buffer di vertice dinamici e buffer di indice

Bloccando un buffer dei vertici statici mentre il processore grafico usa il buffer può avere una significativa penalità sulle prestazioni. La chiamata di blocco deve attendere fino al termine della lettura dei dati del vertice o dell'indice dal buffer prima di poter tornare all'applicazione chiamante, un ritardo significativo. Bloccare e quindi eseguire il rendering da un buffer statico più volte per fotogramma impedisce anche al processore grafico di eseguire il buffering dei comandi, poiché deve completare i comandi prima di restituire il puntatore di blocco. Senza comandi memorizzati nel buffer, il processore di grafica rimane inattiva fino al termine del riempimento del buffer dei vertici o del buffer di indice e non esegue un comando di rendering.

Idealmente, i dati dei vertici o degli indici non cambiano mai, ma non è sempre possibile. Esistono molte situazioni in cui l'applicazione deve modificare i dati dei vertici o indicizzare ogni frame, ad esempio più volte per ogni fotogramma. Per queste situazioni, è necessario creare il vertex buffer o index buffer con D3DUSAGE_DYNAMIC. Questo flag di utilizzo causa l'ottimizzazione di Direct3D per le operazioni di blocco frequenti. D3DUSAGE_DYNAMIC è utile solo quando il buffer è bloccato di frequente; i dati che rimangono costanti devono essere inseriti in un vertex statico o in un buffer di indice.

Per ottenere un miglioramento delle prestazioni quando si usano buffer dei vertici dinamici, l'applicazione deve chiamare IDirect3DVertexBuffer9::Lock o IDirect3DIndexBuffer9::Lock con i flag appropriati. D3DLOCK_DISCARD indica che l'applicazione non deve mantenere i dati del vertice o dell'indice precedenti nel buffer. Se il processore di grafica usa ancora il buffer quando viene chiamato il blocco con D3DLOCK_DISCARD, viene restituito un puntatore a una nuova area di memoria anziché i dati del buffer precedenti. Ciò consente al processore di grafica di continuare a usare i dati precedenti mentre l'applicazione inserisce i dati nel nuovo buffer. Nell'applicazione non è necessaria alcuna gestione aggiuntiva della memoria; il buffer precedente viene riutilizzato o eliminato automaticamente al termine del processore grafico. Si noti che il blocco di un buffer con D3DLOCK_DISCARD rimuove sempre l'intero buffer, specificando un offset diverso da zero o un campo di dimensioni limitate non mantiene le informazioni nelle aree sbloccate del buffer.

In alcuni casi la quantità di dati che l'applicazione deve archiviare per ogni blocco è piccola, ad esempio l'aggiunta di quattro vertici per eseguire il rendering di uno sprite. D3DLOCK_NOOVERWRITE indica che l'applicazione non sovrascriverà i dati già in uso nel buffer dinamico. La chiamata di blocco restituirà un puntatore ai dati precedenti, consentendo all'applicazione di aggiungere nuovi dati nelle aree inutilizzate del vertex o del buffer di indice. L'applicazione non deve modificare vertici o indici usati in un'operazione di disegno perché potrebbero essere ancora in uso dal processore di grafica. L'applicazione deve quindi usare D3DLOCK_DISCARD dopo che il buffer dinamico è pieno per ricevere una nuova area di memoria, rimuovendo i dati del vertice o dell'indice precedenti al termine del processore grafico.

Il meccanismo di query asincrona è utile per determinare se i vertici sono ancora in uso dal processore di grafica. Eseguire una query di tipo D3DQUERYTYPE_EVENT dopo l'ultima chiamata DrawPrimitive che usa i vertici. I vertici non sono più in uso quando IDirect3DQuery9::GetData restituisce S_OK. Il blocco di un buffer con D3DLOCK_DISCARD o nessun flag garantisce sempre che i vertici siano sincronizzati correttamente con il processore di grafica, tuttavia l'uso di blocchi senza flag comporterà la riduzione delle prestazioni descritta in precedenza. Altre chiamate API, ad esempio IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScene e IDirect3DDevice9::P resent non garantiscono che il processore grafico venga completato usando i vertici.

Di seguito sono riportati i modi per usare buffer dinamici e i flag di blocco appropriati.

    // USAGE STYLE 1
    // Discard the entire vertex buffer and refill with thousands of vertices.
    // Might contain multiple objects and/or require multiple DrawPrimitive 
    //   calls separated by state changes, etc.
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // Discard and refill the used portion of the vertex buffer.
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)
    // USAGE STYLE 2
    // Reusing one vertex buffer for multiple objects
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // No overwrite will be used if the vertices can fit into 
    //   the space remaining in the vertex buffer.
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
    
    // Check to see if the entire vertex buffer has been used up yet.
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // No space remains. Start over from the beginning 
        //   of the vertex buffer.
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData, 
               &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)
 
    // Advance to the next position in the vertex buffer.
    m_nNextVertexData += nSizeOfData;

Uso delle mesh

È possibile ottimizzare le mesh usando triangoli indicizzati Direct3D anziché strisce di triangoli indicizzati. L'hardware scoprirà che il 95% dei triangoli successivi forma effettivamente strisce e regola di conseguenza. Molti driver eseguono questa operazione anche per l'hardware meno recente.

Gli oggetti mesh D3DX possono avere ogni triangolo, o viso, contrassegnato con un DWORD, denominato attributo di tale viso. La semantica della DWORD è definita dall'utente. Vengono usati da D3DX per classificare la mesh in subset. L'applicazione imposta gli attributi per viso usando la chiamata ID3DXMesh::LockAttributeBuffer . Il metodo ID3DXMesh::Optimize offre un'opzione per raggruppare i vertici e i visi della mesh sugli attributi usando l'opzione D3DXMESHOPT_ATTRSORT. Al termine, l'oggetto mesh calcola una tabella di attributi che può essere ottenuta dall'applicazione chiamando ID3DXBaseMesh::GetAttributeTable. Questa chiamata restituisce 0 se la mesh non è ordinata in base agli attributi. Non è possibile impostare una tabella di attributi da parte di un'applicazione perché viene generata dal metodo ID3DXMesh::Optimize . L'ordinamento degli attributi è sensibile ai dati, quindi se l'applicazione sa che una mesh è ordinata, deve comunque chiamare ID3DXMesh::Optimize per generare la tabella degli attributi.

Negli argomenti seguenti vengono descritti i diversi attributi di una mesh.

ID attributo

Un ID attributo è un valore che associa un gruppo di visi a un gruppo di attributi. Questo ID descrive quale subset di visi ID3DXBaseMesh::D rawSubset deve disegnare. Gli ID attributo vengono specificati per i visi nel buffer degli attributi. I valori effettivi degli ID attributo possono essere qualsiasi elemento che si adatti a 32 bit, ma è comune usare 0 a n dove n è il numero di attributi.

Buffer degli attributi

Il buffer degli attributi è una matrice di DWORD (una per viso) che specifica il gruppo di attributi in cui appartiene ogni viso. Questo buffer viene inizializzato su zero durante la creazione di una mesh, ma viene riempito dalle routine di caricamento o deve essere riempito dall'utente se è desiderato più di un attributo con ID 0. Questo buffer contiene le informazioni usate per ordinare la mesh in base agli attributi in ID3DXMesh::Optimize. Se non è presente alcuna tabella di attributi, ID3DXBaseMesh::D rawSubset analizza questo buffer per selezionare i visi dell'attributo specificato da disegnare.

Tabella attributi

La tabella degli attributi è una struttura di proprietà e gestita dalla mesh. L'unico modo per generarne uno consiste nel chiamare ID3DXMesh::Optimize con l'ordinamento degli attributi o l'ottimizzazione più efficace abilitata. La tabella degli attributi viene usata per avviare rapidamente una singola chiamata primitiva di disegno a ID3DXBaseMesh::D rawSubset. L'unico altro uso è che le mesh in corso mantengono anche questa struttura, quindi è possibile vedere quali visi e vertici sono attivi a livello di dettaglio corrente.

Prestazioni del buffer Z

Le applicazioni possono aumentare le prestazioni quando si usano il buffer z e il texturing assicurando che il rendering delle scene venga eseguito dall'inizio verso il retro. Le primitive con buffer z con trama sono precedute dal buffer z su una linea di analisi. Se una linea di analisi è nascosta da un poligono di cui è stato eseguito il rendering in precedenza, il sistema lo rifiuta in modo rapido ed efficiente. Il buffering Z può migliorare le prestazioni, ma la tecnica è più utile quando una scena disegna più volte gli stessi pixel. Questo è difficile da calcolare esattamente, ma spesso è possibile creare un'approssimazione vicina. Se gli stessi pixel vengono disegnati meno di due volte, è possibile ottenere le migliori prestazioni disattivando il buffer z e eseguendo il rendering della scena da indietro verso l'alto.

Suggerimenti per la programmazione