Leistungsoptimierungen (Direct3D 9)

Jeder Entwickler, der Echtzeitanwendungen erstellt, die 3D-Grafiken verwenden, ist um die Leistungsoptimierung besorgt. Dieser Abschnitt enthält Richtlinien zum Erzielen der besten Leistung aus Ihrem Code.

Allgemeine Leistungstipps

  • Löschen Sie nur, wenn Sie dies müssen.
  • Minimieren Sie Zustandsänderungen, und gruppierung Sie die verbleibenden Zustandsänderungen.
  • Verwenden Sie, falls möglich, kleinere Texturen.
  • Zeichnen Sie Objekte in Ihrer Szene von vorne nach hinten.
  • Verwenden Sie Dreiecksstreifen anstelle von Listen und Lüftern. Um eine optimale Vertexcacheleistung zu erzielen, ordnen Sie Strips so an, dass Dreieckspunkte früher als später wiederverwendet werden.
  • Herabsetzen Von Spezialeffekten, die einen überproportionalen Anteil an Systemressourcen erfordern.
  • Testen Sie ständig die Leistung Ihrer Anwendung.
  • Minimieren Sie Vertexpufferschalter.
  • Verwenden Sie nach Möglichkeit statische Vertexpuffer.
  • Verwenden Sie einen großen statischen Scheitelpunktpuffer pro FVF für statische Objekte und nicht einen pro Objekt.
  • Wenn Ihre Anwendung zufälligen Zugriff auf den Vertexpuffer im AGP-Arbeitsspeicher benötigt, wählen Sie eine Vertexformatgröße aus, die ein Vielfaches von 32 Bytes ist. Wählen Sie andernfalls das kleinste geeignete Format aus.
  • Zeichnen sie mit indizierten Grundtypen. Dies kann eine effizientere Vertex-Zwischenspeicherung innerhalb der Hardware ermöglichen.
  • Wenn das Tiefenpufferformat einen Schablonenkanal enthält, löschen Sie immer die Tiefen- und Schablonenkanäle gleichzeitig.
  • Kombinieren Sie nach Möglichkeit die Shaderanweisung und die Datenausgabe. Beispiel:
    // 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 
    

Datenbanken und Culling

Der Aufbau einer zuverlässigen Datenbank der Objekte in Ihrer Welt ist der Schlüssel zu einer hervorragenden Leistung in Direct3D. Dies ist wichtiger als Verbesserungen bei der Rasterung oder Hardware.

Sie sollten die niedrigste Polygonanzahl beibehalten, die Sie möglicherweise verwalten können. Entwerfen Sie eine niedrige Polygonanzahl, indem Sie von Anfang an Modelle mit niedrigen Polygonen erstellen. Fügen Sie Polygone hinzu, wenn Sie dies ohne Leistungseinbußen später im Entwicklungsprozess tun können. Denken Sie daran, dass die schnellsten Polygone diejenigen sind, die Sie nicht zeichnen.

Batching Primitives

Um die beste Renderingleistung während der Ausführung zu erzielen, versuchen Sie, mit Primitiven in Batches zu arbeiten und die Anzahl der Renderzustandsänderungen so gering wie möglich zu halten. Wenn Sie beispielsweise über ein Objekt mit zwei Texturen verfügen, gruppieren Sie die Dreiecke, die die erste Textur verwenden, und folgen Sie ihnen mit dem erforderlichen Renderzustand, um die Textur zu ändern. Gruppiert dann alle Dreiecke, die die zweite Textur verwenden. Die einfachste Hardwareunterstützung für Direct3D wird mit Batches von Renderzuständen und Batches von Primitiven über die Hardware abstraction Layer (HAL) aufgerufen. Je effektiver die Anweisungen batchiert werden, desto weniger HAL-Aufrufe werden während der Ausführung ausgeführt.

Lichttipps

