Skip to main content
Avec l’introduction des contextes à sémantique d’objet rapide et des contextes de calcul asynchrone dans DirectX 12, la responsabilité de la gestion des risques liés aux ressources incombe aux développeurs de titres. Étant donné que le GPU XBOX One nécessite une synchronisation explicite lorsque des données sont produites par un type de cache ou de DMA, puis consommées par un autre cache ou DMA, il est particulièrement important de comprendre les différents mécanismes de synchronisation sur le GPU. Cette rubrique considère le GPU comme un « pipeline » et décrit la cohérence des données et la synchronisation en termes de progression dans ce pipeline. Elle sera particulièrement utile aux programmeurs graphiques qui souhaitent obtenir un rendu correct et performant avec les contextes à sémantique d’objet rapide de DirectX 11.X et avec DirectX 12.
Si vous n’êtes pas encore familiarisé avec la structure des caches du GPU XBOX One, nous vous recommandons de lire d’abord la documentation sur le cache GPU sur XGD.
Dans cette rubrique :

Introduction

Le GPU XBOX One nécessite une synchronisation explicite lorsque des données sont produites par un type de cache ou de DMA, puis consommées par un autre cache ou DMA. Avec l’introduction des contextes à sémantique rapide et des contextes de calcul asynchrone dans DirectX 11.X, les développeurs de titres sont désormais responsables de la gestion des risques liés aux ressources. Il est donc particulièrement important de comprendre les différents mécanismes de synchronisation sur le GPU. DirectX 12 supprime également le suivi des risques de l’API. L’abstraction de DirectX 12 se situe à un niveau plus élevé que celle de DirectX 11.X, mais il est également important de comprendre les mécanismes de synchronisation GPU de bas niveau sous DirectX 12.

Synchronisation du GPU

Le GPU XBOX One dispose de huit contextes graphiques, dont sept sont disponibles pour les jeux. En termes simples, on dit qu’une séquence d’appels de dessin partageant le même état de rendu partage le même contexte. Les dispatchs ne nécessitent pas de contextes graphiques et peuvent s’exécuter en parallèle du travail graphique. En général, deux concepts interviennent dans la synchronisation du GPU : le pipelining et les caches. À un niveau très basique, un GPU peut être considéré comme un pipeline dans lequel les appels de dessin et les dispatchs de compute shader entrent par le processeur de commandes et se retirent une fois entièrement exécutés. Même si les dessins et les dispatchs peuvent s’exécuter simultanément sur le GPU, ils ne se dépassent pas les uns les autres et se retirent toujours dans l’ordre. Il est particulièrement important de connaître deux aspects du pipelining :
  • Comment garantir que deux appels de dessin ou dispatchs ne s’exécutent pas simultanément. Cela est nécessaire pour garantir que l’appel de dessin qui produit les données n’exécute pas de vagues en même temps que celui qui consomme les données.
  • Où insérer les différents événements programmables, c’est-à-dire en haut du pipeline ou en bas.
Pour fonctionner efficacement, les différents blocs du GPU disposent de leurs propres caches mémoire, parfois plusieurs. Certains caches, comme le L2 (TCC) et le L1 (TCP), sont organisés de manière hiérarchique. La plupart des blocs du GPU s’exécutent toujours en parallèle et ne synchronisent pas automatiquement leurs caches ni n’espionnent ceux des autres. Un pilote ou un programmeur graphique doit synchroniser les caches et garantir la cohérence des données. Sur les contextes Direct3D à sémantique héritée, le travail de synchronisation est effectué par le pilote, via le mécanisme de détection des risques, tandis que sur les contextes Direct3D à sémantique d’objet rapide, l’application elle-même doit garantir la cohérence des données.

Aperçu des étapes du pipeline

