Optimisations des performances (Direct3D 9)

Chaque développeur qui crée des applications en temps réel qui utilisent des graphiques 3D se préoccupe de l’optimisation des performances. Cette section fournit des instructions pour obtenir les meilleures performances de votre code.

Conseils généraux sur les performances

  • Effacez uniquement quand vous le devez.
  • Réduisez les modifications d’état et regroupez les modifications d’état restantes.
  • Utilisez des textures plus petites, si vous le pouvez.
  • Dessinez des objets dans votre scène d’un avant vers l’autre.
  • Utilisez des bandes de triangles au lieu de listes et de ventilateurs. Pour des performances optimales du cache de vertex, organisez les bandes afin de réutiliser les sommets triangles plus tôt, plutôt que plus tard.
  • Dégrader de façon appropriée les effets spéciaux qui nécessitent une part disproportionnée des ressources système.
  • Testez en permanence les performances de votre application.
  • Réduire les commutateurs de mémoire tampon de vertex.
  • Utilisez des tampons de vertex statiques lorsque cela est possible.
  • Utilisez une grande mémoire tampon de vertex statique par FVF pour les objets statiques, plutôt qu’une mémoire tampon par objet.
  • Si votre application a besoin d’un accès aléatoire dans la mémoire tampon de vertex dans la mémoire AGP, choisissez une taille de format de vertex qui est un multiple de 32 octets. Sinon, sélectionnez le plus petit format approprié.
  • Dessinez à l’aide de primitives indexées. Cela peut permettre une mise en cache de vertex plus efficace au sein du matériel.
  • Si le format de mémoire tampon de profondeur contient un canal de gabarit, effacez toujours les canaux de profondeur et de gabarit en même temps.
  • Combinez l’instruction du nuanceur et la sortie des données si possible. Par exemple :
    // 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 
    

Bases de données et élimination

La création d’une base de données fiable des objets dans votre monde est la clé d’excellentes performances dans Direct3D. Il est plus important que les améliorations apportées à la rastérisation ou au matériel.

Vous devez conserver le nombre de polygones le plus bas que vous pouvez éventuellement gérer. Concevez un nombre de polygones faible en créant des modèles à polygones bas dès le début. Ajoutez des polygones si vous pouvez le faire sans sacrifier les performances ultérieurement dans le processus de développement. N’oubliez pas que les polygones les plus rapides sont ceux que vous ne dessinez pas.

Traitement par lots de primitives

Pour obtenir les meilleures performances de rendu pendant l’exécution, essayez d’utiliser des primitives dans des lots et de maintenir le nombre de modifications d’état de rendu aussi bas que possible. Par exemple, si vous avez un objet avec deux textures, regroupez les triangles qui utilisent la première texture et suivez-les avec l’état de rendu nécessaire pour modifier la texture. Regroupez ensuite tous les triangles qui utilisent la deuxième texture. La prise en charge matérielle la plus simple pour Direct3D est appelée avec des lots d’états de rendu et des lots de primitives via la couche d’abstraction matérielle (HAL). Plus les instructions sont traitées par lot avec efficacité, moins les appels HAL sont effectués pendant l’exécution.

Conseils d’éclairage

Étant donné que les lumières ajoutent un coût par sommet à chaque image rendue, vous pouvez améliorer considérablement les performances en faisant attention à leur utilisation dans votre application. La plupart des conseils suivants dérivent de la maxime « le code le plus rapide est du code qui n’est jamais appelé ».

  • Utilisez le moins de sources de lumière possible. Pour augmenter le niveau d’éclairage global, par exemple, utilisez la lumière ambiante au lieu d’ajouter une nouvelle source de lumière.
  • Les feux directionnels sont plus efficaces que les feux de pointe ou les projecteurs. Pour les feux directionnels, la direction de la lumière est fixe et n’a pas besoin d’être calculée sur une base par sommet.
  • Les projecteurs peuvent être plus efficaces que les lumières pointées, car la zone située à l’extérieur du cône de lumière est calculée rapidement. Si les projecteurs sont plus efficaces ou non dépend de la quantité de votre scène est allumée par les projecteurs.
  • Utilisez le paramètre de plage pour limiter vos lumières aux parties de la scène que vous devez éclairer. Tous les types de lumière sortent assez tôt quand ils sont hors de portée.
  • Les mises en évidence spéculaires doublent presque le coût d’une lumière. Utilisez-les uniquement lorsque vous le devez. Définissez l’état de rendu D3DRS_SPECULARENABLE sur 0, la valeur par défaut, chaque fois que possible. Lorsque vous définissez des matériaux, vous devez définir la valeur de puissance spéculaire sur zéro pour désactiver les surbrillances spéculaires pour ce matériau ; il ne suffit pas de définir la couleur spéculaire sur 0,0,0.