Da beleuchtungsbasierte Kosten pro Vertex für jeden gerenderten Frame erhöhen, können Sie die Leistung erheblich verbessern, indem Sie sorgfältig darauf achten, wie Sie sie in Ihrer Anwendung verwenden. Die meisten der folgenden Tipps leiten sich von der Maxime ab: "Der schnellste Code ist Code, der nie aufgerufen wird."

  • Verwenden Sie so wenige Lichtquellen wie möglich. Um die Gesamtbeleuchtung zu erhöhen, verwenden Sie beispielsweise das Umgebungslicht, anstatt eine neue Lichtquelle hinzuzufügen.
  • Richtungsleuchten sind effizienter als Punktleuchten oder Scheinwerfer. Bei Richtungsleuchten ist die Richtung zum Licht festgelegt und muss nicht pro Scheitelpunkt berechnet werden.
  • Spotlights können effizienter sein als Punktleuchten, da der Bereich außerhalb des Lichtkegels schnell berechnet wird. Ob Scheinwerfer effizienter sind oder nicht, hängt davon ab, wie viel Ihrer Szene vom Scheinwerfer beleuchtet wird.
  • Verwenden Sie den Bereichsparameter, um Ihre Leuchten nur auf die Teile der Szene zu beschränken, die Sie ausleuchten müssen. Alle Lichttypen verlassen relativ früh, wenn sie außerhalb des Bereichs liegen.
  • Specular Highlights fast doppelt so viel wie die Kosten eines Lichts. Verwenden Sie sie nur, wenn Sie dies müssen. Legen Sie den D3DRS_SPECULARENABLE Renderzustand nach Möglichkeit auf 0 (Standardwert) fest. Beim Definieren von Materialien müssen Sie den Wert für die spekuläre Leistung auf 0 festlegen, um die spiegelförmigen Hervorhebungen für dieses Material zu deaktivieren. Das Festlegen der spiegelförmigen Farbe auf 0,0,0 reicht nicht aus.

Texturgröße

Die Texturzuordnungsleistung hängt stark von der Geschwindigkeit des Arbeitsspeichers ab. Es gibt eine Reihe von Möglichkeiten, die Cacheleistung der Texturen Ihrer Anwendung zu maximieren.

  • Halten Sie die Texturen klein. Je kleiner die Texturen sind, desto größer ist die Wahrscheinlichkeit, dass sie im sekundären Cache der Standard CPU verwaltet werden.
  • Ändern Sie die Texturen nicht pro primitiver Basis. Versuchen Sie, Polygone in der Reihenfolge der verwendeten Texturen gruppiert zu halten.
  • Verwenden Sie nach Möglichkeit quadratische Texturen. Texturen, deren Abmessungen 256 x 256 sind, sind die schnellsten. Wenn Ihre Anwendung z. B. vier 128x128-Texturen verwendet, stellen Sie sicher, dass sie dieselbe Palette verwenden, und platzieren Sie sie alle in einer Textur von 256 x 256. Dieses Verfahren reduziert auch den Umfang des Texturaustauschs. Natürlich sollten Sie keine 256x256-Texturen verwenden, es sei denn, Ihre Anwendung erfordert so viel Texturierung, da, wie bereits erwähnt, Texturen so klein wie möglich gehalten werden sollten.

Matrixtransformationen

Direct3D verwendet die von Ihnen festgelegten Welt- und Ansichtsmatrizen, um mehrere interne Datenstrukturen zu konfigurieren. Jedes Mal, wenn Sie eine neue Welt- oder Sichtmatrix festlegen, berechnet das System die zugehörigen internen Strukturen neu. Das häufige Festlegen dieser Matrizen – z. B. tausende Male pro Frame – ist rechnerisch zeitaufwändig. Sie können die Anzahl der erforderlichen Berechnungen minimieren, indem Sie Ihre Welt- und Ansichtsmatrizen zu einer Weltsichtmatrix verketten, die Sie als Weltmatrix festlegen, und dann die Ansichtsmatrix auf die Identität festlegen. Behalten Sie zwischengespeicherte Kopien einzelner Welt- und Ansichtsmatrizen bei, damit Sie die Weltmatrix nach Bedarf ändern, verketten und zurücksetzen können. Aus Gründen der Übersichtlichkeit in dieser Dokumentation verwenden Direct3D-Beispiele diese Optimierung selten.