Voici un aperçu du pipeline du point de vue de la synchronisation : Tout en haut du pipeline se trouve le processeur de prélecture (PFP), qui est la partie du processeur de commandes qui lit la mémoire pour le micro-moteur (ME). Le PFP lance également l’accès direct à la mémoire (DMA) pour le bloc de géométrie et de tessellation des sommets (VGT), en particulier pour les tampons d’index et les tampons de dessin indirect. Le PFP est responsable des tâches suivantes :
  • Lecture des données du tampon de commandes.
  • Déclenchement des transferts DMA du tampon d’index pour le VGT.
  • Lecture des paramètres du tampon indirect.
  • Lecture des informations de prédication.
  • Communication et synchronisation avec le ME.
L’étape suivante du pipeline est le ME. Le PFP et le ME, qui font tous deux partie du processeur de commandes, sont reliés par deux files d’attente de type premier entré, premier sorti. Avant que le ME puisse commencer à exécuter des commandes, toutes les données doivent avoir été lues depuis la mémoire par le PFP. Le processeur de commandes essaie toujours de s’exécuter aussi loin que possible en avance sur le reste du GPU et tente toujours d’exécuter les paquets de commandes s’il n’est pas bloqué. La synchronisation au niveau du PFP est plus coûteuse que la synchronisation au niveau du ME, car il est plus éloigné du GPU et parce que le mécanisme de synchronisation PFP vers ME est coûteux. Le processeur de commandes contient également une unité appelée CP DMA, qui peut être utilisée pour effectuer des copies génériques de la mémoire et du magasin de données global (GDS) par accès direct à la mémoire et par le cache L2 du GPU. Le CP DMA peut s’exécuter de manière asynchrone ou synchrone par rapport aux autres paquets du processeur de commandes et à lui-même. Il peut être lancé par le PFP ou le ME, mais il est principalement utilisé depuis le ME. Pour les besoins de cette discussion, il est utile de considérer les blocs d’exécution des shaders du GPU comme un bloc monolithique. À moins qu’une synchronisation manuelle à l’aide d’opérations atomiques de shader ne soit utilisée, cette partie du pipeline GPU est relativement simple en matière de synchronisation. La partie à fonction fixe du GPU qui exécute les écritures dans le tampon de couleur, le blending, les tests de profondeur, etc., peut également être considérée comme un bloc monolithique. Conceptuellement, le processus de synchronisation à travers le pipeline est le suivant :
  1. Un paquet de dessin ou un paquet de commande de dispatch entre dans le processeur de commandes par le processeur de prélecture.
  2. Le paquet est exécuté par le micro-moteur.
  3. Les étapes de shader du paquet sont exécutées par l’entrée du processeur de shaders (SPI) et le séquenceur (SQ).
  4. Le back-end de rendu à fonction fixe (comprenant le bloc d’exportation des shaders [SX], le bloc de profondeur [DB] et le bloc de couleur [CB]) termine le travail non lié aux shaders.

Événements de fin de pipe et de fin de shader

Divers événements peuvent être insérés dans le pipeline entre les dispatchs et les dessins. Ils transitent tous dans le pipeline dans l’ordre, avec les dispatchs et les dessins. Deux types d’événements présentent un intérêt particulier : la fin de pipe (EOP) et la fin de shader (EOS). Figure 1. Pipeline de synchronisation avec des flèches indiquant à quel moment les événements EOS et EOP sont signalés Pipeline de synchronisation avec des flèches indiquant à quel moment les événements EOS et EOP sont signalés Un événement de fin de pipe signale au processeur de commandes que l’exécution des shaders et l’exécution du back-end de rendu sont terminées, mais avant que l’écriture de la mémoire concernée soit terminée. Notez que les événements EOP traitent le travail de calcul et le travail graphique de la même manière. Un événement de fin de shader est signalé lorsque la dernière vague du dessin ou du dispatch est retirée, mais avant que le back-end termine son exécution et avant que la mémoire soit écrite. Alors qu’il n’existe aucune distinction calcul/graphique pour les événements EOP, les événements EOS sont différents pour le calcul et pour le graphique. Cette distinction fournit des mécanismes de synchronisation efficaces pour différents scénarios.

Barrière de pipeline