Taille de texture

Les performances du mappage de texture dépendent fortement de la vitesse de la mémoire. Il existe plusieurs façons d’optimiser les performances du cache des textures de votre application.

  • Gardez les textures petites. Plus les textures sont petites, plus elles ont de chances d’être conservées dans le cache secondaire du processeur main.
  • Ne modifiez pas les textures par primitive. Essayez de conserver les polygones regroupés dans l’ordre des textures qu’ils utilisent.
  • Utilisez des textures carrées chaque fois que possible. Les textures dont les dimensions sont de 256x256 sont les plus rapides. Si votre application utilise quatre textures 128x128, par exemple, essayez de vous assurer qu’elles utilisent la même palette et de les placer toutes dans une seule texture 256x256. Cette technique réduit également la quantité d’échange de textures. Bien sûr, vous ne devez pas utiliser de textures 256x256, sauf si votre application nécessite autant de texturation, car, comme mentionné, les textures doivent être conservées aussi petites que possible.

Transformations de matrice

Direct3D utilise les matrices world et view que vous définissez pour configurer plusieurs structures de données internes. Chaque fois que vous définissez une nouvelle matrice de monde ou de vue, le système recalcule les structures internes associées. La définition fréquente de ces matrices (par exemple, des milliers de fois par image) prend du temps de calcul. Vous pouvez réduire le nombre de calculs requis en concaténant votre monde et vos matrices de vue dans une matrice de vue du monde que vous définissez comme matrice mondiale, puis en définissant la matrice d’affichage sur l’identité. Conservez des copies mises en cache de matrices de monde et d’affichage individuelles afin de pouvoir modifier, concaténer et réinitialiser la matrice mondiale en fonction des besoins. Pour plus de clarté dans cette documentation, les exemples Direct3D utilisent rarement cette optimisation.

Utilisation de textures dynamiques

Pour savoir si le pilote prend en charge les textures dynamiques, case activée l’indicateur D3DCAPS2_DYNAMICTEXTURES de la structure D3DCAPS9.

Gardez les éléments suivants à l’esprit lorsque vous travaillez avec des textures dynamiques.

  • Ils ne peuvent pas être gérés. Par exemple, leur pool ne peut pas être D3DPOOL_MANAGED.
  • Les textures dynamiques peuvent être verrouillées, même si elles sont créées dans D3DPOOL_DEFAULT.
  • D3DLOCK_DISCARD est un indicateur de verrouillage valide pour les textures dynamiques.

Il est judicieux de créer une seule texture dynamique par format et éventuellement par taille. Les mipmaps dynamiques, les cubes et les volumes ne sont pas recommandés en raison de la surcharge supplémentaire liée au verrouillage de chaque niveau. Pour les mipmaps, D3DLOCK_DISCARD est autorisé uniquement au niveau supérieur. Tous les niveaux sont ignorés en verrouillant uniquement le niveau supérieur. Ce comportement est le même pour les volumes et les cubes. Pour les cubes, le niveau supérieur et la face 0 sont verrouillés.

Le pseudocode suivant montre un exemple d’utilisation d’une texture dynamique.

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

Utilisation de vertex dynamiques et de mémoires tampons d’index