Verwenden dynamischer Texturen

Um herauszufinden, ob der Treiber dynamische Texturen unterstützt, überprüfen Sie das D3DCAPS2_DYNAMICTEXTURES Flag der D3DCAPS9-Struktur .

Beachten Sie beim Arbeiten mit dynamischen Texturen folgendes:

  • Sie können nicht verwaltet werden. Ihr Pool kann beispielsweise nicht D3DPOOL_MANAGED werden.
  • Dynamische Texturen können gesperrt werden, auch wenn sie in D3DPOOL_DEFAULT erstellt werden.
  • D3DLOCK_DISCARD ist ein gültiges Sperrflag für dynamische Texturen.

Es empfiehlt sich, nur eine dynamische Textur pro Format und möglicherweise pro Größe zu erstellen. Dynamische Mipmaps, Cubes und Volumes werden aufgrund des zusätzlichen Mehraufwands beim Sperren jeder Ebene nicht empfohlen. Für mipmaps ist D3DLOCK_DISCARD nur auf der obersten Ebene zulässig. Alle Ebenen werden verworfen, indem nur die oberste Ebene gesperrt wird. Dieses Verhalten ist für Volumes und Cubes identisch. Bei Cubes sind die oberste Ebene und das Gesicht 0 gesperrt.

Der folgende Pseudocode zeigt ein Beispiel für die Verwendung einer dynamischen Textur.

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();
}

Verwenden von dynamischen Scheitelpunkt- und Indexpuffern

Das Sperren eines statischen Vertexpuffers, während der Grafikprozessor den Puffer verwendet, kann zu erheblichen Leistungseinbußen führen. Der Sperraufruf muss warten, bis der Grafikprozessor scheitel- oder Indexdaten aus dem Puffer gelesen hat, bevor er zur aufrufenden Anwendung zurückkehren kann, eine erhebliche Verzögerung. Das sperren und anschließende Rendern aus einem statischen Puffer mehrmals pro Frame verhindert auch, dass der Grafikprozessor Renderingbefehle puffert, da er Befehle beenden muss, bevor der Sperrzeiger zurückgegeben wird. Ohne gepufferte Befehle bleibt der Grafikprozessor im Leerlauf, bis die Anwendung den Vertexpuffer oder Indexpuffer ausgefüllt hat und einen Renderingbefehl ausgibt.

Im Idealfall würden sich die Vertex- oder Indexdaten nie ändern, dies ist jedoch nicht immer möglich. Es gibt viele Situationen, in denen die Anwendung Vertex- oder Indizieren von Daten in jedem Frame ändern muss, vielleicht sogar mehrmals pro Frame. In diesen Situationen sollte der Vertex- oder Indexpuffer mit D3DUSAGE_DYNAMIC erstellt werden. Dieses Verwendungsflag bewirkt, dass Direct3D für häufige Sperrvorgänge optimiert wird. D3DUSAGE_DYNAMIC ist nur nützlich, wenn der Puffer häufig gesperrt wird. Daten, die konstant bleiben, sollten in einem statischen Vertex- oder Indexpuffer platziert werden.