Une barrière de pipeline de dessin et de dispatch est parfois nécessaire pour garantir que deux appels de dessin ou dispatchs ne s’exécutent pas en parallèle. Par exemple, lorsque vous devez invalider un cache entre deux appels de dessin afin que l’appel de dessin suivant voie des données à jour en mémoire, et que les deux appels de dessin s’exécutent en même temps, aucun vidage de cache ne fonctionnera ; les données risquent d’être consommées avant d’être produites. C’est dans ce cas que vous devez vous assurer que l’appel de dessin ou le dispatch précédent est terminé avant que le suivant ne commence. Figure 2. Si les wavefronts ou le travail back-end du producteur chevauchent les wavefronts du consommateur, une barrière est nécessaire pour garantir qu’ils se synchronisent correctement. Si les wavefronts ou le travail back-end du producteur chevauchent les wavefronts du consommateur, une barrière est nécessaire pour garantir qu'ils se synchronisent correctement. Il existe quatre mécanismes pour insérer une barrière de pipeline : un vidage partiel, une fence EOP, une fence EOS et une synchronisation de surface. Les trois premiers sont abordés dans cette section, et la synchronisation de surface est abordée plus loin dans ce document.
Vidage partiel
Un vidage partiel est une barrière qui peut être insérée entre deux appels de dessin ou dispatchs pour garantir qu’un appel de dessin précédent se termine avant que le suivant ne commence. À la réception d’une commande de vidage partiel, le processeur de commandes bloque l’exécution des paquets jusqu’à ce qu’il reçoive un signal indiquant qu’une étape de vertex shader (VS), de pixel shader (PS) ou de compute shader (CS) s’est entièrement vidée. Un seul événement de vidage partiel peut être actif à la fois dans le processeur de commandes. Notez que les barrières de vidage partiel sont levées à l’événement de fin de shader, et non lorsque l’écriture de la mémoire est terminée. Un vidage partiel peut être émis dans DirectX 11.x à l’aide de la méthode GpuSendPipelinedEvent avec l’un des indicateurs PARTIAL_FLUSH.
Fences EOS et EOP
Une fence est une primitive de synchronisation bien connue qui peut également servir de barrière. Au moins deux paquets interviennent dans une telle barrière. Le premier amène le GPU à écrire une valeur à un emplacement mémoire. Le second bloque le processeur de commandes jusqu’à ce que la valeur en mémoire satisfasse une condition spécifiée dans le second paquet. Une fence peut servir de barrière parce que la valeur est généralement écrite à la position EOP ou EOS du pipeline, et que l’attente a lieu en haut du pipe. En pratique, cela signifie que si un appel de dessin « avant la fence » est toujours en cours et que la commande « attendre la fence » a été exécutée, tout le traitement du tampon de commandes est bloqué jusqu’à ce qu’il atteigne l’étape EOP ou EOS. L’inconvénient de l’utilisation de fences comme barrières de pipeline est le coût d’au moins deux transactions mémoire, soit au moins 800 cycles ou 1 µs. Une méthode InsertWaitUntilIdle effectue une écriture EOP suivie d’une attente immédiate. C’est la barrière de pipeline la plus lourde. Cette barrière est la plus courte lorsque le GPU est déjà complètement inactif. La méthode WriteValueEndOfPipe écrit des valeurs EOP ou EOS à un emplacement mémoire, et la méthode InsertWaitOnMemory insère une attente sur l’emplacement mémoire jusqu’à ce qu’il soit égal à la valeur donnée dans le paquet. Notez que le GPU prend en charge plusieurs fonctions de comparaison, telles que inférieur à ou supérieur à, qui sont exposées dans l’API DirectX 11.x. Le GPU prend également en charge les attentes de fence dans le PFP et le ME, qui sont aussi exposées dans DirectX 11.x. Il est également possible d’éviter la synchronisation en comptant le nombre de changements de contexte (context rolls) entre le producteur et le consommateur. Si huit changements de contexte ont eu lieu entre le producteur et le consommateur, il est garanti que le producteur s’est retiré du GPU.

Différences entre les barrières PARTIAL_FLUSH, fence EOS et fence EOP