Le verrouillage d’une mémoire tampon de vertex statique pendant que le processeur graphique utilise la mémoire tampon peut entraîner une pénalité importante en matière de performances. L’appel de verrouillage doit attendre que le processeur graphique a terminé la lecture des données de vertex ou d’index à partir de la mémoire tampon avant de pouvoir revenir à l’application appelante, ce qui représente un retard important. Le verrouillage, puis le rendu à partir d’une mémoire tampon statique plusieurs fois par image empêche également le processeur graphique de mettre en mémoire tampon les commandes de rendu, car il doit terminer les commandes avant de renvoyer le pointeur de verrouillage. Sans commandes mises en mémoire tampon, le processeur graphique reste inactif jusqu’à ce que l’application a fini de remplir la mémoire tampon de vertex ou la mémoire tampon d’index et émet une commande de rendu.

Dans l’idéal, les données de vertex ou d’index ne changent jamais, mais cela n’est pas toujours possible. Il existe de nombreuses situations dans lesquelles l’application doit modifier les données de vertex ou d’index à chaque trame, peut-être même plusieurs fois par image. Pour ces situations, la mémoire tampon de vertex ou d’index doit être créée avec D3DUSAGE_DYNAMIC. Cet indicateur d’utilisation entraîne l’optimisation de Direct3D pour les opérations de verrouillage fréquentes. D3DUSAGE_DYNAMIC n’est utile que lorsque la mémoire tampon est fréquemment verrouillée ; les données qui restent constantes doivent être placées dans une mémoire tampon de vertex ou d’index statique.

Pour bénéficier d’une amélioration des performances lors de l’utilisation de tampons de vertex dynamiques, l’application doit appeler IDirect3DVertexBuffer9::Lock ou IDirect3DIndexBuffer9::Lock avec les indicateurs appropriés. D3DLOCK_DISCARD indique que l’application n’a pas besoin de conserver les anciennes données de vertex ou d’index dans la mémoire tampon. Si le processeur graphique utilise toujours la mémoire tampon lorsque le verrouillage est appelé avec D3DLOCK_DISCARD, un pointeur vers une nouvelle région de mémoire est retourné à la place des anciennes données de mémoire tampon. Cela permet au processeur graphique de continuer à utiliser les anciennes données pendant que l’application place les données dans la nouvelle mémoire tampon. Aucune gestion de mémoire supplémentaire n’est requise dans l’application ; L’ancienne mémoire tampon est réutilisée ou détruite automatiquement lorsque le processeur graphique est terminé. Notez que le verrouillage d’une mémoire tampon avec D3DLOCK_DISCARD ignore toujours la mémoire tampon entière. La spécification d’un décalage différent de zéro ou d’un champ de taille limitée ne conserve pas les informations dans les zones déverrouillées de la mémoire tampon.

Il existe des cas où la quantité de données que l’application doit stocker par verrou est faible, comme l’ajout de quatre sommets pour restituer un sprite. D3DLOCK_NOOVERWRITE indique que l’application ne remplacera pas les données déjà utilisées dans la mémoire tampon dynamique. L’appel de verrou retourne un pointeur vers les anciennes données, ce qui permet à l’application d’ajouter de nouvelles données dans les régions inutilisées de la mémoire tampon de vertex ou d’index. L’application ne doit pas modifier les sommets ou les index utilisés dans une opération de dessin, car ils peuvent toujours être utilisés par le processeur graphique. L’application doit ensuite utiliser D3DLOCK_DISCARD une fois la mémoire tampon dynamique saturée pour recevoir une nouvelle région de mémoire, en ignorant les anciennes données de vertex ou d’index une fois le processeur graphique terminé.

Le mécanisme de requête asynchrone est utile pour déterminer si les sommets sont toujours utilisés par le processeur graphique. Émettez une requête de type D3DQUERYTYPE_EVENT après le dernier appel DrawPrimitive qui utilise les sommets. Les sommets ne sont plus utilisés lorsque IDirect3DQuery9::GetData retourne S_OK. Le verrouillage d’une mémoire tampon avec D3DLOCK_DISCARD ou aucun indicateur garantit toujours que les sommets sont correctement synchronisés avec le processeur graphique. Toutefois, l’utilisation d’un verrou sans indicateur entraîne une pénalité de performances décrite précédemment. D’autres appels d’API tels que IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScene et IDirect3DDevice9::P resent ne garantissent pas que le processeur graphique est terminé à l’aide de sommets.

Vous trouverez ci-dessous des façons d’utiliser des mémoires tampons dynamiques et les indicateurs de verrouillage appropriés.

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

