Version imprimable       Envoyer     
Cliquez pour évaluer et commenter
MSDN
MSDN Library
Articles Techniques
Développement Win32 et COM
Graphiques et Multimedia
DirectX
Driving DirectX
 DirectX 10 - Deuxième partie
DirectX 10 - Deuxième partie

Le point de vue du développeur.

Paru le 22 décembre 2006

Guillaume Randon – Microsoft Services

Sur cette page

Présentation Présentation
L’API
du point de vue du développeur L’API du point de vue du développeur
Application d’exemple Application d’exemple
Une autre perspective sur cette application
d’exemple Une autre perspective sur cette application d’exemple
Conclusion Conclusion

Présentation

Ce document fait suite à l’article intitulé «DirectX 10 – Des changements en profondeur». Cet article traitait du modèle de driver WDDM sur lequel s’appuie cette version de DirectX et il peut être bon de s’y reporter avant d’attaquer la lecture de ce document.

DirectX est une API graphique qui permet d’accéder aux fonctionnalités des cartes graphique. La version 10 sera disponible sous peu sur Windows Vista. Cette dernière version est spécialement intéressante car les changements qu’elle induit ne se limitent pas aux modèles-objets proposés aux développeurs mais touchent en profondeur le modèle de driver utilisé pour communiquer avec la carte ainsi que les fonctionnalités de celle-ci.

Dans ce document, je me propose de décrire plus en profondeur le modèle-objet associé à DirectX 10 et de détailler un exemple de code.

Ce document, bien qu’étant une introduction à DirectX 10, se destine à des personnes qui sont déjà familières avec DX9, les Shaders, et le système d’effets de DirectX. Pour les personnes intéressées par le sujet mais qui n’auraient pas encore eu le loisir de découvrir ces concepts, ils peuvent se reporter entre autre aux documents ci-dessous:

DirectX Managed : Rappel des concepts de bases

Introduction aux Shaders avec Managed DirectX

DirectX est un SDK qui couvre aussi les domaines de l’audio ainsi que la gestion des entrées utilisateur. Dans ce document, je ne décrirais pas ces parties et je me concentrerais essentiellement sur les fonctionnalités touchant à l’utilisation des cartes graphiques.

L’API du point de vue du développeur

Nous allons maintenant décrire l’API, le modèle-objet en lui-même avec lequel tout développeur va interagir pour réaliser une application s’appuyant sur DirectX 10. Cette nouvelle version de l’API, comme pour les précédentes versions abstrait le fonctionnement de la carte graphique en elle-même sous-forme d’un pipeline. Ce pipeline, dans le cas de DirectX 10, présente des différences significatives avec celui sous-tendu par DX9. Par ailleurs, comme nous allons le voir le modèle-objet en lui-même promeut, et d’une certaine manière requiert une meilleure connaissance du pipeline en question. En même temps, il permet une utilisation plus flexible de celui-ci.

Le diagramme ci-dessous est extrait du document «The Direct3D® 10 System» par David Blythe disponible sur msdn.microsoft.com/directx, il permet d’avoir une bonne vision du pipeline graphique associé à DX10:

On peut remarquer qu’un nouveau type de «Shader», le «Geometry Shader» fait son apparition. Jusqu’à présent nous disposions des «Vertex» et «Pixel Shaders». Au niveau des «Vertex Shaders» (VS,) on pouvait jouer sur la position et les attributs d’un vertex donné et ceux vertex après vertex. Dans le «Pixel Shader» (PS), il était possible d’affecter les propriétés d’un pixel donné couvert par une de nos primitives (pour plus d’informations sur ces types de «Shaders» il est possible de ce reporter à l’article ci-dessous ou à tout ouvrage sur DX9: http://www.microsoft.com/france/msdn/directx/Shaders-avec-Managed-DirectX_1.mspx ).

Jusqu’à présent, le nombre de «Vertex» était fixe et il était impossible de demander au GPU de les créer à la volée ou d’émettre de nouvelles primitives. C’est justement ce que permet de faire le «Geometry Shader». D’une part, il est possible de travailler sur les positions de tous les sommets d’une primitive en même temps et ce, en accédant en même temps si nécessaire à des informations concernant leur voisinage et la façon dont ils sont connectés à leurs voisins. Cela était impossible auparavant, pour mémoire dans le VS, on travaille pour ainsi dire point par point sans aucune des informations suscitées. Ceci peut permettre de manipuler les primitives en tant que telles, par exemple pour les repositionner. Par ailleurs le «Geometry Shader» (GS) a la capacité d’émettre de nouvelles primitives. Les performances des cartes sur ce type de traitement ne sont pas encore bien connues mais cela ouvre au GPU la possibilité de créer de façon procédurale des portions de géométrie qui seront affichées à l’écran ou plus généralement d’émettre des données. On remarque par ailleurs une boite intitulé StreamOutput (SO) en sortie du Geometry Shader (GS). Le pipeline DX10 dispose d’une fonctionnalité intéressante qui est de sauver le résultat des étapes antérieures au Rasterizer en mémoire par exemple pour que ces données soient à nouveau soumises en entrée du pipeline, le tout sans qu’elles ne soient traitées par le Rasterizer ou le Pixel Shader. C’est important car le Pixel Shader est par essence une étape coûteuse car il est invoqué pour tous les pixels couverts par une primitive, à partir du moment où une autre primitive n’est pas déjà devant celle-ci. En fonction des scènes et du degré de sophistication des algorithmes qui organisent la scène et effectuent ces appels aux fonctions de rendu des primitives, cela peut représenter un nombre considérable de traitements. C’est une des raisons pour lesquelles il est souvent conseillé pour les scènes d’extérieur d’afficher le ciel en dernier (le ciel par exemple).

En outre cette possibilité de disposer en mémoire du résultat de cette première partie du pipeline sans exécuter les dernières étapes de celui-ci est d’autant plus intéressante qu’une fonctionnalité appelée DrawAuto permet de réinjecter ce résultat en entrée du pipeline sans aucune intervention du CPU.

Quoi qu’il en soit on voit que ces spécificités du nouveau pipeline rendent son utilisation extrêmement flexible. Dans un premier temps, il est possible que pour des raisons de performance il soit peut-être plus intéressant d’utiliser essentiellement les GS pour effectuer des opérations sur les sommets des primitives en travaillant par primitive et en utilisant les informations sur le voisinage de ces points et finalement sans émettre de primitives supplémentaires. Mais l’API et le pipeline donnent toute latitude pour utiliser le GS de façon plus large. S’il s’avère qu’émettre des données s’effectue avec de bonnes performances, DX10 permet ce type d’emploi du GPU.

J’ai mentionné plus haut la possibilité d’émettre de nouvelles données à la volée, de même il est possible d’en supprimer.

Au delà de ce nouveau type de Shader, les VS et PS gardent eux des fonctionnalités et des modes de fonctionnement similaires à ce qu’ils étaient avec DX9. Cette séparation des Shaders en trois catégories bien distinctes peut sembler surprenante aux personnes qui seraient plus familières avec les Shaders RenderMan mais finalement cette séparation forte des opérations qui peuvent être effectuées dans un type de «Shader» ou un autre permet de coller au plus près au mode de fonctionnement des cartes graphiques en elle-même encore aujourd’hui.

Après cette présentation du pipeline en lui-même, voyons un peu plus précisément comment par le biais de DirectX nous pouvons interagir avec un élément ou un autre de ce pipeline.

L’objet Device

Cela ne sera pas une surprise pour les personnes familières avec DX9 mais le composant principal qui représente au niveau de l’API la carte graphique est l’objet Device. Avant de commencer à travailler avec DirectX, il est nécessaire d’acquérir une référence valide sur un objet de ce type. Cet objet sera la base des autres communications que nous pourrons avoir avec la carte par le biais de l’API. Dans le cas de DirectX 10 une fois que l’on a acquis un Device, il est nécessaire d’associer celui-ci explicitement à une ‘Swap Chain’ pour obtenir un résultat visible à l’écran.

Les tutoriaux qui accompagnent le SDK sont intéressants car ils mettent bien en valeur ces étapes. Regardons par exemple le code ci-dessous extrait du tutorial traitant de la création du Device:

    DXGI_SWAP_CHAIN_DESC
sd;
 ZeroMemory(
&sd, sizeof(sd) );

sd.BufferCount =
1;

sd.BufferDesc.Width = g_width;

sd.BufferDesc.Height = g_height;

sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

sd.BufferDesc.RefreshRate.Numerator = 60;

sd.BufferDesc.RefreshRate.Denominator = 1;

  sd.BufferUsage =
DXGI_USAGE_RENDER_TARGET_OUTPUT;

  sd.OutputWindow =
g_hWnd;


sd.SampleDesc.Count = 1;

sd.SampleDesc.Quality = 0;

 sd.Windowed =
TRUE;

hr = D3D10CreateDeviceAndSwapChain(
NULL, D3D10_DRIVER_TYPE_REFERENCE, NULL, createDeviceFlags, D3D10_SDK_VERSION,
&sd, &g_pSwapChain, 
&g_pd3dDevice );

if( FAILED(hr) )

return hr;
// Create a render target view

ID3D10Texture2D
*pBuffer;

 hr =
g_pSwapChain->GetBuffer( 0, __uuidof(
ID3D10Texture2D ), (LPVOID*)&pBuffer );

if( FAILED(hr) )

return hr;

hr =
g_pd3dDevice->CreateRenderTargetView( pBuffer, NULL,
&g_pRenderTargetView );


pBuffer->Release();

if( FAILED(hr) )

return hr;

  g_pd3dDevice->OMSetRenderTargets( 1, &g_pRenderTargetView, NULL );

// Setup the viewport

D3D10_VIEWPORT vp;

 vp.Width = g_width;

  vp.Height = g_height;

vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;

vp.TopLeftX = 0;

vp.TopLeftY = 0;

g_pd3dDevice->RSSetViewports( 1, &vp );

On y voit en particulier la création d’une structure DXGI_SWAP_CHAIN_DESC qui décrit les propriétés de la Swap Chain avec laquelle on souhaite travailler, l’appel D3D10CreateDeviceAndSwapChain qui permet à la fois de créer l’objet Device et de l’attacher à la Swap Chain. Pour finir le travail nécessaire pour obtenir un résultat visible à l’écran, on voit par ailleurs qu’il est nécessaire de créer une ressource de type texture qui sera ensuite associée à la dernière étape de notre pipeline, l’Output Merger (OM) qui en sortie, émet le rendu de notre scène. Ceci est effectué par le biais de l’appel OMSetRenderTargets après avoir indiqué que nous souhaitons utiliser le miplevel 0 de la texture en question comme cible pour nos rendus.

Cette notion de créer une ressource pour contenir nos rendus est d’une certaine manière spécifique à DX10 dans le sens ou avec DX9 il n’était pas nécessaire de créer explicitement une ressource et de l’associer à la dernière étape de notre pipeline.

Après quelques temps, ces opérations apparaissent comme extrêmement naturelles. Il est intéressant de noter que le nom même des fonctions utilisées promeut une meilleure connaissance du pipeline que précédement, la plupart des noms de ces fonctions sont pré-fixés avec le nom de l’élément du pipeline avec lequel elles vont intérargir.

Dans le code ci-dessus, on voit aussi la création du Viewport que l’on spécifit par l’appel à la fonction RSSetViewports dont le nom est préfixé par RS puisque le Viewport est utilisé par le Rasterizer.

Un point qui est aussi extrêmement agréable avec cette nouvelle déclinaison de DirectX est le mode de gestion des ressources. Toutes les ressources sont finalement des zones de mémoire, DirectX permet d’avoir des vues différentes de ces zones de mémoire en fonction de l’usage à un moment donné que l’on souhaite faire de la ressource en question. C’est finalement comme si l’on pouvait donner à des moments différents des types différents à n’importe quelle ressource en fonction de la manière dont l’on souhaite l’utiliser.

Pour illustrer ce point, revenons sur ID3D10Texture2D. Dans l’exemple ci-dessus, nous avons créer une ressource de ce type à partir de la Swap Chain puis nous avons mappé le mipmap level 0 de cette ressource à une ressource de type RenderTargetView. Dans le code ci-dessous, nous créons une ressource du même type à partir d’un fichier préexistant puis nous créons une ShaderResourceView à partir de celle-ci. Cette vue de notre ressource peut alors être passée à un Shader. Regardons le code du tutorial 7:


// Load
the Texture

  ID3D10Resource
*pRes = NULL;

 hr =
D3DX10CreateTextureFromFile( g_pd3dDevice, L"seafloor.dds",
NULL, NULL, &pRes );

if( FAILED(hr) )

return
hr;

pRes->QueryInterface(__uuidof(
ID3D10Texture2D ), (LPVOID*)&g_pTexture );
pRes->Release(); 
D3D10_TEXTURE2D_DESC desc;

  
g_pTexture->GetDesc( &desc );

  
D3D10_SHADER_RESOURCE_VIEW_DESC SRVDesc;

 ZeroMemory(
&SRVDesc, sizeof(SRVDesc) );

 SRVDesc.Format =
desc.Format;

 
SRVDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;

SRVDesc.Texture2D.MipLevels = desc.MipLevels;
   hr =
g_pd3dDevice->CreateShaderResourceView( g_pTexture, &SRVDesc,
&g_pTextureRV );

if( FAILED(hr) )

return hr;

On voit bien qu’une seule et même ressource va pouvoir être associée à différentes étapes de notre pipeline en fonction des besoins de manière extrêment flexible. Par ailleurs, les GPU étant de plus en plus utilisés pour d’autres types de traitements que l’affichage d’une scène 3D, les textures étaient utilisées de plus en plus souvent simplement pour passer des données à la carte. Avec DX10, les ressources qui peuvent être manipulées par le GPU sont mieux reconnues comme pouvant être juste des suites d’informations qui peuvent être à un moment donné vues et manipulées par différentes parties du pipeline. Avec ce nouveau paradigm le système des ressources devient de façon plus évidente très flexible et générique.

Concernant l’objet Device, il est nécessaire de mentionner que si auparant il était possible de passer par cet objet pour spécifier un certain nombre de variables d’états du Fixed Function Pipeline. Celui-ci ayant disparu avec DirectX 10, les fonctionnalités associées à l’objet Device sont plus synthétiques.

Le choix de supprimer le FixedFunction Pipeline peut surprendre dans un premier temps puisque tout le monde ne souhaite pas nécessairement utiliser les fonctionnalités des Shaders et le FixedFunction Pipeline (FFP) présentait une solution intéressante dans ce cas là. Ce choix a été effectué car dans les faits, le FFP n’avait plus d’existence réelle au niveau des cartes. Par ailleurs, le SDK s’accompagne d’un exemple complet de shaders implémentant les fonctionnalités du FFP qu’il est possible d’utiliser sans modifications lorsque l’on ne souhaite pas réaliser ses propres Shaders. L’avantage au niveau de l’API est d’avoir un mode d’intéraction avec le GPU beaucoup plus synthétique et consistant. Avec DX9, la présence à la fois du FFP et du système d’effet avec ces «Shaders» avaient engendré un alourdissement considérable du nombre de propriétés et de fonctions différentes associées à différents composants DirectX. Par ailleurs, dans une seule et même application si l’on souhaitait utiliser le FFP pour certaines géométries et des «Shaders» pour d’autres éléments, on se trouvait finalement avec des portions de code assez différentes pour atteindre un but pourtant similaire c'est-à-dire l’affichage d’objets 3D.

Pour revenir sur notre code manipulant la texture crée à partir d’un fichier, le code ci-dessous permet finalement d’associer celle-ci à une variable reconnue par notre shader :

g_pDiffuseVariable->SetResource(
g_pTextureRV );

Pour bien comprendre cette association, il faut aussi préciser comment est définie notre variable g_pDiffuseVariable :

ID3D10EffectShaderResourceVariable*
g_pDiffuseVariable = NULL;

Et voir comment nous avons de fait récupéré une référence valide pour celle-ci en utilisant les fonctionnalités de reflection du système d’effet de DirectX 10 que nous décrirons plus loin:


g_pDiffuseVariable =g_pEffect->GetVariableByName("txDiffuse")->AsShaderResource();

txDiffuse étant lui même défini dans notre fichier d’effet de la façon suivante :

Texture2D txDiffuse;

Pour l’instant cette dernière étape n’apparait peut-être pas encore nécessairement comme limpide mais le sens réel de ces deux dernières étapes va se révéler plus clairement dès que nous aurons décrit le système d’effet, ce que nous allons faire maintenant.

(Par ailleurs même si j’insère des portions de code dans ce document, je recommande de garder un œil dans visual studio sur les projets des tutoriaux ou sur le code de l’application d’exemple. Cela donne une meilleur vue d’ensemble du code et montre bien où les portions précises incluses ici s’insèrent).

The effect system

Comme nous venons de le voir ci-dessus pour expliquer comment une texture peut être utilisée dans un de nos shaders, lui-même défini dans notre fichier d’effet, nous avons utilisé des fonctionnalités du système d’effet de DX10. Il me paraissait important de donner ci-dessus l’ensemble du code nécessaire pour utiliser une texture même si le système d’effet en lui-même n’avait pas encore été décrit. Mais regardons maintenant celui-ci plus en détails.

Nous avons vu jusqu’à présent comment les ressources pouvaient être crées, comment l’on pouvait obtenir des vues de type précis de ces même ressources et comment ces vues pouvaient elles même être associées au pipeline en différents points. Dans le dernier exemple, cette association a été effectuée en utilisant des fonctionnalités offertes par le système d’effet accompagnant DX10. Je considère ici que le lecteur est familier avec la notion de fichier d’effet et de shader, si ce n’est pas le cas il peut être utile de se reporter à l’article suivant http://www.microsoft.com/france/msdn/directx/Shaders-avec-Managed-DirectX_1.mspx ou à un des excellents ouvrages existant sur le sujet.

Avant de détailler les fonctionnalités du système d’effet, regardons d’abord un peu comment justement charger un tel fichier d’effet:

// Create the effect

   hr = D3DX10CreateEffectFromFile( L"default.fx",
NULL, NULL, D3D10_SHADER_ENABLE_STRICTNESS, 
0, g_pd3dDevice, NULL, NULL,
&g_pEffect, NULL );

if( FAILED( hr ) )
    {
          MessageBox(
NULL, L"The FX file cannot be located.  Please
run this executable from the directory that contains the FX file.",
L"Error", MB_OK );

return hr;

   }
   

Comme je l’ai mentionné il n’y a plus de FFP donc maintenant on manipule en fait des shaders en permanence. Le système d’effet présent dans DX9 a été remanié en profondeur et devient maintenant une brique essentielle au cœur de DirectX. En particulier, ce système d’effets s’accompagne de fonctionnalités de réflexion permettant de créer une association entre des interfaces ou des adresses mémoire accessibles par le CPU et des constantes ou des ressources connus de nos effets et shaders et manipulables par le GPU.

C’est en fait exactement ce qui est fait dans l’appel:

g_pDiffuseVariable =g_pEffect->GetVariableByName(
"txDiffuse")->AsShaderResource();

Cet appel crée une association entre la ressource txDiffuse déclarée dans notre ficher d’effet et le pointeur g_pDiffuseVariable. Celui-ci étant un pointeur sur une interface ID3D10EffectShaderResourceVariable qui nous permet l’appel:

g_pDiffuseVariable->SetResource(
g_pTextureRV );

Ayant pour but de spécifier que txDiffuse référence la vue de notre ressource que nous avions dans g_pTextureRV.

Regardons maintenant d’autres exemples (extrait du tutorial 7, il peut être intéressant de regarder le code de ce tutorial en parallèle à la lecture de ce document) d’utilisation des fonctionnalitées de réflexion de l’API DirectX 10:

// Obtain the technique
    g_pTechnique =
g_pEffect->GetTechniqueByName( "Render"
);
// Obtain the variables
   g_pWorldVariable
= g_pEffect->GetVariableByName( "World"
)->AsMatrix();
   g_pViewVariable =
g_pEffect->GetVariableByName( "View"
)->AsMatrix();
  g_pProjectionVariable
= g_pEffect->GetVariableByName( "Projection"
)->AsMatrix();

g_pMeshColorVariable = g_pEffect->GetVariableByName( 
"vMeshColor" )->AsVector();



g_pDiffuseVariable = g_pEffect->GetVariableByName( "txDiffuse"
)->AsShaderResource();


// Define the input layout

D3D10_INPUT_ELEMENT_DESC layout[] =
   {
    { 
    "POSITION", 0,
DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },  
   { 
"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,
0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 }, 
  };
   UINT numElements
= 
sizeof(layout)/
sizeof(layout[0]);



// Create the input layout
 D3D10_PASS_DESC
PassDesc;

g_pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );
   hr =