Si le producteur est immédiatement suivi du consommateur, un vidage partiel et une barrière de fence EOP sont pratiquement équivalents. Si le producteur et le consommateur sont tous deux des dessins ou tous deux des dispatchs, la barrière de fence EOS fonctionnera de la même manière. Dans ce cas, les trois barrières seraient équivalentes, sauf que chaque fence doit effectuer deux allers-retours en mémoire. Par conséquent, lorsqu’un producteur est suivi d’un consommateur, il est judicieux d’utiliser une barrière de vidage partiel. Toutefois, s’il existe d’autres appels de dessin et dispatchs entre le producteur et le consommateur, l’utilisation d’un vidage partiel pourrait entraîner un blocage inutile du pipeline. Rappelez-vous qu’un vidage partiel vide toujours le pipeline à partir de l’étape de shader spécifiée avant que le paquet de commande suivant soit exécuté, comme l’illustre la figure 3. Le blocage du pipeline entre la fin du second appel de dessin non producteur et le début de l’appel de dessin consommateur peut être inutile. Figure 3. Même si le producteur a terminé au moment où le consommateur commence et que les deux appels de dessin intermédiaires sont indépendants, le vidage partiel introduit un blocage. Dans ce cas, le blocage est inutile ; le producteur ne chevaucherait jamais le consommateur. Même si le producteur a terminé au moment où le consommateur commence et que les deux appels de dessin intermédiaires sont indépendants, le vidage partiel introduit un blocage. Dans ce cas, le blocage est inutile ; le producteur ne chevaucherait jamais le consommateur. Avec un vidage partiel, il est possible de spécifier si une étape CS, PS ou VS doit être vidée. La méthode InsertWaitUntilIdle fonctionne de la même manière, mais elle vide toujours complètement toutes les étapes de shader. Dans certains cas, une fence EOP ou EOS est la méthode à privilégier. Comme une écriture mémoire EOP ou EOS est insérée immédiatement après le producteur, le consommateur doit simplement vérifier la fence correspondante avant de commencer. Consultez la figure 4 pour une illustration. Cela élimine le blocage du pipeline illustré dans la figure 3. Figure 4. En utilisant des fences, il est possible de réduire ou d’éliminer le blocage du pipeline entre le producteur et le consommateur, à condition que les dessins intermédiaires soient indépendants. En utilisant des fences, il est possible de réduire ou d'éliminer le blocage du pipeline entre le producteur et le consommateur, à condition que les dessins intermédiaires soient indépendants. Le pilote à sémantique héritée privilégie les vidages partiels pour la synchronisation du pipeline, car cela implique moins de gestion. Toutefois, sur les appareils à sémantique d’objet rapide, où la suppression ou la réduction des blocages du pipeline est importante, nous vous recommandons d’utiliser plutôt des fences.
La différence entre EOS et EOP est que EOP s’applique à la fois aux dispatchs et aux dessins. Par conséquent, un événement EOP attend que tous les dessins et dispatchs lancés précédemment soient terminés avant d’être signalé, tandis qu’un événement EOS peut être configuré pour être signalé séparément pour les dispatchs et pour les dessins. Dans certains cas, lorsque du travail de calcul s’exécute en parallèle des dessins, cette distinction peut être importante. La synchronisation du pipeline n’affecte aucun cache GPU ; pour une synchronisation complète, des vidages de cache peuvent être nécessaires.

Présentation des caches

Le GPU XBOX One comporte de nombreux caches. S’assurer que les barrières de pipeline sont correctement placées constitue la première étape de la synchronisation. La seconde étape consiste à vider et à invalider les caches correspondants afin que les données écrites par le producteur soient visibles par le consommateur. Les caches du GPU XBOX One prennent généralement en charge une réécriture et invalidation (writeback-and-invalidate) en une seule opération et, à l’exception du bloc de profondeur, il est impossible de spécifier la plage mémoire de cette opération. Le processeur de commandes peut signaler les vidages de cache en haut ou en bas du pipe. Toutes les lignes de cache font 64 octets.

Opérations de cache prises en charge