Um bei verwendung dynamischer Vertexpuffer eine Leistungsverbesserung zu erhalten, muss die Anwendung IDirect3DVertexBuffer9::Lock oder IDirect3DIndexBuffer9::Lock mit den entsprechenden Flags aufrufen. D3DLOCK_DISCARD gibt an, dass die Anwendung die alten Vertex- oder Indexdaten nicht im Puffer speichern muss. Wenn der Grafikprozessor weiterhin den Puffer verwendet, wenn die Sperre mit D3DLOCK_DISCARD aufgerufen wird, wird anstelle der alten Pufferdaten ein Zeiger auf einen neuen Speicherbereich zurückgegeben. Dadurch kann der Grafikprozessor weiterhin die alten Daten verwenden, während die Anwendung Daten im neuen Puffer platziert. In der Anwendung ist keine zusätzliche Speicherverwaltung erforderlich. der alte Puffer wird automatisch wiederverwendet oder zerstört, wenn der Grafikprozessor damit fertig ist. Beachten Sie, dass durch das Sperren eines Puffers mit D3DLOCK_DISCARD immer der gesamte Puffer verworfen wird, da die Angabe eines Offsetfelds ungleich null oder einer begrenzten Größe keine Informationen in entsperrten Bereichen des Puffers beibehalten.

Es gibt Fälle, in denen die Datenmenge, die die Anwendung pro Sperre speichern muss, klein ist, z. B. das Hinzufügen von vier Scheitelpunkten zum Rendern eines Sprites. D3DLOCK_NOOVERWRITE gibt an, dass die Anwendung keine Daten überschreibt, die bereits im dynamischen Puffer verwendet werden. Der Sperraufruf gibt einen Zeiger auf die alten Daten zurück, sodass die Anwendung neue Daten in nicht verwendeten Bereichen des Scheitelpunkts oder Indexpuffers hinzufügen kann. Die Anwendung sollte keine Scheitelpunkte oder Indizes ändern, die in einem Zeichnungsvorgang verwendet werden, da sie möglicherweise noch vom Grafikprozessor verwendet werden. Die Anwendung sollte dann D3DLOCK_DISCARD verwenden, nachdem der dynamische Puffer voll ist, um einen neuen Speicherbereich zu erhalten, wobei die alten Vertex- oder Indexdaten verworfen werden, nachdem der Grafikprozessor fertig ist.

Der asynchrone Abfragemechanismus ist nützlich, um zu bestimmen, ob Scheitelpunkte weiterhin vom Grafikprozessor verwendet werden. Geben Sie nach dem letzten DrawPrimitive-Aufruf, der die Scheitelpunkte verwendet, eine Abfrage vom Typ D3DQUERYTYPE_EVENT aus. Die Scheitelpunkte werden nicht mehr verwendet, wenn IDirect3DQuery9::GetData S_OK zurückgibt. Durch das Sperren eines Puffers mit D3DLOCK_DISCARD oder ohne Flags wird immer sichergestellt, dass die Scheitelpunkte ordnungsgemäß mit dem Grafikprozessor synchronisiert werden. Die Verwendung der Sperre ohne Flags verursacht jedoch die zuvor beschriebene Leistungseinbuße. Andere API-Aufrufe wie IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScene und IDirect3DDevice9::P resent garantieren nicht, dass der Grafikprozessor mit Scheitelpunkten fertig ist.

Im Folgenden finden Sie Möglichkeiten, dynamische Puffer und die richtigen Sperrflags zu verwenden.

    // 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;

Verwenden von Gittern

Sie können Gitter optimieren, indem Sie Direct3D-indizierte Dreiecke anstelle von indizierten Dreiecksstreifen verwenden. Die Hardware erkennt, dass 95 Prozent der aufeinanderfolgenden Dreiecke tatsächlich Streifen bilden, und passt sich entsprechend an. Viele Treiber tun dies auch für ältere Hardware.