Utilisation de maillages

Vous pouvez optimiser les maillages à l’aide de triangles indexés Direct3D au lieu de bandes de triangle indexées. Le matériel découvrira que 95 % des triangles successifs forment en fait des bandes et s’ajustent en conséquence. De nombreux pilotes le font également pour du matériel plus ancien.

Les objets de maillage D3DX peuvent avoir chaque triangle, ou visage, marqué avec un DWORD, appelé attribut de ce visage. La sémantique du DWORD est définie par l’utilisateur. Ils sont utilisés par D3DX pour classifier le maillage en sous-ensembles. L’application définit des attributs par visage à l’aide de l’appel ID3DXMesh::LockAttributeBuffer . La méthode ID3DXMesh::Optimize permet de regrouper les sommets et les visages de maillage sur les attributs à l’aide de l’option D3DXMESHOPT_ATTRSORT. Lorsque cette opération est effectuée, l’objet de maillage calcule une table d’attributs qui peut être obtenue par l’application en appelant ID3DXBaseMesh::GetAttributeTable. Cet appel retourne 0 si le maillage n’est pas trié par attributs. Il n’existe aucun moyen pour une application de définir une table d’attributs, car elle est générée par la méthode ID3DXMesh::Optimize . Le tri d’attribut est sensible aux données. Par conséquent, si l’application sait qu’un maillage est trié, elle doit toujours appeler ID3DXMesh::Optimize pour générer la table d’attributs.

Les rubriques suivantes décrivent les différents attributs d’un maillage.

ID d’attribut

Un ID d’attribut est une valeur qui associe un groupe de visages à un groupe d’attributs. Cet ID décrit le sous-ensemble de visages ID3DXBaseMesh::D rawSubset à dessiner. Les ID d’attribut sont spécifiés pour les visages dans la mémoire tampon d’attributs. Les valeurs réelles des ID d’attribut peuvent être tout ce qui correspond à 32 bits, mais il est courant d’utiliser 0 à n, où n est le nombre d’attributs.

Mémoire tampon d’attribut

La mémoire tampon d’attributs est un tableau de DWORD (un par visage) qui spécifie le groupe d’attributs auquel appartient chaque visage. Cette mémoire tampon est initialisée à zéro lors de la création d’un maillage, mais elle est remplie par les routines de chargement ou doit être remplie par l’utilisateur si plusieurs attributs avec l’ID 0 sont souhaités. Cette mémoire tampon contient les informations utilisées pour trier le maillage en fonction des attributs dans ID3DXMesh::Optimize. Si aucune table d’attributs n’est présente, ID3DXBaseMesh::D rawSubset analyse cette mémoire tampon pour sélectionner les visages de l’attribut donné à dessiner.

Table d’attributs

La table d’attributs est une structure détenue et gérée par le maillage. La seule façon d’en générer un consiste à appeler ID3DXMesh::Optimize avec le tri des attributs ou une optimisation plus forte activée. La table d’attributs est utilisée pour lancer rapidement un appel primitif de dessin unique à ID3DXBaseMesh::D rawSubset. La seule autre utilisation est que les maillages en progression conservent également cette structure, de sorte qu’il est possible de voir quels visages et sommets sont actifs au niveau de détail actuel.

Performances de Z-Buffer

Les applications peuvent améliorer les performances lors de l’utilisation de la mise en mémoire tampon z et de la texturation en veillant à ce que les scènes soient rendues de l’avant vers l’arrière. Les primitives z-buffered texturées sont prétestées par rapport à la mémoire tampon z sur une ligne d’analyse. Si une ligne de balayage est masquée par un polygone précédemment rendu, le système la rejette rapidement et efficacement. La mise en mémoire tampon Z peut améliorer les performances, mais la technique est particulièrement utile lorsqu’une scène dessine les mêmes pixels plusieurs fois. C’est difficile à calculer exactement, mais vous pouvez souvent faire une approximation étroite. Si les mêmes pixels sont dessinés moins de deux fois, vous pouvez obtenir les meilleures performances en désactivant la mise en mémoire tampon z et en rendant la scène de l’arrière vers l’avant.

Conseils de programmation