Le tableau suivant répertorie les différents caches du GPU XBOX One, ainsi que leurs noms matériels et les opérations prises en charge. Une explication de ces caches suit le tableau.
  • Les caches L2 et L1 prennent en charge le vidage et l’invalidation en une seule opération. Les caches L1 et L2 prennent également en charge une opération de réécriture sans invalidation, mais uniquement pour les lignes de cache spécialement marquées comme « volatiles ». Les lignes de cache volatiles doivent être marquées spécifiquement au moyen d’un descripteur de ressource et, en octobre 2014, elles ne sont pas prises en charge par le pilote graphique XBOX One. Les caches L1 et L2 ne respectent pas les plages de vidage.
  • Les caches CB et DB prennent tous deux en charge le vidage et l’invalidation en une seule opération. Le CB ne respecte pas les plages de vidage.
  • Seul le DB prend en charge une opération de vidage et d’invalidation par plage sur ses caches. Les autres blocs invalident toujours l’intégralité du cache. Cela signifie que le coût d’un vidage de cache peut être important.
Aucun vidage n’est nécessaire si les données sont consommées par le même cache que celui par lequel elles ont été écrites. Cela s’applique au test et à l’écriture de profondeur, au blending des couleurs et aux accès en lecture et en écriture des vues d’accès non ordonné (UAV). La seule exception actuelle est le cache L1, dont la stratégie actuelle exige qu’il soit vidé lorsqu’un producteur écrit à travers lui et qu’un consommateur lit à travers lui. Pour plus d’informations, consultez « Stratégies du cache L1 » ci-dessous.

Ressources D3D et caches

Les différents types de ressources D3D utilisent différents caches GPU.
  • Toutes les lectures et écritures de tampons non constants, de textures et d’UAV sont effectuées via les caches L1 et L2, respectivement le TCP et le TCC. En d’autres termes, toutes les instructions VMEM passent par les caches L1 et L2, qui sont des caches en lecture et en écriture.
  • Les lectures de tampons scalaires sont effectuées via le KetlecacheL2.LeK et le cache L2. Le K est un cache en lecture seule qui lit via le L2. Toutes les instructions SMEM lisent via le K$ et le cache L2.
  • Les instructions de shader sont lues via le IetlecacheL2.LeI et le cache L2. Le I est un cache en lecture seule.
  • L’accès à la mémoire du tampon de couleur s’effectue via un ensemble de caches CB (bloc de couleur). Cela inclut la lecture des données pour le blending ; le blending lui-même a lieu dans l’unité CB. Les caches CB incluent les pixels réels et les métadonnées de surface, telles que CMask et FMask.
  • L’accès à la mémoire du tampon de profondeur s’effectue via les caches du bloc de profondeur (DB). Cela inclut les données en lecture et en écriture pour les tests de profondeur, la compression et les mises à jour Hi-Z. Les caches DB incluent les pixels de profondeur réels, les métadonnées de la surface de profondeur telles que la compression (PMask et plans) et les informations Hi-Z (HTile).

Vidages en haut de pipe et en fin de pipe

Il existe une différence entre une opération d’invalidation de cache en haut de pipe et en fin de pipe (pipelinée). Une opération d’invalidation de cache en haut de pipe envoie le signal d’invalidation au cache cible immédiatement après que le processeur de commandes a lu le paquet de vidage correspondant, quel que soit le travail en cours d’exécution sur le GPU à ce moment-là. C’est pourquoi les vidages de cache en haut de pipe sont généralement effectués après un blocage de barrière de pipeline : le producteur a terminé son travail et le consommateur n’a pas encore commencé le sien. Une opération de cache pipelinée est exécutée à la fin de pipe de l’appel de dessin précédent. Certains vidages ne peuvent être émis que sous forme d’événements de fin de pipe, par exemple les vidages des caches de métadonnées CB et DB. Tout vidage de cache se déclenche immédiatement, mais prend un certain temps pour se terminer. Certains appels de dessin et dispatchs en cours peuvent se retrouver avec des caches froids en cours d’exécution. C’est une autre raison pour laquelle les vidages de cache peuvent être coûteux. Un vidage de cache en fin de pipe peut être émis à l’aide de l’API GpuSendPipelinedEvent dans DirectX 11.x, et le vidage de cache en haut de pipe peut être émis par des paquets FlushGpuCacheRange. Des API équivalentes existent pour les contextes de calcul asynchrone : FlushGpuCachesBottomOfPipe et FlushGpuCachesTopOfPipe.