g_pd3dDevice->CreateInputLayout
( layout, numElements, PassDesc.pIAInputSignature,
PassDesc.IAInputSignatureSize, &g_pVertexLayout );
if( FAILED(hr) )
return hr;
// Set the input layout
 g_pd3dDevice->IASetInputLayout(
g_pVertexLayout );

On voit que par le biais du même mécanisme décrit pour notre texture, il est possible de créer des associations qui permettent de communiquer des valeurs, des matrices à nos effets.

En dessus des appels à GetVariableByName, on voit par ailleurs une étape extrêmement importante dans l’utilisation d’un fichier d’effet, il s’agit de la spécification du format des données qui vont être envoyées en entrée de notre pipeline à l’input assembler. Ce format est spécifié par le biais de l’appel à la fonction IASetInputLayout mais la description de ce format est, elle créée, en s’appuyant à la fois sur un passe bien précise d’une technique contenu dans notre fichier d’effet et sur une structure D3D10_INPUT_ELEMENT_DESC visible plus haut dans notre code. Cette étape effectuée par la fonction CreateInputLayout permet de vérifier l’adéquation entre ce que nous précisons dans note code C++ et ce qui de l’autre côté peut être consommé par les shaders contenu dans le fichier d’effet.

Une telle vérification n’est effectuée qu’à ce moment là. On voit que dans notre structure nous utilisons des sémantiques du type POSITION, TEXTURE NORMAL. Avec DX9 l’ordre de ces composantes tel que spécifié côté C++ et côté effet pouvait être différent. Le shader récupérait la valeur associé à une sémantique donnée, maintenant l’ordre doit être strictement le même dans les deux cas, le shader va récupérer les valeurs de ces attributs en fonction de leur position. Ce changement permet d’obtenir de meilleurs résultats en terme de performance mais une erreur d’ordre peut avoir des répercutions importantes alors que ce n’était pas le cas avec DX9.

Pour être complet au sujet des GetVariableByName, regardons ce qui est défini dans le fichier d’effet:

 


Texture2D txDiffuse;

SamplerState samLinear

{

    Filter = MIN_MAG_MIP_LINEAR;

    AddressU = Wrap;

    AddressV = Wrap;

};
cbuffer cbNeverChanges

 

{

    matrix View;

};

 

cbuffer cbChangeOnResize

{

    matrix Projection;

};

 

cbuffer cbChangesEveryFrame

{

    matrix World;

    float4 vMeshColor;

};

Ces déclarations sont groupées par cbuffer. Comme indiqué dans la première partie de ce document, un effort important a été réalisé pour fournir les données à la carte par block avec une granularité adaptée pour obtenir les meilleures performances possibles (en minimisant le nombre d’échanges). On voit ci-dessus que le programmeur a finalement, tout du moins pour les variables utilisées par les shaders, la main pour choisir le niveau de granularité qui lui parait le plus approprié en fonction de son applications et des valeurs qu’il doit transmettre à la carte. Ici, le choix a été fait de regrouper les valeurs par blocs de valeurs qui doivent être mis à jour avec la même fréquence et au même moment, c’est sans doute souvent le choix qui sera fait. Il est évident comme nous l’avons dit que communiquer un ensemble d’information à la carte au lieu d’effectuer une communication pour chaque information isolé peut résulter en des gains de performance importants. Bien sûr avec la possibilité de définir soi-même les groupes de valeurs qui vont être transmis en même temps, des erreurs peuvent être commises et se révéler contre productives. En cas de changement de l’application, il peut être intéressant de revenir régulièrement sur la définition de ces blocs pour vérifier que les groupements qui ont initialement été effectués sont toujours appropriés. Un exemple de mauvaise pratique serait d’avoir un cbuffer d’une taille conséquente au sein duquel une et une seule valeur serait modifiée très régulièrement. A ce moment là pour communiquer cette unique valeur notre gros blocks de donné serait systématiquement transmis.

On dispose d’un maximum de 4096 constant buffers. Par ailleurs, toute variable qui serait définie en dehors du cadre d’un de ces buffer se retrouverait instantanément dans un constant buffer global ($Global).

On ne peut pas avoir de constant buffer imbriqués. Il est possible de spécifier explicitement à quels registres vont être associés ces buffers. Le maximum de buffers possibles par shader est de 16.

Nous avons vu que l’utilisation de ces buffers peut potentiellement résulter en une amélioration significative des performances. Un autre point ayant traît aux performances ne concerne que le cas de figure où l’on utilise le système d’effet (un exemple du SDK montre que l’on peut aussi utilisé DirectX sans le système d’effet même s’il est peut probable que cela se révèle intéressant dans la mesure ou le système d’effet fait maintenant partie des composants centraux de DirectX, il s’agit de l’exemple intitulé HLSL without Effects). En effet lorsque l’on met à jour une valeur d’un constant buffer, le système d’effet buferise ce changement jusqu’à un appel à Apply pour une passe contenu dans une technique définie dans un effet donné (tout du moins dans la version Oct06 du SDK).

Un autre mécanisme du système d’effet est intéressant à connaitre dans la perspective d’éviter au maximum des échanges d’information redondants avec la carte vidéo. Il est possible de définir des pools d’effet. Lorsque cela est effectué, les ressources (textures ou autres) et les blocs d’états (j’en parle un peu, plus dans la suite du document) peuvent être signalés comme étant partagées entre les différents effets contenu dans un même pool.

Les diagrammes ci-dessous extrait de FX10: Driving the New Effects System by Relja Markovic played at GameFest 06 illustrent parfaitement ce qui se passe en mémoire:

Ci-dessus, on visualise le graphe de dépendances pour un effet et ses ressources.

Ci-dessous, il s’agit des graphes pour deux effets différents qui utilisent des ressources identiques mais ne se trouvent pas dans le même pool d’effets:

Et enfin voici le graphe de dépendances pour deux effets qui partagent des ressources et qui se trouvent de surcroit dans le même pool d’effets:

Il est de fait recommandé d’utiliser au maximum les pools d’effets même si initialement vos effets ne partagent pas de ressources.

Un dernier point, j’ai mentionné plus haut qu’il était possible d’utiliser DirectX sans utiliser le système d’effet. Les fonctions { VS | GS | PS }SetConstantsBuffer que vous trouvez décrites dans la documentation et dans certains documents sur DirectX 10 ne peuvent être utilisées que dans ce cadre là. Le système d’effet garde une trace des constants buffers qui doivent être mis à jour ou non pour un effet donné, utiliser les fonctions ci-dessus en conjonction avec le système d’effet peut très rapidement et assez sûrement créer un chaos inextricable. Par ailleurs, le système d’effet est maintenant un composant central de DirectX 10, destiné à adresser tous le cas de figure.

Le shader model 4.0

Jusqu’à présent nous avons parlé d’effet et de shaders sans réellement mentionner aucune information au sujet du langage qui est utilisé pour exprimer ceux-ci. Avec DirectX 10, les shaders peuvent être écrit uniquement en HLSL (High Level Shading Langage). Avec DX9, ils étaient aussi possible de spécifier ces programmes en langage assembleur. Néanmoins ce langage ne correspondait pas non plus au code réellement consommé par les GPU et il a été jugé inutile de garder deux représentations intermédiaires différentes. De fait, il est toujours possible de visualiser une telle représentation intermédiaire mais uniquement à des fins de débogage.

Il est déconseillé d’utiliser ce mode de visualisation des shaders dans l’espoir d’améliorer les performances d’un shader exprimé en HLSL. Cette représentation en langage assembleur destinée à l’analyse de problème peut apparaitre comme inutilement lourde et non optimisée si on la consulte dans ce but et finalement fournira peu d’informations valides sur les performances d’un shader donné par rapport à un shader écrit différemment et provoquant les même effets.

Entre autre chose, on peut mentionner que le langage HLSL est maintenant dit unifié entre les différents types de shaders. Même si ceux-ci gardent leurs spécificités en terme de données sur lesquels ils peuvent travailler et au niveau des opérations qu’ils peuvent effectuer, le langage pour exprimer tout cela est maintenant le même d’un type de shader à un autre.

Pour plus d’informations sur les instructions supportées, il est préférable de se reporter à la documentation du SDK qui en fourni une liste exhaustive.

Les choses auxquelles il faut faire attention lorsque l’on est habitué à DX9

J’ai déjà mentionné certains de ces points mais il n’est pas inutile de les mentionner à nouveau. Prenons le vertex shader défini ci-dessous:

struct VS_INPUT

{

    float4 Pos : POSITION;

    float2 Tex : TEXCOORD;

    float4 Norm : NORMAL;

};

 

//-----------------------------------------------------------

// Vertex Shader

//-----------------------------------------------------------

PS_INPUT VS( VS_INPUT input )

{

    PS_INPUT output = (PS_INPUT)0;

    output.Pos = mul( input.Pos, World );

    output.Pos = mul( output.Pos, View );

    output.Pos = mul( output.Pos, Projection );

    output.Tex = input.Tex;

    

    output.Norm = mul( input.Norm, World );

    output.Norm = mul( output.Norm, View );

    output.Norm = mul( output.Norm, Projection );

 

    return output;

}

