Condividi tramite


Ottimizzazioni delle prestazioni (Direct3D 9)

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

Suggerimenti generali sulle prestazioni

  • Cancella solo quando devi.
  • Ridurre al minimo le modifiche dello stato e raggruppare le modifiche dello stato rimanenti.
  • Se possibile, usare trame più piccole.
  • Disegnare oggetti nella scena dall'inizio alla parte posteriore.
  • Usare strisce di triangolo invece di elenchi e fan. Per ottenere prestazioni ottimali della cache dei vertici, disporre le strip per riutilizzare i vertici dei triangoli prima, anziché in un secondo momento.
  • Degrada normalmente 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 vertex buffer statici laddove possibile.
  • Usare un buffer di vertici statici di grandi dimensioni per FVF per gli oggetti statici, anziché uno per oggetto.
  • Se l'applicazione richiede l'accesso casuale al buffer dei vertici nella memoria AGP, scegliere una dimensione di formato vertice composta da più di 32 byte. In caso contrario, selezionare il formato più piccolo appropriato.
  • Disegnare utilizzando 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 i canali depth e stencil contemporaneamente.
  • Combinare l'istruzione shader e l'output dei dati laddove possibile. Per 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 ottenere prestazioni eccellenti in Direct3D. È più importante dei miglioramenti apportati alla rasterizzazione o all'hardware.

È consigliabile mantenere il numero di poligoni più basso che è possibile gestire. Progettare un numero di poligoni basso creando modelli a basso poligono fin 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.

Batch di primitive

Per ottenere prestazioni di rendering ottimali durante l'esecuzione, provare a usare 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 raggruppate in batch, mentre durante l'esecuzione vengono eseguite meno chiamate HAL.

Suggerimenti per l'illuminazione

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

  • Utilizzare 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 sorgente luminosa.
  • Le luci direzionali sono più efficienti rispetto alle luci punto o ai riflettori. Per le luci direzionali, la direzione della luce è fissa e non deve essere calcolata per ogni vertice.
  • I riflettori possono essere più efficienti delle luci punto, perché l'area esterna al cono di luce viene calcolata rapidamente. Se i riflettori sono più efficienti o meno dipende dalla quantità di luce della scena illuminata dai riflettori.
  • Usa il parametro range per limitare le luci solo alle parti della scena che devi illuminare. Tutti i tipi di luce escono abbastanza presto quando sono fuori intervallo.
  • Speculare 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, quando possibile. Quando si definiscono i materiali, è necessario impostare il valore di potenza speculare su zero per disattivare le evidenziazioni speculari per tale materiale; basta impostare il colore speculare su 0,0,0 non è sufficiente.

Dimensioni trama

Le prestazioni di mapping delle trame dipendono in larga misura dalla velocità della memoria. Esistono diversi modi per ottimizzare le prestazioni della cache delle trame dell'applicazione.

  • Mantenere le trame piccole. Più piccole sono le trame, maggiore è la probabilità che vengano mantenute nella cache secondaria della CPU principale.
  • Non modificare le trame per ogni primitiva. Provare a mantenere i poligoni raggruppati in ordine delle trame usate.
  • Usa trame quadrate quando possibile. Le trame le cui dimensioni sono 256x256 sono le più veloci. Se l'applicazione usa quattro trame 128x128, ad esempio, provare a garantire che usino la stessa tavolozza e le inseriscano tutte 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 molto testo perché, come accennato, le trame devono essere mantenute il più piccolo possibile.

Trasformazioni matrice

Direct3D usa le matrici di mondo e di visualizzazione impostate per configurare diverse strutture di dati interne. Ogni volta che si imposta una nuova matrice globale o di visualizzazione, il sistema ricalcola le strutture interne associate. L'impostazione frequente di queste matrici, ad esempio migliaia di volte per fotogramma, richiede molto tempo di calcolo. È possibile ridurre al minimo il numero di calcoli necessari concatenando le matrici del mondo e visualizzando le matrici in una matrice di visualizzazione globale impostata come matrice globale 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 globale 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.

Quando si lavora con trame dinamiche, tenere presente quanto segue.

  • 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 le trame dinamiche.

È consigliabile creare una sola trama dinamica per formato e possibilmente per dimensione. I mipmap dinamici, i cubi e i volumi non sono consigliati a causa del sovraccarico aggiuntivo durante il blocco di ogni livello. Per le mipmap, D3DLOCK_DISCARD è consentito solo al livello superiore. Tutti i livelli vengono eliminati 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 pseudocodice seguente illustra un esempio di utilizzo 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 vertex dinamici e buffer di indice

Il blocco di un buffer dei vertici statici mentre il processore grafico usa il buffer può avere una riduzione significativa delle prestazioni. La chiamata di blocco deve attendere fino al termine della lettura dei dati dei vertici o dell'indice dal buffer prima che possa tornare all'applicazione chiamante, un ritardo significativo. Il blocco e quindi il rendering da un buffer statico più volte per fotogramma impedisce anche al processore grafico di memorizzare nel buffer i comandi di rendering, 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 genera un comando di rendering.

Idealmente, i dati del vertice o dell'indice non cambieranno mai, ma questo non è sempre possibile. Esistono molte situazioni in cui l'applicazione deve modificare i dati dei vertici o degli indici ogni frame, ad esempio più volte per ogni fotogramma. Per queste situazioni, il vertex o il buffer di indice deve essere creato 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 frequentemente; i dati che rimangono costanti devono essere inseriti in un vertice 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 grafico 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 grafico di continuare a usare i dati precedenti mentre l'applicazione inserisce i dati nel nuovo buffer. Non è necessaria alcuna gestione aggiuntiva della memoria nell'applicazione; il buffer precedente viene riutilizzato o distrutto automaticamente al termine del processore grafico. Si noti che il blocco di un buffer con D3DLOCK_DISCARD elimina 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 in 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 grafico. 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 asincrono è utile per determinare se i vertici sono ancora in uso dal processore grafico. 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 garantirà sempre che i vertici vengano sincronizzati correttamente con il processore grafico, tuttavia l'uso di blocco senza flag comporterà la penalità delle prestazioni descritta in precedenza. Altre chiamate API, ad esempio IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScenee IDirect3DDevice9::P resent non garantiscono che il processore grafico venga completato usando 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 lo fanno 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 necessario 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 si desidera più attributi 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 un'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 uso consiste nel fatto che le mesh in corso mantengono anche questa struttura, quindi è possibile vedere quali visi e vertici sono attivi al livello di dettaglio corrente.

Prestazioni del buffer Z

Le applicazioni possono migliorare le prestazioni quando si usano il buffer z e la formattazione del testo assicurandosi che il rendering delle scene venga eseguito dall'inizio verso il retro. Le primitive con buffer z con trama vengono pretestate rispetto al buffer z su una linea di analisi. Se una linea di analisi è nascosta da un poligono sottoposto a rendering in precedenza, il sistema lo rifiuta in modo rapido ed efficiente. Il buffer 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 fare un'approssimazione stretta. Se gli stessi pixel vengono disegnati meno di due volte, è possibile ottenere prestazioni ottimali disattivando il buffer z ed eseguendo il rendering della scena dall'inizio all'altro.

suggerimenti per la programmazione