Stratégies du cache L1

Toutes les opérations VMEM passent par la hiérarchie de caches L1-L2. Alors que le cache L2 est partagé entre toutes les unités de calcul et qu’aucune synchronisation n’est généralement nécessaire pour garantir la cohérence des instructions VMEM, le L1 est instancié par unité de calcul. Il est donc important de comprendre les stratégies du cache L1. Le cache L1 prend en charge les quatre stratégies suivantes : HIT_LRU Cette stratégie autorise les accès réussis (hits) sur les données du wavefront précédent et conserve les données de la vague actuelle dans le cache pour les vagues suivantes. HIT_EVICT Cette stratégie autorise les accès réussis sur les données du wavefront précédent, et les données de la vague actuelle sont invalidées dans le cache lorsque la vague est terminée. MISS_LRU Cette stratégie n’autorise pas les accès réussis sur les données du wavefront précédent et conserve les données de la vague actuelle dans le cache pour les vagues suivantes. MISS_EVICT Cette stratégie n’autorise pas les accès réussis sur les données du wavefront précédent, et les données de la vague actuelle sont invalidées dans le cache lorsque la vague est terminée. Les lectures prennent en charge l’une ou l’autre des quatre stratégies, les écritures prennent en charge les variantes MISS*, et les opérations atomiques ne prennent en charge que la stratégie MISS_EVICT. Actuellement, le pilote est programmé pour utiliser HIT_LRU pour les lectures et MISS_LRU pour les écritures. Étant donné que ces deux stratégies permettent à une lecture d’atteindre l’écriture d’une vague précédente, le vidage du cache L1 peut être nécessaire dans certains cas.

Synchronisation de surface

Une synchronisation de surface (surface sync) est une commande en haut de pipe que le pilote utilise pour déclencher divers vidages et invalidations de cache. Elle garantit également que les confirmations d’écriture des écritures mémoire ont été reçues. C’est ce que l’on appelle une opération de cohérence. Les opérations de cohérence peuvent être effectuées sur les blocs de couleur et les blocs de profondeur, ainsi que sur quatre emplacements mémoire arbitraires, utilisés pour la synchronisation du stream-out (SO). Une opération d’invalidation de cache et une opération de cohérence peuvent être spécifiées dans le même paquet. Une opération de cohérence fonctionne en vérifiant d’abord si une surface définie par les paramètres de pointeur de base et de taille est actuellement utilisée par des contextes graphiques actifs, puis en bloquant le CP jusqu’à ce que ces contextes graphiques soient terminés et que les confirmations d’écriture des surfaces soient reçues. Une opération de synchronisation de surface implique un changement de contexte, sauf si elle est émise entre la définition d’un registre de contexte et un dessin. Notez qu’un événement pipeliné est associé à un contexte actuel ; ainsi, une séquence Draw/ContextRoll/InsertPipelinedEvent/InsertSurfaceSync-attendant-l’événement-pipeliné ne fonctionnera pas, car le contexte nouvellement changé ne comporte aucun dessin. Pourquoi une opération de cohérence peut-elle être nécessaire ? Le déclenchement d’une invalidation de cache ne rend pas cette mémoire immédiatement disponible et visible pour les autres blocs du GPU. Il faut parfois des milliers de cycles pour terminer l’opération de vidage du cache. En plus de rendre les vidages de cache très coûteux, cela signifie également qu’un mécanisme est nécessaire pour garantir que le vidage est terminé avant de démarrer un appel de dessin qui consomme ces données. Une opération de synchronisation de surface peut être utilisée comme barrière entre un producteur et un consommateur si le producteur écrit via le CB ou le DB. Pour configurer une telle barrière, activez simultanément les opérations de vidage et de cohérence sur le paquet de synchronisation de surface. Vous pouvez le faire comme indiqué dans l’exemple suivant.
Dans DirectX 11.x, la méthode ID3D11DeviceContextX::FlushGpuCacheRange utilise un paquet de synchronisation de surface et peut effectuer à la fois des opérations de cache en haut de pipe et des opérations de cohérence. Vous ne devez pas définir simultanément les valeurs de l’adresse de base et de la taille sur zéro ; cela provoque parfois le blocage de l’unité DB. Utilisez plutôt zéro pour la base et 0xfffffff00 pour la taille. Ces valeurs semblent fonctionner raisonnablement bien. Notez que le vidage de l’opération FMask ne peut pas être synchronisé à l’aide d’un paquet de synchronisation de surface. Si vous devez le relire via une texture, par exemple, une inactivité complète du GPU est nécessaire. Vous pouvez y parvenir à l’aide de l’API InsertWaitUntilIdle.