Dans notre code C++ nous allons créer l’association avec la structure suivante:

D3D10_INPUT_ELEMENT_DESC layout[] =

    {

     { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 
     0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },  

     { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 
     0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },

     { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 
     0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 }, 

    };



L’association ci-dessus va créer de gros problèmes. Avec DX9, en fonction de la sémantique, le système d’effet allait récupérer les valeurs associées à une sémantique quelque soit l’ordre dans lequel elles étaient définies. Avec DX10 ce n’est plus le cas, un effort important a été fourni pour supprimer toutes les opérations qui n’étaient pas strictement nécessaires. Dans le cas ci-dessus avec DX10, les valeurs pour notre normal vont être utilisées comme coordonnées de texture… Rien ne va échouer de manière spectaculaire mais il est probable que le résultat à l’écran ne le soit pas non plus (tout du moins dans le sens ou nous aimerions qu’il le soit).

Deuxième changement de taille qui pourra surprendre des développeurs DX9 endurcis, c’est l’absence des informations concernant les fonctionnalités supportées par une carte graphique (les Caps). En contre partie, toute carte DX10 supporte finalement les fonctionnalités offerte par l’API. Bien sûr les différences de performances entre différents niveaux de gamme de carte graphique ou en fonction de la mémoire disponible vont rester très importantes de sorte que comme aujourd’hui, il peut être judicieux d’utiliser un système pour afficher quelque chose à l’écran plutôt qu’un autre en fonction de la carte présente sur la machine. Pour effectuer ce genre de choix, il était déjà conseillé de ne pas le faire en s’appuyant sur les capacités exprimées par la carte en termes de fonctionnalités supportées par celle-ci mais d’effectuer des tests de vitesse de rendu en utilisant un système ou un autre pour décider lequel se comporte le mieux. Finalement aujourd’hui c’est la solution qui reste, c'est-à-dire de comparer les vitesses d’affichage d’un système ou d’un effet par rapport à une autre pour décider celui qui va être retenu. Cela revient à avoir une sorte de «profiler» intégré à son application. Bien sûr cela représente un travail supplémentaire d’effectuer ce genre de tests mais si l’application est destinée à tourner sur une grande variété de cartes différentes et si l’on souhaite investir suffisamment pour développer plusieurs codes différents pour gérer l’affichage pour adapter celui-ci à certaines familles de cartes, à ce moment ce petit «profiler» intégré est finalement la seule solution raisonnable pour effectuer des choix entre les différents chemins de codes possibles.

Sinon, que l’on ait un passé de développeur DX9 ou non, il est finalement utile d’être pour l’instant prudent quand à l’utilisation qui est envisagée pour le GS. Pour l’instant il est difficile de se faire une idée des performances qu’ils pourront avoir si on leur demande d’émettre ou de supprimer des primitives par rapport à ce qui a été envoyé dans le pipeline. Aussi pour l’instant il peut être plus prudent de ne pas tout miser sur ce type de fonctionnalités. Ce n’est pas nécessairement que les performances seront mauvaises mais il s’agit essentiellement d’une inconnue à ce jour.

Dans le même registre, il est certes possible de modifier des constants buffers par primitive mais il s’agit peut être d’une pratique un peu extrême.

DrawAuto est une fonctionnalité très intéressante (la possibilité de réinjecter le résultat de la première partie du pipeline sans exécuter les étapes de rastérisation et les pixels shaders et sans intervention du CPU) mais là encore il est impossible d’avoir beaucoup de recul aujourd’hui sur les performances qui seront obtenues avec ce système. D’autant plus qu’il est extrêmement tentant d’utiliser justement cette fonctionnalité de pair avec la possibilité de créer ou supprimer des primitives au sein du GS. Il est relativement plus aisé de penser à des usages possibles de ces mécanismes que d’anticiper comment ils vont se comporter dans le cadre d’opérations à effectuer plus ou moins en «temps réel». Par contre ces possibilités sont de fait d’ores et déjà présentes dans l’API.

Variables d’état du Device

J’ai mentionné plus haut qu’il n’y avait plus de FFP et que les variables d’états associés avaient disparues, ce qui est exact (J), mais cela ne veut pas dire pour autant qu’il n’y a plus du tout de variables d’état associées au Device qui affecte le pipeline. L’état du rendering pipeline continue d’être contrôlé par des «state blocks». Le turorial 14 illustre ce point à merveille. Par ailleurs dans l’exemple de code DX10 que nous allons voir ci-dessous, nous utilisons aussi des variables d’état pour contrôler les opérations de blending entre le contenu du render target et de la contribution d’une primitive donnée.

Je pense avoir couvert un certain nombre de points importants de DirectX 10. Il ne s’agit pas d’une liste exhaustive des fonctionnalités ou de ce qui a pu changer par rapport à la version précédente mais je pense avoir mentionné un certain nombre de choses qu’il est intéressant de connaitre lorsque l’on souhaite aborder cette nouvelle déclinaison du SDK DirectX. Je fais souvent référence dans la partie ci-dessus aux tutoriaux et aux exemples contenus avec le SDK, ceux-ci sont en effet très didactiques et offrent une aide précieuse lors de la prise en main du SDK. Je ne peux conseiller de s’y reporter souvent pour découvrir plus complètement les possibilités de ce SDK.

Application d’exemple

Passons maintenant à la description d’un exemple qui va nous permettre de revisiter un peu plus en profondeur ou tout du moins sur un cas concret un certain nombre des points qui ont été vu jusqu’ici.

Dans la grande majorité des cas pour afficher un objet en trois dimensions il est suffisant d’utiliser une représentation géométrique qui décrit essentiellement la surface qui le sépare l’intérieur de l’objet de l’extérieur. Cette façon de faire n’est pas nécessairement adaptée à tout les besoins. Sous l’appellation «Volume Rendering», on regroupe un certain nombre de techniques qui permettent justement d’afficher des données volumiques. L’exemple de code que nous allons ici détailler s’attache à afficher à l’écran des données volumiques contenues dans une texture en trois dimensions. Cet exemple de code reste très simple, nos données volumiques sont contenues dans un cube et nous allons quadriller ce cube de triangles pour que la couleur en chaque point de l’écran soit le résultat de la contribution des données de notre texture en 3D dimension aux points de coupe par nos triangles. En l’occurrence, nous allons utiliser le GS pour faire générer par notre GPU un certain nombre de ces triangles. L’orientation des triangles en question fait que la technique utilisée ici n’est pas nécessairement adaptée à certains besoins comme l’imagerie médicale ou scientifique où il peut être nécessaire d’attacher un plus grand soin au choix de la géométrie que l’on va utiliser comme support pour obtenir quelque chose à l’écran de manière à limiter des effets de perspective. Par exemple, on pourra souhaiter que notre cube soit coupé par des surfaces sphériques dont la normale est alignée avec la ligne que l’on peut créer entre la position de la caméra et le point de la surface en question (et du cube), de sorte que l’opération de «blending» entre la couleur de chaque sphère qui contribue à la couleur du pixel final corresponde à une intégration avec un pas uniforme sur toutes les lignes allant de la caméra à un point du far plane.

Dans notre cas où nous avons des triangles qui ne sont pas nécessairement ordonnés d’une manière particulière cela correspond finalement à faire une intégration numérique avec une valeur de pas différente en fonction de l’orientation et de la position du cube et du point de l’écran que l’on considère, ce qui n’est pas «exact». Par ailleurs, toujours pour la visualisation médicale ou scientifique, les données volumiques disponibles ne sont pas nécessairement des données qui doivent avoir une traduction visuelle directe et l’on souhaite souvent contrôler la façon dont ces données contribuent à ce que l’on souhaite visualiser ou non. De fait, un champ entier de recherche est dédié à déterminer des fonctions de transfert, appropriées à chaque domaine, qui permettent de visualiser des informations intéressantes à partir de données volumiques brutes issues du domaine en question. Ceci étant dit, notre propos ici, n’est pas de répondre à un besoin précis de ce type mais plutôt d’illustrer de la manière la plus synthétique possible l’utilisation de l’API DirectX 10, aussi l’exemple de code présenté ici permet de visualiser un élément décrit par des données volumiques plutôt que par des informations concernant sa surface.

Dans notre cas nous allons en fait visualiser les données résultats d’un scan 3D. Le fichier de données initiales se trouve sur le site suivant http://www.volvis.org . Pour obtenir le résultat visible dans les captures d’écran visible à la fin de ce document, il vous faudra récupérer ce fichier (bonzai.raw.gs qui contient bonzai.raw), en effet il n’est pas inclus dans le fichier .zip contenant les sources et le projet Visual Studio d’exemple décrit ici.

La densité et la précision du résultat visible à l’écran augmentent avec le nombre de primitives utilisées pour produire ce résultat. C’est pour cela que l’idée de générer ces géométries à l’aide du GPU est séduisante. En effet, cela laisse espérer de pouvoir générer plus de géométrie grâce à celui-ci au moment où les cartes DX 10 seront disponibles. Dans notre cas, nous utilisons le référence Device qui est une implémentation sur les CPU des fonctionnalités d’un carte 3D, aussi les performances ne sont pas très bonnes à ce jour mais cet exemple de code nous fourni toutefois un alibi pour donner un exemple de GS.

Récapitulons donc un peu ce que notre exemple va effectuer exactement.

  • Tout d’abord, notre application doit bien sûr effectuer les initialisations nécessaires pour que l’on puisse utiliser DirectX puis visualiser un résultat à l’écran (création du Device…).

  • Ensuite, nous allons construire la texture en 3D qui va contenir les données que nous souhaitons visualiser.

  • Nous devons aussi créer les primitives graphiques que nous allons utiliser comme support pour afficher les données contenues dans notre texture en trois dimensions (après tout notre GPU ne comprend qu’un nombre réduit de type de primitives différentes, à savoir des points, des lignes et des triangles, l’affichage d’une texture en trois dimensions sans le support de telles primitives n’a pas encore de sens).

  • Enfin, nous devons effectuer tout le travail nécessaire pour communiquer à notre Device toutes les informations nécessaires au rendu et provoquer celui-ci. Entre autre chose nous devons spécifier à la carte quel effet et quelle technique puis quelle passe de cette technique (et donc quels shaders) nous souhaitons utiliser pour calculer le résultat de l’affichage. Nous devons aussi envoyer un minimum de données au pipeline et donner l’ordre à celui-ci de commencer ses traitements.