D3DX-Gitterobjekte können jedes Dreieck oder jedes Gesicht aufweisen, das mit einem DWORD gekennzeichnet ist, das als Attribut dieses Gesichts bezeichnet wird. Die Semantik von DWORD ist benutzerdefinierte. Sie werden von D3DX verwendet, um das Gitter in Teilmengen zu klassifizieren. Die Anwendung legt mithilfe des ID3DXMesh::LockAttributeBuffer-Aufrufs Attribute pro Gesicht fest. Die ID3DXMesh::Optimize-Methode verfügt über eine Option zum Gruppieren der Gittervertices und -gesichter in Attributen mithilfe der Option D3DXMESHOPT_ATTRSORT. Wenn dies geschehen ist, berechnet das Mesh-Objekt eine Attributtabelle, die von der Anwendung durch Aufrufen von ID3DXBaseMesh::GetAttributeTable abgerufen werden kann. Dieser Aufruf gibt 0 zurück, wenn das Gitter nicht nach Attributen sortiert ist. Es gibt keine Möglichkeit für eine Anwendung, eine Attributtabelle festzulegen, da sie von der ID3DXMesh::Optimize-Methode generiert wird. Die Attributsortierung ist datenabhängig. Wenn die Anwendung also weiß, dass ein Gitter attributsortiert ist, muss sie weiterhin ID3DXMesh::Optimize aufrufen, um die Attributtabelle zu generieren.

In den folgenden Themen werden die verschiedenen Attribute eines Gitters beschrieben.

Attribut-ID

Eine Attribut-ID ist ein Wert, der einer Attributgruppe eine Gruppe von Gesichtern zuordnet. Diese ID beschreibt, welche Teilmenge der Gesichter ID3DXBaseMesh::D rawSubset gezeichnet werden soll. Attribut-IDs werden für die Gesichter im Attributpuffer angegeben. Die tatsächlichen Werte der Attribut-IDs können alles sein, was in 32 Bits passt, aber es ist üblich, 0 bis n zu verwenden, wobei n die Anzahl der Attribute ist.

Attributpuffer

Der Attributpuffer ist ein Array von DWORDs (eins pro Gesicht), das angibt, zu welcher Attributgruppe die einzelnen Gesichter gehören. Dieser Puffer wird bei der Erstellung eines Gitters auf Null initialisiert, wird aber entweder von den Laderoutinen gefüllt oder muss vom Benutzer gefüllt werden, wenn mehrere Attribute mit id 0 gewünscht werden. Dieser Puffer enthält die Informationen, die verwendet werden, um das Gitter nach Attributen in ID3DXMesh::Optimize zu sortieren. Wenn keine Attributtabelle vorhanden ist, überprüft ID3DXBaseMesh::D rawSubset diesen Puffer, um die Gesichter des angegebenen Attributs auszuwählen, das gezeichnet werden soll.

Attributtabelle

Die Attributtabelle ist eine Struktur, die sich im Besitz des Gitters befindet und von dieser verwaltet wird. Die einzige Möglichkeit für eine Generierung besteht darin , ID3DXMesh::Optimize mit aktivierter Attributsortierung oder stärkerer Optimierung aufzurufen. Die Attributtabelle wird verwendet, um schnell einen einzelnen Draw-primitiven Aufruf von ID3DXBaseMesh::D rawSubset zu initiieren. Die einzige andere Verwendung ist, dass progressierende Gitter auch diese Struktur beibehalten, sodass es möglich ist, zu sehen, welche Gesichter und Scheitelpunkte auf der aktuellen Detailebene aktiv sind.

Leistung des Z-Puffers

Anwendungen können die Leistung steigern, wenn Z-Pufferung und Texturierung verwendet werden, indem sie sicherstellen, dass Szenen von vorne nach hinten gerendert werden. Texturierte z-gepufferte Grundtypen werden mit dem Z-Puffer auf Scanzeilenbasis vorab getestet. Wenn eine Scanlinie von einem zuvor gerenderten Polygon ausgeblendet wird, lehnt das System sie schnell und effizient ab. Die Z-Pufferung kann die Leistung verbessern, aber die Technik ist am nützlichsten, wenn eine Szene die gleichen Pixel mehr als einmal zeichnet. Dies ist schwierig, genau zu berechnen, aber Sie können oft eine genaue Näherung vornehmen. Wenn die gleichen Pixel weniger als zweimal gezeichnet werden, können Sie die beste Leistung erzielen, indem Sie die Z-Pufferung deaktivieren und die Szene von hinten nach vorne rendern.

Programmiertipps