Synthèse

Compte tenu de cette discussion sur les différentes options de synchronisation et de cohérence du GPU, comment déterminer quelles opérations fourniront le scénario de synchronisation optimal pour votre titre ? La matrice des combinaisons possibles est très vaste, ce qui rend peu pratique une approche par formule. Considérez plutôt le fonctionnement de la synchronisation d’un point de vue pratique. Ainsi, toute opération peut être déduite à partir des principes fondamentaux. Idéalement, la synchronisation doit être aussi légère que possible. Il est toujours possible d’insérer la barrière GPU la plus lourde et de vider tous les caches au niveau du PFP, ce qui arrête complètement le GPU et invalide tout ce qu’il contient. Cette approche est utile pour déboguer les problèmes de synchronisation, mais elle est trop brutale pour être utilisée en production. Voici un algorithme possible pour choisir la synchronisation appropriée :
  1. Identifiez les blocs qui seront responsables de la production et de la consommation des données. Producteurs typiques :
    1. CB : surfaces de couleur, pixels et métadonnées.
    2. DB : surfaces de profondeur, pixels et métadonnées.
    3. TCP/TCC : UAV en écriture directe, CopyMemoryToMemory.
    4. SO : stream-out (une synchronisation complète est nécessaire).
    5. CP : WriteValueEndOfPipe, WriteTimestampToMemory.
    6. CP DMA (peut utiliser le TCC) : WriteGDS, certaines CopyResources.
    7. CPU : écritures sur le CPU.
    [!NOTE] Pour plus d’informations sur l’amélioration des performances du rendu CPU, reportez-vous au livre blanc intitulé « Command Lists, Draw Bundles, and Deferred Contexts », disponible en téléchargement sur la page Livres blancs de XGD. Consommateurs typiques : 1. TCP/TCC : toutes les textures, tous les tampons de sommets et toutes les lectures d’UAV.
    1. K$/TCC : tous les tampons constants.
    2. CB : blending des couleurs.
    3. DB : test de profondeur.
    4. CP/PFP : tampons de prédication, indices de sommets et tampons indirects.
    5. CPDMA (peut éventuellement lire via le TCC) : ReadGDS depuis la mémoire, certaines CopyResources.
  2. Insérez un vidage de cache pipeliné (en fin de pipe) pour le producteur.
    1. GpuSendPipelinedEvent pour CB, DB et SO.
    2. WriteValueEndOfPipe avec les indicateurs L1/L2 pour le TCP/TCC.
  3. Utilisez la barrière de pipeline appropriée entre le producteur et le consommateur.
    1. Vidage partiel
    2. Fence EOS
    3. Fence EOP
    4. Synchronisation de surface
  4. Après l’opération d’attente de la barrière, insérez une invalidation de cache en haut de pipe pour le consommateur. Cela se fait invariablement via une opération GpuCacheFlushRange, qui invoque une synchronisation de surface.
Figure 5. L’ordre du pipeline va de haut en bas. Les blocs bleus sont les blocs fonctionnels du GPU. Les blocs verts sont les opérations qu’ils effectuent. L'ordre du pipeline va de haut en bas. Les blocs bleus sont les blocs fonctionnels du GPU. Les blocs verts sont les opérations qu'ils effectuent.