Ces différentes opérations vont être respectivement réalisées dans notre code dans les fonctions suivantes:

  • InitDevice

  • Build3DData

  • MakeSlices

  • Render

Notre application contient aussi les fonctions ci-dessous qui, elles sont plus directement liées au fonctionnement d’une application windows qu’à DirectX, aussi je ne les détaillerai pas:

  • WndProc

  • wWinMain

  • InitWindow

InitDevice nous permettra donc de revoir le code nécessaire à la création du Device et de la swap chain auquel il sera associé, la définition du viewport et l’envoi de celui-ci au rasterizer. Dans cette fonction, nous chargerons aussi notre fichier d’effet pour que soit directement accessible dans les autres fonctions, les fonctions de réflexion du système d’effet, ce qui nous permettra au fil de l’eau de créer les associations entre les ressources que nous créons et ce qui est utilisé dans les shaders de notre système d’effet.

Build3DData nous montrera comment créer une resource de type texture3D comment générer de façon procédurale des valeurs pour le contenu de cette texture puis comment créer à partir de celle-ci, une vue de cette ressource que nous allons «binder» à notre pipeline en précisant la façon dont elle doit être associée à des ressources que les shaders référencent dans leur code (bon c’était une phrase un peu longue mais j’espère que le cheminement est clair).

Enfin dans MakeSlices nous allons construire effectivement les primitives que nous allons utiliser pour obtenir quelque chose à l’écran (en l’occurrence une liste de triangles).

Maintenant que nous avons décrit notre application dans ses grandes lignes, nous pouvons aller plus dans le détail de chaque fonction.

InitDevice

Par essence, cette fonction effectue souvent le même type d’opérations quelque soit l’application. Il s’agit certainement du code le moins spécifique à une application donnée, ce que nous voyons ci-dessus est donc redondant avec ce qui a été vu précédemment.

Deux structures sont créées et contiennent les informations concernant la surface d’affichage et la swap chain que l’on désire, ensuite on s’appuit sur D3D10CreateDeviceAndSwapChain pour obtenir à la fois notre Device et l’attacher à la swap chain (il est possible d’effectuer ces opérations indépendement l’une de l’autre par exemple si l’on souhaite attacher un device donné à plusieurs swap chain mais ici comme nos besoins restent simples, cette fonction D3D10CreateDeviceAndSwapChain nous évite de réaliser explicitement chaque étape).

Ensuite, comme il a été décrit dans la partie précédente on crée une ressource de type Texture2D et on en extrait une vue que nous allons associer avec la dernière étape de notre pipeline, l’output merger. Ceci assure que le résultat des traitements effectués dans notre pipeline se retrouve stocké dans cette ressource qui est utilisé par la swap chain pour présenter le résultat à l’écran:

//--------------------------------------------------


// Create Direct3D device and swap chain

//---------------------------------------------------



HRESULT InitDevice()

{

    HRESULT hr;

 

    RECT rc;

    GetClientRect( g_hWnd, &rc );

    UINT width = rc.right - rc.left;

    UINT height = rc.bottom - rc.top;

 

    UINT createDeviceFlags = 0;

#ifdef _DEBUG

    createDeviceFlags |= D3D10_CREATE_DEVICE_DEBUG;

#endif

 

    DXGI_SWAP_CHAIN_DESC sd;

    ZeroMemory( &sd, sizeof(sd) );

    sd.BufferCount = 1;

    sd.BufferDesc.Width = width;

    sd.BufferDesc.Height = height;

    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

    sd.BufferDesc.RefreshRate.Numerator = 60;

    sd.BufferDesc.RefreshRate.Denominator = 1;

    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

    sd.OutputWindow = g_hWnd;

    sd.SampleDesc.Count = 1;

    sd.SampleDesc.Quality = 0;

    sd.Windowed = TRUE;

 

    hr = D3D10CreateDeviceAndSwapChain
    ( NULL, D3D10_DRIVER_TYPE_REFERENCE, 
     NULL, createDeviceFlags, 
     D3D10_SDK_VERSION, &sd, 
     &g_pSwapChain, &g_pd3dDevice );

    if( FAILED(hr) )

        return hr;

 

    // Create a render target view

    ID3D10Texture2D *pBuffer;

    hr = g_pSwapChain->GetBuffer
    ( 0, __uuidof( ID3D10Texture2D ), (LPVOID*)&pBuffer );

    if( FAILED(hr) )

        return hr;

    hr = g_pd3dDevice->CreateRenderTargetView
    ( pBuffer, NULL, &g_pRenderTargetView );

    pBuffer->Release();

    if( FAILED(hr) )

        return hr;

 

    g_pd3dDevice->OMSetRenderTargets
    ( 1, &g_pRenderTargetView, NULL );

Ci-dessous nous définissons aussi le ViewPort et nous l’attachons  au rasterizer où il est utilisé pour déteminer les portions de primitives qu’il faut ou non clipper.

  // Setup the viewport

    D3D10_VIEWPORT vp;

    vp.Width = width;

    vp.Height = height;

    vp.MinDepth = 0.0f;

    vp.MaxDepth = 1.0f;

    vp.TopLeftX = 0;

    vp.TopLeftY = 0;

    g_pd3dDevice->RSSetViewports( 1, &vp );

Ensuite toujours dans InitDevice, nous allons chargé notre fichier, la fonction D3D10CreateEffectFromFile va aussi passer notre fichier d’effet et compiler les shaders qu’il contient. Il est donc intéressant de récupérer dans pErrBlob des informations riches sur les erreurs qui pourrait apparaitre pendant ces traitements. Si cela n’est pas effectué nous serons juste notifié de l’échec de l’opération par le biais du HRESULT hr, ce qui est assez pauvre comme information pour savoir exactement quel a été le problème qui a empêché la création de l’effet. En l’occurrence en utilisant le pErrBlob, on récupère entre autre chose une string qui par exemple peut contenir un descriptif exact de l’erreur de compilation d’un shader et la ligne du fichier d’effet sur laquelle cette erreur a été rencontrée.

// Create the effect

       ID3D10Blob* pErrBlob = NULL; 

    hr = 
    D3DX10CreateEffectFromFile( L"DX10Cube.fx", NULL, 
    NULL, D3D10_SHADER_ENABLE_STRICTNESS, 0,
     g_pd3dDevice, NULL, NULL, &g_pEffect, &pErrBlob );

    if( FAILED( hr ) )

    {
      std::string errStr((LPCSTR)pErrBlob->GetBufferPointer(), 
      pErrBlob->GetBufferSize());

      WCHAR err[256];

      MultiByteToWideChar(CP_ACP, 0, errStr.c_str(),

      (int)errStr.size(), err, errStr.size());

      MessageBox( NULL, (LPCWSTR)err, L"Error", MB_OK );

        return hr;

    }

 

    // Obtain the technique

    g_pTechnique = g_pEffect->GetTechniqueByName( "Render" );

 

    // Obtain the variables

    g_pWorldVariable = g_pEffect->GetVariableByName( "World" )->AsMatrix();

    g_pViewVariable = g_pEffect->GetVariableByName( "View" )->AsMatrix();

    g_pProjectionVariable = g_pEffect->GetVariableByName( "Projection" )->AsMatrix();

       g_pCubeVertices = g_pEffect->GetVariableByName( "CubeVertices" )->AsVector();

       g_pNumSlicesEffVar = g_pEffect->GetVariableByName( "numSlices" )->AsScalar();

       g_pNumSlicesEffVar->SetInt(g_numSlices);

    g_pDiffuseVariable = g_pEffect->GetVariableByName( "txDiffuse" )->AsShaderResource();

       g_p3DData = g_pEffect->GetVariableByName( "tdData" )->AsShaderResource();

Ci-dessus on voit les appels au méthodes de réflection de notre système d’effet qui permettent d’associer des variables définies dans le fichiers d’effets avec des interfaces DX10 que nous pouvons utiliser à partir de notre code C++ pour fixer ces valeurs avant d’effectuer le rendu de primitives en utilisant une passe donnée, d’une technique contenu dans notre effet.

Une fois ces opérations effectuées, nous devons spécifier à l’entrée de notre pipeline, l’input assembler (IA) le format des données que nous allons lui passer.

Comme cela a été décrit précédement, nous nous appuyons pour cela sur la fonction CreateInputLayout à qui l’on spécifie une passe d’une technique de notre effet de sorte qu’elle puisse vérifier l’adéquation du format de donnée que nous allons passer à l’IA avec le format de donnée que les shaders associés à cette passe peuvent consommer. Une fois que nous avons un format dont à ce stade nous sommes sûr qu’il est valide nous pouvons appeler la méthode IASetInputLayout en lui passant le format en question. Notre fonction MakeSlices va ensuite générer des informations dans le format spécifié ci-dessous:

 // Define the input layout

    D3D10_INPUT_ELEMENT_DESC layout[] =

    {

        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 
        0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },  

        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 
        0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 }, 

    };

    UINT numElements = sizeof(layout)/sizeof(layout[0]);

  

    // Create the input layout

    D3D10_PASS_DESC PassDesc;

    g_pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );

    hr = g_pd3dDevice->CreateInputLayout
    ( layout, numElements, PassDesc.pIAInputSignature, 
    PassDesc.IAInputSignatureSize, &g_pVertexLayout );

    if( FAILED(hr) )

        return hr;

 

    // Set the input layout

    g_pd3dDevice->IASetInputLayout( g_pVertexLayout );

 
    // Initialize the world matrices

    D3DXMatrixIdentity( &g_World );

 

    // Initialize the view matrix

    D3DXVECTOR3 Eye( 0.0f, 3.0f, -6.0f );

    D3DXVECTOR3 At( 0.0f, 1.0f, 0.0f );

    D3DXVECTOR3 Up( 0.0f, 1.0f, 0.0f );

    D3DXMatrixLookAtLH( &g_View, &Eye, &At, &Up );

 

    // Initialize the projection matrix

    D3DXMatrixPerspectiveFovLH
    ( &g_Projection, 
    (float)D3DX_PI * 0.25f, width/(FLOAT)height, 0.1f, 100.0f );

 

    // Update Variables that never change

    g_pViewVariable->SetMatrix( (float*)&g_View );

    g_pProjectionVariable->SetMatrix( (float*)&g_Projection );

    g_pDiffuseVariable->SetResource( g_pTextureRV );

 

       MakeSlices();

       Build3DData();

 

    return S_OK;

}