Exemples

Les exemples suivants illustrent l’application pratique des opérations de synchronisation abordées dans cette rubrique. Synchronisation lorsqu’un tampon constant est mis à jour par le CPU
  1. Si le CPU utilise de la mémoire à écriture combinée et que l’adresse du tampon dynamique n’a pas été vue dans cette image, ne faites rien. Si les pointeurs ne sont pas alignés sur 64 octets et que l’allocation est séquentielle, videz le cache K.
  2. Émettez un vidage GPU en haut de pipe des caches K et L2. Comme les vidages ne respectent pas la plage, n’émettez un vidage qu’une seule fois par lancement.
Synchronisation d’une cible de rendu destinée à être utilisée comme texture dans le dessin suivant
  1. Émettez un appel de dessin producteur qui effectue le rendu dans l’objet X en tant que cible de rendu via CB0.
  2. Émettez un vidage et une invalidation pipelinés des pixels du tampon de couleur. Méthode 1. À utiliser lorsqu’il existe davantage de dessins ou de dispatchs indépendants entre les producteurs et les consommateurs, afin de tenter d’éliminer une bulle.
    1. Émettez une écriture mémoire EOS PS_DONE.
    2. Avant que le consommateur ne démarre, insérez une attente sur la mémoire.
Méthode 2. Similaire à la méthode 1, mais ne nécessite ni écriture ni lecture en mémoire. Opération de synchronisation de surface qui vide le L2 et effectue une vérification de cohérence sur l’adresse de base et la taille de CB0. Méthode 3. Introduit une bulle dans le pipeline ; à utiliser idéalement lorsque le producteur et le consommateur sont adjacents.
  1. Insérez un PS_PARTIAL_FLUSH.
  2. Insérez un vidage du cache L2 en haut de pipe.
  3. Émettez un dessin consommateur.
Synchronisation d’une écriture UAV destinée à être utilisée comme cible de rendu
  1. Émettez un dispatch producteur qui écrit dans l’UAV X.
  2. Émettez un vidage pipeliné des caches L1/L2.
  3. Émettez une écriture de fence EOP F (nécessite une synchronisation entre un CS et un PS).
  4. Avant que le consommateur ne démarre, insérez une attente sur la fence F. (Vous pouvez également utiliser un CS_PARTIAL_FLUSH à la place des étapes 3 et 4.)
  5. Émettez un vidage et une invalidation pipelinés des caches CB et CB_META.
  6. Émettez un dessin.
Synchronisation d’un tampon indirect écrit par un CS via un UAV
  1. Émettez un dispatch producteur qui écrit dans l’UAV X.
  2. Émettez un vidage pipeliné des caches L1/L2.
  3. Émettez une écriture de fence EOP F (nécessite une synchronisation entre un CS et un PS). Méthode 1 Avant que le consommateur ne démarre, insérez une attente sur la mémoire F sur le PFP. La synchronisation doit avoir lieu au niveau du PFP, car c’est lui qui lit les tampons indirects. Méthode 2 Utilisez une attente sur la mémoire sur le ME, suivie de PFP_SYNC_TO_ME.
  4. Émettez un dessin indirect.

Conclusion

Les performances et l’exactitude sont toujours des priorités absolues pour le développeur graphique. Les contextes à sémantique d’objet rapide apportent une amélioration significative des performances sur le CPU et le GPU par rapport au contexte à sémantique héritée. Cela est dû en partie à la désactivation de la détection et de la prévention des risques dans le pilote graphique. DirectX 12 reconnaît également qu’il est difficile de rendre efficace la détection automatique des risques ; il introduit donc le concept de « barrières de transition de ressources » pour faciliter la prévention des risques. Dans les deux cas, la synchronisation est désormais entre les mains du développeur. En comprenant la synchronisation sur le GPU XBOX One, vous devriez la plupart du temps pouvoir obtenir un rendu correct et atteindre de meilleures performances de synchronisation qu’avec la sémantique héritée.
Last modified on October 6, 2026