Enfin ci-dessus, nous créeons nos matrices world, view et projection ce qui reste tout aussi nécessaire qu’avec des versions précédentes de l’API J. Pour plus d’informations sur le role de ces matrices on pourra se reporter au document: http://www.microsoft.com/france/msdn/directx/tutoriel/part1.mspx

(ou à tout autre tutorial traitant de la programmation 3D en fait).

Build3DData

Dans cet exemple, la fonction Build3DData génère juste une texture en trois dimensions qui contient des pixels dont les canaux rgb contiennent la valeur issue de la lecture de notre fichier contenant le résultat du scan du bonzaï. Typiquement, il serait plus intéressant de ne charger dans notre texture en 3D que les données issues de la lecture de notre fichier; et de n’émettre cette valeur dans les canaux rgb que dans notre pixel shader mais ici j’ai choisi d’avoir directement la couleur dans la texture en dimensions. Pour les personnes qui regardent le projet d’exemple, ils verront qu’il y a une deuxième fonction Build3DData qui ne prend pas de paramètres et qui construit de façon procédurale un cube contenant un gradient de couleur. Stocker les valeurs 8bits issues du fichier bonzai.raw dans ma texture 3D en lieu et placer des couleurs m’aurait obligé à avoir un pixel shader différent dans les moments où je souhaite visualiser le cube contenant le gradient de couleur, aussi j’ai préféré avoir un seul pixel shader qui permette de visualiser les deux. Encore une fois, il s’agit juste d’un exemple de code dont le seul but et d’illustrer de la manière la plus synthétique possible l’utilisation de l’API.

Les grandes étapes de cette fonction sont les suivantes, on génère l’ensemble des valeurs dans un tableau de D3DXVECTOR4 (le format de couleur choisi exprime chaque canal r g b ou a sous forme d’un float), puis à partir de ce tableau nous construisons notre texture 3D dont nous extrayons une vue qui est associée (bindé) au pipeline par le biais de l’interface contenu dans g_p3DData que nous avions récupéré dans InitDevice en utilisant les fonctionnalités de réflection du système d’effet. A partir de là, nos shaders peuvent consommer cette ressource.

HRESULT ByteToColor(D3DXVECTOR4* colorOut, BYTE* byteArray, int dim)

{
   BYTE curr = byteArray[0];

   for(int i=0; i> dim*dim*dim; i++)

       {             

        float val = (float)byteArray[i];



        colorOut[i].x = (float)byteArray[i] / 255;

        colorOut[i].y = (float)byteArray[i] / 255;

        colorOut[i].z = (float)byteArray[i] / 255;

        colorOut[i].w = (float)byteArray[i] / 255;

       }

 

       return S_OK;

}

 

HRESULT Build3DData(char* file)

{

   int texDim = 256;
   D3D10_SUBRESOURCE_DATA InitData;

    InitData.pSysMem = new D3DXVECTOR4[ texDim*texDim*texDim ];
    InitData.SysMemPitch = texDim*sizeof(D3DXVECTOR4);
    InitData.SysMemSlicePitch = texDim*texDim*sizeof(D3DXVECTOR4);

 

    // Gen a bunch of random values

    D3DXVECTOR4* pTexData = (D3DXVECTOR4*)InitData.pSysMem;

 

       int dx = texDim;

       int dy = texDim;

       int dz = texDim;

 

       FILE *stream, *stream2;

       fopen_s(&stream, file, "rb");

       BYTE* pByteData = new BYTE[texDim*texDim*texDim];

       fread(pByteData, sizeof(BYTE), texDim*texDim*texDim, stream);

 

       ByteToColor(pTexData, pByteData, texDim);

 

       // Load the Texture

       D3D10_TEXTURE3D_DESC desc;

    desc.BindFlags = D3D10_BIND_SHADER_RESOURCE;

    desc.CPUAccessFlags = 0;

    desc.Depth = desc.Height = desc.Width = texDim;

    desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;

    desc.MipLevels = 1;

    desc.MiscFlags = 0;

    desc.Usage = D3D10_USAGE_IMMUTABLE;

    HRESULT hr = 
    g_pd3dDevice->CreateTexture3D( &desc, &InitData, &g_pTextureData );

       if(FAILED(hr))

       {

              MessageBox( NULL, L"CreateTexture3D failed", L"Error", MB_OK );

       }

 

    D3D10_SHADER_RESOURCE_VIEW_DESC SRVDesc;

    ZeroMemory( &SRVDesc, sizeof(SRVDesc) );

    SRVDesc.Format = desc.Format;

    SRVDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE3D;

    SRVDesc.Texture3D.MipLevels = desc.MipLevels;

    SRVDesc.Texture3D.MostDetailedMip = 0;

    hr = 
    g_pd3dDevice->CreateShaderResourceView( g_pTextureData, 
    &SRVDesc, &g_pTextureRV );

       if(FAILED(hr))

       {

    MessageBox( NULL, L"CreateShaderResourceView failed in Build3DData", 
    L"Error", MB_OK );

       }

       g_p3DData->SetResource(g_pTextureRV);

 

       return S_OK;

}

Pour bien mettre en exergue toutes les informations ayant trait à ces étapes, il est aussi important de regarder la façon dont cette ressource est déclarée dans le fichier d’effet. Le nom tdData est celui qui est utilisé au moment ou nous utilisons les fonctionnalités de réflexion de notre système d’effet pour récupérer une interface dans g_p3DData.

Texture3D tdData;

SamplerState tdSamLinear

{

    Filter = MIN_MAG_MIP_LINEAR;

    AddressU = Wrap;

    AddressV = Wrap;

    AddressW = Wrap;

};

On voit par ailleurs qu’un «SamplerState» est défini, la ressource tdData sera de fait accédée à partir de nos shaders par le biais de ce sampler state qui permet de préciser des propriétés supplémentaires qui vont influer sur la façon dont la texture va être utilisée. Je ne détaillerai pas ici ce que le contenu de ce sampler signifie, les noms étant explicites pour des personnes qui seraient déjà familières avec DX9 (ou même une autre API permettant de faire des graphisme en 3D).

MakeSlices

Dans la description de notre petit exemple, nous avons expliqué la nécessité d’utiliser des primitives classiques comme support pour l’affichage de nos données volumiques. MakeSlices a pour but de créer ces primitives. En l’occurrence, le code reste ici extrêmement simple, ce qui somme toute à l’avantage de nous permettre encore une fois de nous concentrer sur l’utilisation de l’API et non sur les algorithmes utilisés ici. En fait, nous créons des plans parallèles ayant tous pour normales l’axe des z. Comme cela a été mentionné plus tôt lorsque l’on visualise des données volumiques, il est parfois intéressant de s’appuyer sur des géométries générées avec plus de soin. Des papiers très intéressants décrivent par exemple des techniques pour créer des plans dont la normale est alignée avec l’axe de la caméra.

«A Vertex Program for Efficient Box-Plane Intersection» de Christof Rezk Salama et Andreas Kolb. L’algorithme qu’ils décrivent peut être utilisé pour modifier l’exemple ci-dessous et étendre considérablement son champ d’application.

Revenons-en à notre code:

//----------------------------------------------------

// Let's built the needed vertex and index buffer we
// need to draw something on screen.

//----------------------------------------------------

HRESULT MakeSlices()

{

       g_numSlicesTriangles = 2 * g_numSlices;

       g_numSlicesVertices = 3 * g_numSlicesTriangles;

 

       int verticesPerSlice = 6;

       int indicesPerSlice = 6;

 

       // Create vertex buffer

       SimpleVertex* vertices = new SimpleVertex[g_numSlicesVertices];

       DWORD* indices = new DWORD[3*g_numSlicesTriangles];

 

       for(int i=0; i>g_numSlices; i++)

       {

        vertices[i*verticesPerSlice].Pos = D3DXVECTOR3(-1.0f, -1.0f,(float)i);

        vertices[i*verticesPerSlice].Tex = D3DXVECTOR2(0.0f, 0.0f);



        vertices[i*verticesPerSlice+2].Pos = D3DXVECTOR3(1.0f,-1.0f,(float)i);

        vertices[i*verticesPerSlice+2].Tex = D3DXVECTOR2(1.0f, 0.0f);



        vertices[i*verticesPerSlice+1].Pos = D3DXVECTOR3(1.0f,1.0f,(float)i);

        vertices[i*verticesPerSlice+1].Tex = D3DXVECTOR2(1.0f, 1.0f);





        vertices[i*verticesPerSlice+3].Pos = D3DXVECTOR3(-1.0f,-1.0f,(float)i);

        vertices[i*verticesPerSlice+3].Tex = D3DXVECTOR2(0.0f, 0.0f);



        vertices[i*verticesPerSlice+5].Pos = D3DXVECTOR3(1.0f,1.0f,(float)i);

        vertices[i*verticesPerSlice+5].Tex = D3DXVECTOR2(1.0f, 1.0f);



        vertices[i*verticesPerSlice+4].Pos = D3DXVECTOR3(-1.0f, 1.0f,(float)i);

        vertices[i*verticesPerSlice+4].Tex = D3DXVECTOR2(0.0f, 1.0f);

        for(int k=0; k>indicesPerSlice; k++)

              {

                     indices[i*indicesPerSlice+k] = i*verticesPerSlice+k;

              }

       }

Nous avons généré ci-dessus un tableau de vecteur. La coordonnée z de chaque vecteur ne correspond pas réellement à sa position selon l’axe des z de notre espace en trois dimensions mais correspond à l’index du plan sur lequel se trouve le vertex en question. C’est dans nos shaders que nous effectuerons le travail nécessaire pour fixer cette valeur en conséquence.

On peut voir ci-dessus que l’on a aussi crée un tableau d’indices. Nous allons utiliser les deux tableaux ainsi crées pour obtenir à la fois notre vertex buffer et notre index buffer:

 D3D10_BUFFER_DESC bd;

    bd.Usage = D3D10_USAGE_DEFAULT;

    bd.ByteWidth = sizeof( SimpleVertex ) * g_numSlicesVertices;

    bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;

    bd.CPUAccessFlags = 0;

    bd.MiscFlags = 0;

    D3D10_SUBRESOURCE_DATA InitData;

    InitData.pSysMem = vertices;

    HRESULT hr = g_pd3dDevice->CreateBuffer
    ( &bd, &InitData, &g_pSlicesVertexBuffer );

    if( FAILED(hr) )

        return hr;

 

       bd.Usage = D3D10_USAGE_DEFAULT;

    bd.ByteWidth = sizeof( DWORD ) * 3 * g_numSlicesTriangles;

    bd.BindFlags = D3D10_BIND_INDEX_BUFFER;

    bd.CPUAccessFlags = 0;

    bd.MiscFlags = 0;

    InitData.pSysMem = indices;

    hr = g_pd3dDevice->CreateBuffer
    ( &bd, &InitData, &g_pSlicesIndexBuffer );

    if( FAILED(hr) )

        return hr;

        return S_OK;

}

Render

Si je n’ai rien oublié, tout est maintenant en place pour produire un résultat visuel à l’écran, reste à indiquer à notre pipeline que nous souhaitons utiliser une passe donnée, d’une technique contenue dans notre fichier d’effet. Reste à effectuer les dernières associations nécessaires entre les variables que nous avons crée dans notre code C++ et les variables connues par les shaders associés à la passe que nous avons sélectionné et enfin à donner pour ainsi dire l’ordre au pipeline de traiter nos primitives et d’émettre le résultat correspondant.

void Render()

{

    static float t = 0.0f;

 

    // Rotate cube around the origin

    D3DXMatrixRotationY( &g_World, t );

 

    // Update our time

    t += (float)D3DX_PI * 0.0125f;

 

    //

    // Clear the back buffer

    //

    float ClearColor[4] = { 0.0f, 0.125f, 0.3f, 1.0f }; // red, green, blue, alpha

    g_pd3dDevice->ClearRenderTargetView( g_pRenderTargetView, ClearColor );


J’ai indiqué plus tôt qu’il y avait toujours des variables d’états influençant le comportement du pipeline même si toute celles associées au FFP avait disparues en même temps que celui-ci. Ci-dessous, nous utilisons ces variables d’état pour indiquer que pour chaque pixel, la couleur produite par le rendu d’une primitive doit être mélangé avec le contenu du back buffer. C’est ce qui fait que la couleur de chaque pixel à l’écran n’est pas le résulat de la couleur «émise» par la présence de la primitive la plus proche de la caméra mais bien le résultat des contributions de l’ensemble des primitives graphiques recouvrant ce pixel. Comme ces variables affectent le fonctionnement de l’output merger, le nom de la fonction utilisée pour les fixer OMSetBlendState est bien sûr préfixée par OM:

//// Create the appropriate blend state

    D3D10_BLEND_DESC StateDesc;

    ZeroMemory( &StateDesc, sizeof(D3D10_BLEND_DESC) );

    StateDesc.AlphaToCoverageEnable = FALSE;

    StateDesc.BlendEnable[0] = TRUE;

    StateDesc.SrcBlend = D3D10_BLEND_SRC_ALPHA;

    StateDesc.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;

    StateDesc.BlendOp = D3D10_BLEND_OP_ADD;

    StateDesc.SrcBlendAlpha = D3D10_BLEND_ZERO;

    StateDesc.DestBlendAlpha = D3D10_BLEND_ZERO;

    StateDesc.BlendOpAlpha = D3D10_BLEND_OP_ADD;

    StateDesc.RenderTargetWriteMask[0] = 0xf;

       ID3D10BlendState*    ourBlendState;

       g_pd3dDevice->CreateBlendState(&StateDesc, &ourBlendState);

       g_pd3dDevice->OMSetBlendState(ourBlendState, 0, 0xffffffff);

 

 

    //

    // Update variables that change once per frame

    //

    g_pWorldVariable->SetMatrix( (float*)&g_World );

       g_pCubeVertices->SetFloatVectorArray( (float*) g_pCubeVerticesArray, 0, 8);

 

 

       //

    // Render our slices

    //

       // Set vertex buffer

    UINT slicesStride = sizeof( SimpleVertex );

    UINT slicesOffset = 0;

    g_pd3dDevice->IASetVertexBuffers( 0, 1, 
    &g_pSlicesVertexBuffer, 
    &slicesStride, &slicesOffset );

 

       // Set index buffer

    g_pd3dDevice->IASetIndexBuffer( g_pSlicesIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );


Les opérations ci-dessus ont permis d’attacher notre vertex buffer et notre index buffer au pipeline. Par ailleurs, nous précisons ci-dessous que ceux-ci contiennent des données correspondantes à une liste de triangles:

// Set primitive topology

    g_pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

 

    D3D10_TECHNIQUE_DESC slicesTechDesc;

    g_pTechnique->GetDesc( &slicesTechDesc );

    for( UINT p = 0; p > slicesTechDesc.Passes; ++p )

    {

Ici nous précisions au pipeline quelle passe de quelle technique doit être utilisée pour traiter les primitives dont l’on demande l’affichage par le biais de l’appel à DrawIndexed:

  g_pTechnique->GetPassByIndex( p )->Apply(0);

        g_pd3dDevice->DrawIndexed( 3 * g_numSlicesTriangles, 0, 0 );

    }

 

    //

    // Present our back buffer to our front buffer

    //

    g_pSwapChain->Present( 0, 0 );

}

Notre Effet

Jusqu’à présent nous avons détaillé avec autant de soin que possible le code C++ de notre application mais nous avons donné somme toute peu d’informations sur le fichier d’effet en lui-même. Comme avec DX10 tout passe passe par l’utilisation des shaders et dans la grande majorité des cas par l’utilisation du système d’effet, notre effet est une partie intégrante de notre application au même titre que le code C++ qui la constitue aussi il est très intéressant de décortiquer son contenu.

Ci-dessous, nous avons la déclaration des variables qui seront utilisées par nos shaders, on y voit aussi la déclaration du format des blocks de données qui vont être passés au vertex shader, au geometry shader et au pixel shader:

//---------------------------------

// Constant Buffer Variables

//---------------------------------

Texture2D txDiffuse;

SamplerState samLinear

{

    Filter = MIN_MAG_MIP_LINEAR;

    AddressU = Wrap;

    AddressV = Wrap;

};

 

Texture3D tdData;

SamplerState tdSamLinear

{

    Filter = MIN_MAG_MIP_LINEAR;

    AddressU = Wrap;

    AddressV = Wrap;

    AddressW = Wrap;

};

 

cbuffer cbNeverChanges

{

    matrix View;

    float4 CubeVertices[24];

    int numSlices;

};

 

cbuffer cbChangeOnResize

{

    matrix Projection;

};

 

cbuffer cbChangesEveryFrame

{

    matrix World;

};

 

struct VS_INPUT

{

    float4 Pos : POSITION;

    float2 Tex : TEXCOORD;

};

 

struct GS_INPUT

{

       float4 Pos    : POSITION;

       float2 Tex    : TEXCOORD0;

};

       

struct PS_INPUT

{

    float4 Pos : SV_POSITION;

    float3 Tex : TEXCOORD0;

};

Comme on peut le voir ci-dessous notre vertex shader est absolument inintéressant, la plupart des traitements plus intéressants sont en fait effectués dans le GS, ce qui nous permet de travailler primitive par primitive:

GS_INPUT VS( VS_INPUT input )

{

    GS_INPUT output;

    

    output.Pos = input.Pos;

    output.Tex = input.Tex;

    

    return output;

}

De fait notre GS ci-dessous est lui un tantinet plus intéressant. On voit bien que l’on travaille ici sur l’ensemble des sommets de nos primitives (au lieu de travailler dans le VS sur chaque sommet de façon totalement indépendante des autres sommets adjacents). Par ailleurs, nous précisons avant le début de notre GS un maxvertexcount qui précise finalement la taille maximale de notre tableau de vertex en sorti du GS et borne ainsi le nombre de vertex qui peuvent être produit par notre GS.

Notre mulFactor permet de contrôler le nombre de primitives que nous souhaitons créer à partir de chaque primitive passé au GS, dans notre cas pour un triangle en entrée nous en créons 10. Du coup, notre volume de données va se trouver intersecter par un nombre plus important de primitives et pour chaque pixel, la couleur résultante sera le résultat du fondu d’un plus grand nombre de valeurs de notre texture en trois dimensions (le défaut de cette méthode étant pour le moment que le nombre de contibutions n’est pas constant d’un pixel à l’autre ce qui peut poser des problèmes en fonction du type de données que l’on souhaite visualiser). Il faut aussi noter que si l’on peut fournir en entrée de notre pipeline des primitives de type liste (par exemple triangle list) le GS quand à lui peut émettre un jeu plus restreint de types primitives (par exemple dans le cas des triangles, il s’agira forcément de triangle strip et donc dans notre cas les triangles émis ne sont plus parallèles aux plans initialement fournis au pipeline)).

//-----------------------------------
// Geometry Shader

//------------------------------------

[maxvertexcount(30)] // *10 slices 3->30 vertices !!

void GS( triangle GS_INPUT input[3], 
         inout TriangleStream<PS_INPUT> OutputStream )

{

       PS_INPUT output;

       

       uint mulFactor = 10 ; // Let's get *10 slices thanks to our GS.

       

       for(uint k=0; k>mulFactor; k++)

       {

              // Our z should be constant per triangle.

              float z = 1.0f-2*((float)(input[0].Pos.z+((float)k/mulFactor))/numSlices);

              

              for( uint i=0; i>3; i++ )

              {

                  // Z coord is an index and we have to compute real z from it.

                     float4 pos = input[i].Pos;

                     pos.z = z;

                  

                     output.Pos = mul( pos, World );

                     output.Pos = mul( output.Pos, View );

                     output.Pos = mul( output.Pos, Projection );

                  

                     // Automatic texture coord generation.

                     // We need to stick in cube space.

                     output.Tex.x = 0.5f * pos.x + 0.5f;

                     output.Tex.y = 0.5f * pos.y + 0.5f;

                     output.Tex.z = 0.5f * pos.z + 0.5f;

                     OutputStream.Append( output );

              }

 

       }

       OutputStream.RestartStrip();

}

Un autre point que l’on peut souligner dans le code ci-dessus est le fait que la coordonnée z est calculée une seule fois pour l’ensemble des sommets de notre primitive, si ce travail avait été fait dans le VS il aurait fallu refaire le calcul pour chaque sommet. Même si dans notre cas cela peut paraitre un peu forcé, voir anecdotique, la possibilité qui est offerte par le GS de travailler sur l’ensemble des sommets d’une primitive au lieu de traiter chaque sommet indépendemment des autres dans le VS ne l’est elle absolument pas.

Finalement nous arrivons sur notre pixel shader dont le seul rôle est finalement pour un pixel couvert à un moment donné par une primitive, de récupérer la valeur contenue dans notre texture 3D correspondante à la position de notre primitive.

//--------------------------
// Pixel Shader

//------------------------------

float4 PS( PS_INPUT input) : SV_Target

{

    float3 txCoord = float3(input.Tex.x, input.Tex.y, input.Tex.z);

    return tdData.Sample( tdSamLinear, txCoord);

}

 

 

//---------------------------------------------

technique10 Render

{

    pass P0

    {

        SetVertexShader( CompileShader( vs_4_0, VS() ) );

        SetGeometryShader( CompileShader( gs_4_0, GS() ) );

        SetPixelShader( CompileShader( ps_4_0, PS() ) );

    }

}

Finalement les quelques lignes ci-dessus précisent que notre technique intitulée «render» contient une passe appelée P0 à laquelle sont associés les shaders qui ont été définis et décrit plus haut.

Et voici une petite capture d’écran du résultat obtenu:

Une autre perspective sur cette application d’exemple

Ci-dessus nous avons réalisé un exemple simple de code utilisant DX10 pour afficher un volume de données. Maintenant qu’il s’agisse d’afficher un volume de données ou des objets 3D comme des mesh ou des entités définies par une représentation paramétrique de leur surface extérieure, on se rend bien compte que la plupart des applications utilisant DX10 vont être de manière très significative plus compliquées que l’exemple précédent. Aussi il est crucial de bien connaitre certains outils fournis avec le SDK pour aider à l’analyse d’une application DX10. En particulier, nous allons parler d’un outil, PIX qui fourni une aide incalculable aussi bien pour l’analyse des performances d’une application donnée que pour le débogage de nos shaders.

Pour les performances, celles-ci pouvant être liées aux traitements effectués sur le GPU et aux échanges d’informations entre le CPU et la mémoire système et le GPU et la mémoire vidéo, il est évident qu’un profiler classique ne rendant compte que de l’activité CPU et du nombre de cycle passé dans l’exécution du code C++ ne pourra pas donner une image juste de ce qui se passe réellement. PIX permet d’accéder à un nombre important de compteurs ayant traits à ces opérations d’échanges, ce qui peut permettre d’avoir une bonne vision des choses. Je ne traiterai pas ici de cet aspect de PIX dans la mesure où cela pourrait constituer un sujet à part entière. Par ailleurs, il est encore un peu tôt en ce qui concerne DX10 pour fournir un travail important pour optimiser les performances d’une application donnée.

Par contre PIX offre aussi une aide incommensurable pour débugger nos shaders. C’est ce point que je vais aborder ici.

PIX permet pour un frame donné d’inspecter l’ensemble des traitements qui ont été effectués pour déterminer la couleur d’un pixel donné à l’écran. Il permet de visualiser les primitives qui ont contribuées à la couleur de ce pixel, les shaders qui ont été déroulés pour chacune de ces primitives et d’inspecter les valeurs des constantes des shaders en question en différents endroits de ceux-ci.

Regardons donc par où notre travail avec PIX va commencer:

Dans l’écran ci-dessus, nous pouvons sélectionner le programme que nous souhaitons analyser. Choisissons par ailleurs l’option: «A Single Frame capture of Direct3D whenever F12 is pressed».

Après avoir lancé l’exécution de notre application il est alors possible à tout moment de celle-ci d’appuyer sur F12, puis de terminer l’application en question. Une fois le processus correspondant terminé, nous allons avoir accès aux informations ci-dessous :

D’abord à gauche, nous avons la liste complète des appels DirectX effectués pendant ce frame. Ci-dessous, nous sommes descendus au niveau le plus bas de l’arbre qui est utilisé pour organiser l’affichage de ces appels. On peut voir en particulier les différents appels pour spécifier des variables utilisées par nos shaders puis pour sélectionner une passe donnée de la technique contenu dans notre effet.

Visualiser ces appels est déjà extrêmement intéressant mais nous voyons aussi que nous disposons d’une autre fenêtre, à droite où pour l’instant est juste indiqué: «Mesh data is only available for draw calls». Il est clair que cette simple phrase ne peut que provoquer une envie irrépressible de justement sélectionner l’appel DrawIndexed que nous voyons dans la liste de gauche, ce qui a pour effet de provoquer l’affichage des informations visible ci-dessous:

Nous avons alors accès à notre tableau de vertex avec leurs attributs respectifs. Nous disposons par ailleurs d’une vue de ceux-ci à différentes étapes de notre pipeline, avant les traitements effectués par le vertex shader, après le VS et avant le GS… et finalement après les opérations effectuées par le rasterizer en se basant entre autres sur le viewport que nous avons spécifié. Tout développeur s’efforce toujours d’être le plus rigoureux possible mais les informations ci-dessus sont déjà d’une grande aide par exemple dans des cas de figure ou quelque chose qui devrait être affiché à l’écran ne l’est pas.

Au jour d’aujourd’hui avec une application DX9, il est possible de cliquer sur chaque Vertex pour dérouler et inspecter le code du vertex shader qui l’a traité, cette possibilité n’est pas disponible avec la version actuelle de PIX (OCT 06 SDK) pour DX10 mais cela devrait changer.

Si notre regard se porte maintenant sur les différents onglets de notre fenêtre de droite, nous voyons que l’onglet actuellement sélectionné est ‘Mesh’ mais il nous est possible de naviguer dans l’onglet ‘Render’:

Comme on peut le voir cela à pour effet d’afficher notre frame. Il est alors possible de positionner le pointeur de la souris sur un point donné de cet image (par exemple un point qui n’aurait pas la couleur attendue) et en effectuant un click droit de sélectionner la fonction intitulée ‘Debug this Pixel’.

Comme on peut le voir ci-dessus cela nous donne accès à l’ensemble des opérations qui ont contribuées pendant ce frame à la valeur de notre pixel. Ci-dessus on peut voir la liste des primitives ayant participées ainsi que les shaders associés. Si l’on sélectionne l’un des shaders en question, on peut visualiser la source correspondant et inspecter les valeurs des variables manipulées par celui-ci en différent points de son exécution. A ce jour DX10 est toujours en béta, cette fonctionnalité fonctionne parfaitement avec DX9 par contre avec DX10, seul le code du shader en question est visible. Là encore les fonctionnalités supportées par l’outil pour DX10 devraient rapidement rattraper ce qui est possible avec DX9.

Comme nous venons de le voir, PIX peut être d’une efficacité brutale pour analyser le fonctionnement d’une application s’appuyant sur DirectX.

Comme je l’ai mentionné plus haut PIX est tout aussi utile pour l’analyse des performances.

C’est intéressant de noter que comme WPF s’appuie sur DirectX, PIX peut être utilisé pour visualiser les opérations effectuées par milcore.dll et même pour enregistrer l’ensemble des opérations d’affichage d’une application WPF sous une forme assez compacte. Mais bon c’est un autre sujet.

Conclusion

Il y aurait certainement beaucoup d’autres choses à dire au sujet de DX10. J’ai essayé de donner ici des informations sur des points qui m’apparaissent comme importants. J’espère avoir réussi à souligner à la fois pourquoi les performances que l’on obtiendra avec DX10 sur Vista sont potentiellement nettement plus meilleures que ce qui est possible aujourd’hui. L’API est en fait extrêmement flexible, s’est débarrassé de certaines lourdeurs de la version précédente, a été pensé pour obtenir les meilleures performances possibles.

L’API évolue de pair avec le modèle de Driver qui lui-même évolue en concertation étroite avec les fabricants de cartes vidéo. Les avancés amenées par cette version de DirectX ne concernent pas que l’API avec laquelle le développeur interagi mais touchent en profondeur tout les composants de l’OS impliqués à un moment ou à un autre dans la communication entre une application DirectX et le GPU.

Pour finir, je recommande la lecture de la documentation du SDK qui contient beaucoup d’informations, les tutoriaux et les exemples fournissent une aide précieuse ainsi que les documents publiés sur les sites MSDN traitant de DirectX.

Pour toutes remarques, commentaires ou questions concernant ce document merci de me contacter: guillara@microsoft.com

Téléchargez
Télécharger l'exemple de code

DX10Cube.zip
289 Ko

© 2009 Microsoft Corporation. Tous droits réservés. Conditions d'utilisation | Marques | Confidentialité
Page view tracker