Astuces DotNet (Sébastien Courtois)

28/01/2012

[Kinect SDK] Presentation de Kinect SDK

Filed under: C#, C++, Débutant, Jeux vidéos, Kinect — Étiquettes : , , , , , , , — sebastiencourtois @ 08:44

Cet article est le premier d’une série d’articles sur le SDK de Kinect qui sort le 1er février. Ce SDK pour PC permet de dialoguer avec Kinect en .NET ainsi qu’en C++.

Au travers de cette série d’articles, vous apprendrez à récupérer les informations que fournissent les différents capteurs Kinect. Ce post d’introduction va présenter les différentes possibilités du SDK.

 

1. Kinect : Le Matériel

kinect-hardware

Kinect est composé de :

* une camera video RGB : Il s’agit d’une caméra couleur d’une résolution 640×480 (30 images/seconde) ou 1280×960 (15 images/seconde).

* une caméra de profondeur : Il s’agit d’un laser infrarouge (capteur de gauche) qui balaie la pièce de rayon infrarouge. Ces rayons sont visualisés par un capteur infrarouge (capteur de droite). La sensibilité de ce capteur s’ajuste automatiquement en fonction des conditions de la pièce (luminosité, obstacles…) Ce capteur de profondeur a une résolution de 320×240 ou 80×60 (30 images par secondes).

* 4 microphones chargés d’enregistrer le son et de réaliser des opérations de traitements du signal afin de supprimer les parasites (bruits extérieurs, éclos…). Ce système de microphone réparti sur l’ensemble du capteur permet de détecter la direction du son et ainsi reconnaitre le joueur qui a parlé.

* Un moteur pour incliner verticalement Kinect. Kinect peut aller de –27 degrés à + 27 degrés d’inclinaison. Cela permet au développeur d’adapter la vue si elle n’est pas adéquate. (Par exemple, si on voit le corps d’une personne, mais pas sa tête, le développeur peut incliner Kinect vers le haut afin d’avoir le corps en entier.)

 

2. Le SDK et la partie logicielle de Kinect

Le SDK Kinect est disponible sur le site : Kinect For Windows.

En plus des fonctions de bases de Kinect, le SDK embarque aussi :

* Le tracking de personnes (Skeleton Tracking) : Kinect peut indiquer si elle reconnait une personne et permet de tracker 20 points du corps en temps réel.

* En plus de la récupération d’un son sans parasites, le SDK fournit des aides pour la reconnaissance de la parole basée sur l’API Windows Speech (aussi utilise dans Windows Vista et 7 pour la reconnaissance de la parole).

kinect-voicelocator

Kinect Voice Locator : Un des samples du SDK

3. Les caractéristiques techniques et limitations de Kinect

Pour la caméra vidéo, Kinect fournit des images de 640×480 (30 images/seconde) ou 1280×960 (15images/seconde). Chaque pixel est codé en BGR sur 32 bits.

Pour le capteur de profondeur, Kinect fournit des images en 320×240 (30 images/seconde). Les données du capteur de profondeur sont codées sur 13 bits. Elle représente une distance en millimètre allant de 800 mm à 4000 mm. Lorsque le tracking des personnes est actif, les données de reconnaissance des personnes sont fusionnées avec les données de profondeur. Au final, chaque pixel correspond à 16 bits (13 pour la profondeur et 3 pour l’identification d’une personne). Il est possible de tracker 2 personnes en temps réel + 4 autres personnes « passives » (les personnes sont détectes, mais il n’y a pas de tracking de leur squelette).

L’une des difficultés est que les images venant de la caméra vidéo et du capteur de profondeur ne sont pas synchronisées par le SDK. Si le développeur souhaite faire du traitement d’images en utilisant les deux capteurs, il devra réaliser lui même la synchronisation. Cela peut s’avérer difficile, car les framerates entre les capteurs peuvent être différents et ne sont pas toujours constants.

Le moteur d’inclinaison de Kinect possède aussi ses limitations. En dehors de sa plage de valeurs (allant de –27 dégrées a +27 dégrées d’inclinaison verticale), il n’est pas possible d’envoyer des ordres a Kinect plus d’une fois par secondes ou plus de quinze fois toutes les 20 secondes. Cela est du a des limitations matériel, car le moteur n’est pas conçu pour bouger constamment. De plus, le fait de bouger la caméra prend du temps et peut perturber les capteurs Kinect.

La reconnaissance de la parole est entièrement basée sur des grammaires que le développeur doit fournir. Les grammaires correspondantes aux mots à reconnaitre. Cela peut être défini dans un fichier XML ou directement dans le code.

 

4. Conclusion

Nous avons fait un petit tour du propriétaire du composant Kinect et de ses possibilités. Dans les prochains articles Kinect, nous verrons comment récupérer les différentes informations (flux vidéo & profondeur, détections et tracking de joueurs…) et comment les utiliser (traitement d’images, dessin des squelettes…).

11/12/2010

[XNA 4] Tutoriel 3D 4 : Modèles 3D et gestion de la lumière

Filed under: .NET, 3D, C#, Débutant, XBOX 360, XNA — Étiquettes : , , , , , , — sebastiencourtois @ 01:20

Lors du dernier article sur la 3D dans XNA, nous avons vu comment charger des données graphiques pour les afficher (exemple du cube). Rentrer l’ensemble des coordonnées des points à la main est fastidieux rien que pour dessiner un cube alors je vous laisse imaginer pour une maison ou un personnage. C’est pourquoi il existe des modeleurs 3D permettant de dessiner les modèles 3D puis de générer des fichiers contenant l’ensemble des données pré-formatés pour être utilisable par la carte graphique.

Il existe de nombreux modeleurs 3D. L’un des plus utilisé est Blender. Il offre une bonne partie des fonctionnalités que propose les modeleurs pro come 3DSMAX ou Maya et il est gratuit. Son seul inconvénient est d’être parfois complexe à utiliser (beaucoup de raccourcis clavier à connaitre).

Les modeleurs 3D peuvent générer des fichiers contenant les informations 3D de la scène dessiné (position des vertex, indices, triangles, textures ….). Il existe un très grand nombre de format dans l’univers de la 3D (certains éditeurs de jeu crée parfois leur propre format pour un moteur de jeu défini). XNA prend en charge nativement au moins deux formats : le .X (format DirectX) et le format .fbx (Format Autodesk).

Dans ce tutoriel, nous utiliserons 3 modèles : Une sphère exporté depuis Blender (fait par moi même), un personnage texturé (venant du SDK DirectX) et un tank en 3D venant des démos XNA disponible sur le site officiel.

  • Chargement d’un modèle 3D

La première étape est de charger les modèles ainsi que leurs textures dans le projet ‘Content’ de la solution.

blog_model_1

(sphere.fbx correspond à la sphère / tiny.x et tiny_skin.dds correspond au personnage et tank.fbx,engine_diff_tex.tga et turret_alt_diff_tex.tga correspondent au tank)

Ensuite il suffit de charger les données dans le ContentManager de XNA. Un modèle 3D se charge dans la classe Model. Le chargement se réalise grâce à la méthode Load du ContentManager.

Model sphere;
Model tiny;
Model tank;

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);

    this.sphere = Content.Load<Model>("sphere");
    this.tiny = Content.Load<Model>("tiny");
    this.tank = Content.Load<Model>("tank");

    this.font = Content.Load<SpriteFont>("font");
}
  • Dessin d’un modèle 3D

Une fois chargé, on souhaite afficher le modèle. Cela se réalise dans la méthode Draw de la classe Game. On peut faire afficher un modèle de plusieurs manières.

Tout d’abord, on peut afficher le modèle dans sa totalité en appelant la méthode Draw du Model :

this.sphere.Draw(Matrix.Identity, this.View, this.Projection);

La méthode Draw demande 3 paramètres correspondant aux 3 matrices principales que l’on à déjà vu dans les tutoriaux précédent (World, View, Projection). La plus importante des trois est la première (World) car elle permet d’indiquer la position, l’orientation et la taille de l’objet à afficher et varie donc généralement d’un modèle à l’autre.

Une autre méthode revient à afficher le modèle morceaux par morceaux. En effet, si vous regarder la classe Model, elle est composé de plusieurs ModelMesh eux mêmes composé de MeshPart. Dans chacun de ces objets se trouve un objet Effect (BasicEffect la plupart du temps) qui va interagir avec ces données. On peut donc afficher un modèle de la façon suivante :

foreach (ModelMesh mesh in this.tank.Meshes)
    foreach (BasicEffect effect in mesh.Effects)
    {
        effect.World = Matrix.Identity;
        effect.View = this.View;
        effect.Projection = this.Projection;
        mesh.Draw();
    }

Ce moyen peut paraitre plus long et compliqué mais il peut avoir son utilité notamment lors des animations de modèles que nous verrons dans un prochain tutoriel.

blog_model_2

  • La lumière : Les bases

Maintenant que vous savez charger des modèles 3D, nous allons nous intéresser à la lumière.

Si on se réfère à la définition physique Wikipedia, la lumière est une onde électromagnétique visible par l’œil humain. Sa longueur d’onde va de 380nm (violet) à 780 nm (rouge). La lumière a généralement une source qui peut être plus ou moins éloigné des objets éclairés. Lorsque la lumière part de cette source, elle se propage dans toutes les directions. Lorsqu’elle touche un objet, cet objet absorbe une partie des longueurs d’onde  et réfléchi le reste. La lumière réfléchie arrive dans notre œil. Si l’on regarde une balle rouge, cela veut dire que la balle a absorbé toute les autres longueurs d’onde que le rouge.

De plus chaque face d’un objet à une normale. Il s’agit d’un vecteur perpendiculaire à la  surface. Lorsqu’un rayon lumineux “touche” une face d’un objet selon un angle (angle d’incidence) par rapport à la normale de cette face, ce rayon est réfléchi avec le même angle à l’opposé de cette normale.

Afin d’avoir des rendus lumineux réalistes, XNA (et les API 3D en général) s’inspire très fortement des phénomènes physiques décrits ci-dessus.

  • Gestion de la lumière dans XNA : Pratique

Après ces rappels physiques, nous allons maintenant passer à l’utilisation de la lumière en XNA. Pour faire simple, nous utiliserons les lumières disponibles au travers de l’effet BasicEffect fourni par XNA. Les lumières sont gérés dans les effets car ce sont des opérations parfois couteuses en temps et que les effets sont exécutés par le GPU (donc plus rapidement). Nous verrons dans les tutoriaux sur les shaders comment créer nos propres types de lumières.

Par défaut, les lumières sont désactivés. On voit alors les modèles selon leurs couleurs originales (textures/couleurs définis directement dans le modèle). Pour l’activer, il suffit de passer la propriété LightingEnabled de BasicEffect à true.

blog_model_3

Notre modèle apparait alors en noir. Cela est normal car, bien que l’objet soit texturé, aucun rayon lumineux ne se réfléchi sur le tank donc notre camera (notre œil virtuel) ne reçoit aucune lumière donc il voit l’objet en noir.

  • Nous allons activer une première composante lumineuse : La composante ambiante.

Cette lumière est une lumière sans source lumineuse défini et dont la lumière est présente partout. Pour faire une analogie, on pourrait comparer cela à un jour nuageux au travers desquels ont ne voit pas la lumière. La lumière est pourtant présente car on parvient à voir les objets nous entourant. Par défaut, la lumière d’ambiante est noire. Il est possible de changer cette valeur grâce à la propriété AmbientLightColor.

blog_model_ambient

En terme de calcul, cela est très rapide car il suffit de rajouter la couleur ambiante sur les pixels du modèle sans tenir compte “des données physiques “(normales notamment). Le soucis est que cela rend le modèle plat.

  • Deuxième type de composante lumineuse: La composante diffuse

A l’instar de la lumière ambiante, la lumière diffuse ne provient d’aucune source définie. Toutefois, le calcul de la lumière prend en compte les normales de chacune des faces. Ainsi l’affichage fera apparaitre des ombres selon l’angle sous lequel l’objet est regardé. Il est possible de modifier la couleur de la lumière diffuse avec la propriété DiffuseColor de BasicEffect.

blog_model_diffuse

  • Troisième type de composante lumineuse : La composante spéculaire

La lumière spéculaire est le léger reflet que l’on voit dans un objet réfléchissant. Elle dépend de la position de la caméra, de la source lumineuse et différents vecteurs incidences/réflexions de la lumière. Il est possible de définir la force de la tache spéculaire ainsi que sa couleur avec les propriétés SpecularPower et SpecularColor.

blog_model_specular

Ici, la lumière est blanche (diffuse) et la lumière spéculaire est jaune.

  • Dernier type de composante lumineuse : La composante émissive

Cette composante va simuler le fait que le modèle émettent de la lumière. On peut définir la couleur d’émission avec la propriété EmissiveColor de BasicEffect.

blog_model_emissive

Exemple avec une lumière émissive bleu.

Si vous voulez plus d’informations sur les lumières en 3D, je vous conseille cet article.

  • Une lumière réaliste : La lumière directionnelle

BasicEffect permet la création de 3 lumières directionnelle. Une lumière directionnelle n’a pas de source et n’est défini que par ses composants et un vecteur directeur. On peut envisager cela comme étant une source lumineuse se trouvant à l’infini dont l’ensemble des rayons arrivent sur l’objet parallèle les uns aux autres selon le vecteur directeur.

blog_model_noDL blog_model_DL
Modèle sans lumières Modèle avec une lumière directionnelle blanche.

Le calcul de la lumière se faisant en fonction du vecteur directeur et des différentes normales des surfaces, on obtient un rendu plus réalistes avec des ombres et des tâches spéculaires.

Les propriétés des lumières directionnelles sont disponible avec la propriétés DirectionalLightX (avec X étant 0, 1 ou 2).

Exemple :

ef.DirectionalLight0.Enabled = DirectLightOn;
ef.DirectionalLight0.Direction = this.DirectLightPosition;
ef.DirectionalLight0.DiffuseColor = this.DirectLightColor.ToVector3();
ef.DirectionalLight0.SpecularColor = this.SpecularLight.ToVector3();
  • Méthode de calcul de la lumière : Par vertex ou par Pixel

Il est possible de calculer la lumière de deux façons. En calculant la lumière en fonction de chacun des vertex ou en faisant la calcul pour chacun des pixels. En général, le rendu par pixel est plus réaliste mais parfois plus lent que le traitement par vertex. Cela dépend de la définition des modèles. Plus il y aura de vertex sur le modèle, plus la qualité du traitement par vertex sera lourd et réaliste.

blog_model_noDL2 blog_model_DL2 blog_model_DL2_PL
Modèle normal Lumière calculée par vertex Lumière calculé par pixel
  • Brouillard

BasicEffect permet aussi de faire des sortes de brouillard. Les brouillards sont souvent des astuces dans les jeux vidéos pour réduire la profondeur de champ et de ne pas à avoir à afficher les objets 3D loin de la caméra. Le brouillard comporte 4 propriétés. La propriété FogEnabled permet d’activer ou désactiver le brouillard. FogColor permet de définir la couleur du brouillard. On peut définir la distance où commence et ou se termine le brouillard. Les propriétés  FogStart et FogEnd sont des valeurs numériques définissant la distance par rapport à la caméra.

blog_model_fog

  • La démo

J’ai réalisé une démo afin de pouvoir réaliser l’ensemble des screenshots pour ce tutoriel. Il est pilotable au clavier et permet de modifier l’ensemble des propriétés présentées ici.

La démo est disponible ici (11 Mo)

Raccourcis clavier :

Flèches directionnelles : Déplace la caméra autour de l’objet

Page Up/Page Down: Zoom +/-

A /Z/E: Changer le modèle (A : Sphère / Z : Personnage / E : Tank)

Q : Activation/Désactivation des lumières

S : Change le mode de calcul de la lumière (Vertex/Pixel)

D : Activation/Désactivation de la lumière directionnelle 0

Q : Change la couleur de la composante ambiante

H : Change la couleur de la composante diffuse

J : Change la couleur de la composante d’émission

R : Change la couleur de la composante spéculaire

T/Y : Augmente ou réduit la force de la composante spéculaire

W : Activation/Désactivation du brouillard

X : Change la couleur du brouillard

C/V : Change la valeur FogStart

B/N : Change la valeur de FogEnd

K/L/M/O : Déplacement de la lumière directionnelle 0 autour de l’objet

  • Conclusion

Nous avons fait un bon tour sur les lumières. Nous avons aussi exploré BasicEffect, l’effet de base fourni par XNA. Nous verrons, dans le prochain tutoriel, comment créer son propre effet en HLSL ainsi que la façon dont on peut réaliser des animations sur des modèles chargés dans XNA.

26/11/2010

[DirectX 10/11] Tutoriel 2 : Création d’un device Direct3D

Filed under: 3D, C++, Débutant, DirectX, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 17:26

Après un premier article d’introduction à DirectX, nous allons maintenant voir comment créer un device Direct3D (accès à la carte graphique). L’idée générale de cette série d’article est de réaliser un moteur de jeu gérant à la fois DirectX 10 et 11 afin de voir les évolutions des deux technologies.

  • Architecture générale du moteur

Avant de rentrer dans le vif du sujet, nous allons parler un peu architecture. Afin de simplifier la gestion des différentes versions de DirectX, nous allons créer une classe pour gérer chacune des versions de DirectX. Ces classes hériteront d’une super classe indiquant les méthodes obligatoires pour un gestionnaire.

dxt2-archi1

Les trois méthodes de DxManager sont :

  • Init(HWND* hWnd) : Initialisation du gestionnaire DirectX. Cette méthode va créer l’ensemble des ressources nécessaire pour l’utilisation de DirectX. Cette méthode ne sera appelé qu’une seule fois lors du lancement du programme. Il est nécessaire de lui fournir le handle de la fenêtre afin que DirectX puisse savoir où il doit afficher le rendu. Cette méthode renvoie ‘false’ en cas de problème lors de l’initialisation.
  • Render() : Méthode de rendu. Cette méthode sera appelée à chaque fois que l’on souhaite dessiner quelque chose à l’écran.
  • Error(LPCST msg) : Gestion des erreurs. Affichage de messages d’erreurs (MessageBox).

C’est le WinMain de l’application qui sera chargé de créer le gestionnaire directX approprié.

//DirectX 10/11 Manager
DXManager *dxManager;
//Activation/Désactivation de DirectX 11
bool DirectX11Enabled = true;

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hInstance,nCmdShow)) return 0;

    if(DirectX11Enabled)
        dxManager = new DX11Manager();
    else
        dxManager = new DX10Manager();

    if(!dxManager->Init(&hWnd)) return -1;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
        dxManager->Render();
    }
    return (int) msg.wParam;
}

  • Informations générales sur l’initialisation Direct3D

Dans ce tutorial, nous allons principalement parler de l’initialisation de Direct3D. Celle-ci se passe en 5 étapes :

  1. Création de la SwapChain
  2. Création du Device
  3. Récupération du Backbuffer
  4. Création du RenderTarget
  5. Création du ViewPort

L’étape la plus importante est la création du Device (Etape 2). Le Device est l’accès à la carte graphique. C’est à travers cet objet que nous allons pouvoir fournir les informations 3D que la carte graphique devra afficher.

Une autre notion importante est la notion de FrontBuffer et de BackBuffer. La carte graphique contient une zone mémoire où se trouve les données pour les pixels affichés à l’écran (FrontBuffer). Si l’on modifie l’un de ces pixels directement dans le FrontBuffer, le résultat sera instantanément affiché à l’écran. Le problème est que, si la chose que l’on souhaite dessiner à l’écran prend un peu trop de temps, on verra les morceaux du dessins apparaitre petit à petit ce qui peut être désagréable.Pour éviter ce problème, on utilise une deuxième zone mémoire nommé ‘BackBuffer’ qui permet de réaliser le dessin complet puis, une fois fini, d’envoyer l’ensemble des données dans le FrontBuffer pour affichage. Pour résumer, un buffer pour dessiner et un buffer pour afficher. Ce processus “d’échange” de buffer est réalisé par un élément de DirectX nommé SwapChain (Swap = échange en anglais).

IC412615

Fonctionnement de la SwapChain (tiré de MSDN). Comme vous pouvez le voir, il est possible d’avoir plusieurs BackBuffer.

Le RenderTarget défini l’endroit où l’on souhaite dessiner les données envoyés à la carte graphique. Dans notre cas, on souhaite dessiner sur le BackBuffer. Le renderTarget sera donc lié à notre BackBuffer.

Le Viewport défini la zone de l’écran sur laquelle on souhaite dessiner les pixels. On utilisera généralement l’ensemble de la fenêtre. Mais, dans certains cas, on peut souhaite afficher des choses différentes selon les parties de la fenêtre.

super_mario_kart-155809-1

Exemple d’utilisation de multiple Viewports avec un Viewport par joueur (Mario Kart SNES).

  • Gestionnaire Direct3D 10

Le gestionnaire DirectX 10 est constitué d’un fichier DX10Manager.h contenant la définition de la classe et une classe DX10Manager.cpp contenant son implémentation.

  1. Définition de la classe DX10Manager
#pragma once
#include <Windows.h>
#include <D3DX10.h>
#include "DXManager.h"

class DX10Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D10Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D10RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D10_VIEWPORT                ViewPort;

public:
    DX10Manager(void);
    //Destructeur
    ~DX10Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11 Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

2.     Implémentation de la classe DX10Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX

bool DX10Manager::Init(HWND* hWnd)

{

    //Sauvegarde du handle de la fenêtre

  
this->hWnd = hWnd;

   
    //Récupération des dimensions de la fenêtre

  
RECT rc;

    GetClientRect( *hWnd, &rc );

    UINT width = rc.right – rc.left;

    UINT height = rc.bottom – rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

    //Création de la Swap Chain
    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));
    
    //Création de deux buffers (un back buffer et un front buffer)
    swapChainDesc.BufferCount = 2;
    swapChainDesc.BufferDesc.Width = width;
    swapChainDesc.BufferDesc.Height = height;
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    //Définition du nombre d'image par secondes maximun (60 images).
    swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
    swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    
    //Multisampling setting
    swapChainDesc.SampleDesc.Quality = 0;
    swapChainDesc.SampleDesc.Count = 1;

    //Liaison de la swapchain et de le fenêtre
    swapChainDesc.OutputWindow = *hWnd;
    swapChainDesc.Windowed = true;    

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
HRESULT codeErreur = D3D10CreateDeviceAndSwapChain( NULL, 
                                                    D3D10_DRIVER_TYPE_HARDWARE, 
                                                    NULL, 
                                                    D3D10_CREATE_DEVICE_DEBUG, 
                                                    D3D10_SDK_VERSION, 
                                                    &swapChainDesc, 
                                                    &this->SwapChain, 
                                                    &this->D3DDevice); 
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D10CreateDeviceAndSwapChain. Le premier paramètre représente l’adapteur à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D10_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D10_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini le numéro de version du SDK DirectX utilisé pour créer le programme.

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le sixième paramètre fourni la structure de description de cette SwapChain.

Les deux derniers paramètres sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

La méthode D3D10CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

    //Création du rendertargetview
    ID3D10Texture2D* backbuffer;
    if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D10Texture2D), (LPVOID*) &backbuffer)))
    {
        //Si erreur
        this->Error("Erreur lors de la récupération du backbuffer.");
        return false;
    }
    if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
    {
        //Si erreur
        this->Error("Erreur lors de la création du render target view.");
        return false;
    }
    backbuffer->Release();
    this->D3DDevice->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiquer où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupéré, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au device le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié aux profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

    //Création du viewport
    ViewPort.Width = width;
    ViewPort.Height = height;
    ViewPort.MinDepth = 0.0f;
    ViewPort.MaxDepth = 1.0f;
    ViewPort.TopLeftX = 0;
    ViewPort.TopLeftY = 0;
    //Assignation du viewport
    this->D3DDevice->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device gràce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le viewport.

  • Gestionnaire Direct3D 11

Le gestionnaire DirectX 11 est constitué d’un fichier DX11Manager.h contenant la définition de la classe et une classe DX11Manager.cpp contenant son implémentation.

  1. Définition de la classe DX11Manager
#pragma once
#include <Windows.h>
#include <D3D11.h>
#include "DXManager.h"

class DX11Manager : public DXManager
{
private:
    //Handle de la fenêtre
    HWND*                        hWnd;
    
    //Device (Lien avec la carte graphique
    ID3D11Device*                D3DDevice;
    //Swap Chain 
    IDXGISwapChain*                SwapChain;
    //Render Target
    ID3D11RenderTargetView*        RenderTargetView;
    //Viewport (zone d'affichage)
    D3D11_VIEWPORT                ViewPort;
    //Context du device
    ID3D11DeviceContext* d3dImmediateContext;

public:
    DX11Manager(void);
    ~DX11Manager(void);
    //Initialisation DirectX
    virtual bool Init(HWND* hWnd);
    //Rendu 3D
    virtual void Render();

private:
    //Message d'erreur
    virtual void Error(LPCSTR Message);
};

Comme indiqué plus haut, la classe DX11Manager hérite de la classe DXManager. Par conséquent, elle doit implémenter les méthodes Init,Render et Error décrite plus haut. Les méthodes supplémentaires de cette classe sont le constructeur et le destructeur (pour la libération des ressources). Au niveau des propriétés, on retrouve les propriétés décrites dans la section précédente.

Remarque Différences DX10/DX11 : Vous pouvez voir une différence entre la version DirectX 10 et 11. En effet, une nouvelle propriété à été rajoutée (ID3D11DeviceContext). Cela est dû à une nouvelle architecture de DirectX facilitant le multithreading. En effet, jusqu’à DirectX 10, il était impossible d’utiliser plusieurs threads pour envoyer des données à la carte graphique. Avec DirectX 11, il est possible de faire des appels à la carte graphique depuis plusieurs threads à l’aide de contexte.

directx-opengl5-w-157028-13

Chaque thread a un DeviceContext lié au Device. Un thread donne la liste de ses données à afficher à son DeviceContext. Une fois l’ensemble des données fournis, celle-ci sont donné au Device principal qui réalise l’affichage.

2.     Implémentation de la classe DX11Manager

Dans cette partie, nous allons nous interesser à la méthode Init.

//Initialisation DirectX
bool DX11Manager::Init(HWND* hWnd)
{
    //Sauvegarde du handle de la fenêtre
    this->hWnd = hWnd;
    
    //Récupération des dimensions de la fenêtre
    RECT rc;
    GetClientRect( *hWnd, &rc );
    UINT width = rc.right - rc.left;
    UINT height = rc.bottom - rc.top;

Ce morceau de code ne fait que sauvegarder le handle de la fenêtre (car il sera réutilisé plus loin) puis de récupérer les dimensions de la fenêtre.

//Création de la Swap Chain
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(DXGI_SWAP_CHAIN_DESC));

//Création de deux buffers (un back buffer et un front buffer)
swapChainDesc.BufferCount = 2;
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;;

//Définition du nombre d'image par secondes maximun (60 images).
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

//Multisampling setting
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.SampleDesc.Count = 1;

//Liaison de la swapchain et de le fenêtre
swapChainDesc.OutputWindow = *hWnd;
swapChainDesc.Windowed = true;  

On crée ensuite une structure décrivant la SwapChain. On indique tout d’abord le nombre de buffer que l’on souhaite utiliser (ici 2 car on compte utiliser FrontBuffer  et un BackBuffer). Il est nécessaire d’indiquer la taille des buffers (taille de la fenêtre) ainsi que le format des données pour les pixels (DXGI_FORMAT_R8G8B8A8_UNORM correspond à 8 bit pour chaque canal RGBA. La liste des formats de pixel est disponible sur le site MSDN.). La propriété BufferUsage indique que les buffers seront dessiné par un RenderTarget (Liste des BufferUsage disponible sur le site MSDN).La propriété RefreshRate permet de définir le nombre d’images par secondes maximum .La propriété SampleDesc permet de piloter le multisampling. Cette technique permet de réduire l’aliasing (effet de crénelage). Dans notre cas, nous allons la désactiver en mettant Quality à 0 et Count à 1 (en effet, ce domaine est vaste et nécessiterait un tutoriel à lui tout seul). Les deux dernières propriétés sont la définition de la fenêtre sur laquelle on souhaite faire apparaitre le résultat ainsi que si l’on souhaite que l’application fonctionne en plein écran ou non.

//Création du device
D3D_FEATURE_LEVEL FeatureLevels[] = {    
                                        D3D_FEATURE_LEVEL_11_0,
                                        D3D_FEATURE_LEVEL_10_1,
                                        D3D_FEATURE_LEVEL_10_0,
                                        D3D_FEATURE_LEVEL_9_3,
                                        D3D_FEATURE_LEVEL_9_2,
                                        D3D_FEATURE_LEVEL_9_1,
                                    };
int FeatureLevelCount = sizeof(FeatureLevels) / sizeof(D3D_FEATURE_LEVEL);
D3D_FEATURE_LEVEL SelectedFeatureLevel;
HRESULT codeErreur = D3D11CreateDeviceAndSwapChain(NULL, 
                                            D3D_DRIVER_TYPE_HARDWARE, 
                                            NULL, 
                                            D3D11_CREATE_DEVICE_DEBUG, 
                                            FeatureLevels, 
                                            FeatureLevelCount, 
                                            D3D11_SDK_VERSION, 
                                            &swapChainDesc, 
                                            &this->SwapChain, 
                                            &this->D3DDevice,
                                            &SelectedFeatureLevel,
                                            &this->d3dImmediateContext);
if (FAILED(codeErreur)) 
{
    //Si erreur
    this->Error("Erreur lors de la création du device 3D.");
    return false;
}

On arrive dans la partie de création du Device avec la méthode D3D11CreateDeviceAndSwapChain. Le premier paramètre représente l’adapter à utiliser. Cela correspond tout simplement aux GPU ou autre processeur vidéo que l’on souhaite utiliser. Si vous voulez utiliser l’adapter par défaut, il suffit de mettre NULL. Vous pouvez avoir la liste des adapters disponible grâce à la méthode EnumAdapters.

Le deuxième paramètre est le type de driver que vous souhaitez utiliser. Il y a deux valeurs qui sont utilisés principalement. D3D11_DRIVER_TYPE_HARDWARE indique que l’on souhaite que le rendu 3D soit réalisé par la carte graphique. Cela est le mode par défaut et le plus rapide. A l’inverse, le mode D3D11_DRIVER_TYPE_REFERENCE demande au CPU de prendre en charge le rendu 3D. Ce mode est très lent et est surtout utilisé si la carte graphique ne contient pas les instructions nécessaires pour réaliser le rendu (par exemple une rendu DX11 ne peut être fait sur une carte ne gérant que du DX10. On préfèrera dans ce cas utiliser le mode REFERENCE).

La troisième paramètre est le software rasterizer. Il s’agit d’un pointeur vers du code permettant de faire un rendu en plus de DirectX (sorte d’extension). Nous n’utiliserons jamais cela dont nous pouvons mettre NULL ici.

Le quatrième paramètre correspond aux attributs de création du device. Nous utiliserons un device de DEBUG afin de pouvoir avoir accès aux messages d’erreurs DirectX. Ce paramètre est un flag et peut être cumulés avec d’autres valeurs disponibles ici.

Le cinquième paramètre défini l’ensemble de FEATURE LEVEL à tester lors de la création du Device. Le paramètre suivant indique le nombre de FEATURE_LEVEL à tester. Les FEATURE_LEVEL sont des niveaux de fonctionnalités disponibles pour une carte. Ainsi, DirectX va tester si elle peut créer un device pour avec les fonctionnalités du premier FEATURE_LEVEL (dans notre cas DX11.0). S’il n’y arrive pas, il va tester avec le FEATURE_LEVEL suivant et ainsi de suite jusqu’à ce qu’il trouve un FEATURE_LEVEL compatible avec la carte.Il renverra l’info dans l’avant dernier paramètre (SelectedFeatureLevel).

La méthode utilisé ici crée le Device + la SwapChain. Afin de la créer, le septième paramètre fourni la structure de description de cette SwapChain.

Les deux suivants paramètres (8/9) sont des paramètres de sorties qui nous permettent de récupérer un pointeur sur le Device (D3DDevice) et sur la SwapChain.

Le dernier paramètre permet de récupérer le DeviceContext principal à utiliser. C’est au travers de celui ci que nous communiquerons avec la carte graphique (et non pas avec le D3DDevice comme en DX10).

La méthode D3D11CreateDeviceAndSwapChain renvoie une valeur HRESULT indiquant s’il y a eu une erreur ou non. La méthode FAILED() analyse le HRESULT et renvoie true s’il y a une erreur.

//Création du rendertargetview
ID3D11Texture2D* backbuffer;
if(FAILED(this->SwapChain->GetBuffer(0,__uuidof(ID3D11Texture2D), (LPVOID*) &backbuffer)))
{
    //Si erreur
    this->Error("Erreur lors de la récupération du backbuffer.");
    return false;
}
if(FAILED(this->D3DDevice->CreateRenderTargetView(backbuffer,NULL,&this->RenderTargetView)))
{
    //Si erreur
    this->Error("Erreur lors de la création du render target view.");
    return false;
}
backbuffer->Release();
this->d3dImmediateContext->OMSetRenderTargets(1,&this->RenderTargetView,NULL);

Maintenant nous allons créer le RenderTarget qui va indiqué où l’on souhaite faire le dessin de la scène. Tout d’abord il est nécessaire de récupérer le backbuffer créé par la SwapChain gràce à la méthode GetBuffer de celle-ci. Le premier paramètre est l’index du backbuffer que l’on souhaite (0 = premier back buffer). Le deuxième paramètre indique le type de données que l’on souhaite récupérer. On utilisera ici un ID3D10Texture2D représentant une texture en 2 dimensions.

Une fois récupérer, on peut créer le RenderTarget en fournissant le backbuffer. Le deuxième paramètre de CreateRenderTargetView  permet fournir une structure de description pour le RenderTarget. Dans notre cas, nous utiliserons celui par défaut d’où la valeur NULL.

Une fois cela défini, on libère la ressource prise pour le backbuffer (on en avait besoin uniquement pour défini le RenderTarget). Puis on indique au DeviceContext le RenderTarget à utiliser avec la méthode OMSetRenderTargets. Le premier paramètre indique le nombre de RenderTarget utilisé (ici 1). Le deuxième paramètre est un pointeur vers le RenderTarget choisi. Le dernier paramètre est un autre RenderTarget lié à la profondeur (DepthStencilView). Nous n’en aurons pas besoin ici => NULL.

//Création du viewport
ViewPort.Width = width;
ViewPort.Height = height;
ViewPort.MinDepth = 0.0f;
ViewPort.MaxDepth = 1.0f;
ViewPort.TopLeftX = 0;
ViewPort.TopLeftY = 0;
//Assignation du viewport
this->d3dImmediateContext->RSSetViewports(1,&this->ViewPort);

Dernière étape de l’initialisation : Le viewport.  Il suffit de remplir une structure D3D10_VIEWPORT et d’indiquer la taille (Width/Height), la position (TopLeftX/TopLeftY) ainsi que les profondeurs minimale et maximale (MinDepth/MaxDepth). Une fois défini,on indique ces information au Device grâce à la méthode RSSetViewports. Comme pour SetRenderTarget, le premier paramètre indique le nombre de viewport utilisé. Le deuxième est un pointeur vers la structure décrivant le Viewport.

  • La Méthode de rendu

L’objectif de ce tutoriel est surtout l’initialisation de Direct3D. Donc le rendu sera ici très sommaire. Nous allons uniquement vider le RenderTarget (donc le BackBuffer) en le remplissant d’une couleur (noire dans notre exemple) puis dire à la SwapChain d’afficher ce BackBuffer.

En DirectX 10, cela se fait comme suit :

//Rendu 3D
void DX10Manager::Render()
{
    this->D3DDevice->ClearRenderTargetView(this->RenderTargetView,D3DXCOLOR(0,0,0,0));
    
    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}

En DirectX 11 :

//Rendu 3D
void DX11Manager::Render()
{
    float clearColor[] = {0,0,0,0};
    this->d3dImmediateContext->ClearRenderTargetView(this->RenderTargetView, clearColor);

    //Dessin de la scène 3D

    this->SwapChain->Present(0,0);
}
  • Libération des ressources

La création de device, buffer et ainsi … consomme de la mémoire et bloque des ressources. Il est nécessaire, lorsque l’on termine le programme, de libérer ces ressources. Nous allons nous servir du destructeur de la classe pour réaliser ces opérations.

En DX 10 :

//Destructeur
DX10Manager::~DX10Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

En DX 11 :

//Destructeur
DX11Manager::~DX11Manager(void)
{
    //Libération des interfaces
    if(this->RenderTargetView) this->RenderTargetView->Release();
    if(this->SwapChain) this->SwapChain->Release();
    if(this->d3dImmediateContext) this->d3dImmediateContext->Release();
    if(this->D3DDevice) this->D3DDevice->Release();
}

La méthode Release se charge de libérer les ressources. Le if permet de vérifier si ces ressources n’ont pas déjà été libérées.

  • Conclusion

Nous avons vu comment initialiser Direct3D en Direct 10 et 11. Le résultat à l’exécution, bien que très simpliste, est le suivant :

dxt2-final1

Code source du tutorial

Ce tutorial est encore un peu frustrant (beaucoup de code pour un écran noir). Dans le prochain tutorial, nous allons afficher un triangle à l’écran. Nous verrons par la même occasion un outil indispensable en DirectX 10 / 11 : Les Shaders.

08/11/2010

[DirectX 10/11] Tutoriel 1 : Création d’une application Win32

Filed under: 3D, C++, Débutant, DirectX, Win32 — Étiquettes : , , , , , , — sebastiencourtois @ 00:09

J’ai fait une petite coupure avec ce blog compte tenu des préparatifs de mon départ aux USA et de l’avancement de certains projets persos. Au passage, je voulais souligner la création d’un nouveau blog d’un ex-futur-collègue qui vient de rejoindre la communauté des blogueurs .NET : Gilles Peron.

J’ai aussi décidé de me lancer dans une nouvelle série d’articles sur la programmation en parallèle de celle sur XNA. Le sujet est donc DirectX 10/11. L’une des grosses nouveautés est que nous allons faire ça à l’ancienne : en C++ (oui j’ai dû réapprendre à faire du C++ pour vous Sourire).

  • Introduction à DirectX

DirectX est une boite à outil pour la création de jeu vidéo fournie par Microsoft et fonctionnant principalement sur Windows. DirectX est composé de nombreux catégories chacune étant liée à une partie précise d’un jeu (Direct3D pour le graphisme 3D, DirectInput pour la gestion clavier/souris/manette, DirectPlay pour le réseau …).

Nous en sommes aujourd’hui à la onzième version de DirectX (sorti en 2009). Toutefois, DirectX 11 ajoute des nouvelles fonctionnalités à DirectX 10 qui était une évolution majeure de l’API. C’est pourquoi nous étudierons les deux versions 10 et 11.


  • Installation et configuration du kit de développement DirectX

Pour développer en DirectX, il faut :

    • Un environnement de développement C++
    • Le kit de développement Direct X : Page MSDN DirectX SDK.

Une fois le SDK installé, il suffit de créer  un projet C++ vide et d’ajouter les dossiers includes et libs du DirectX SDK (par défaut dans c:\Program Files\) au projet.

blog_dx_t1_0 blog_dx_t1_1 blog_dx_t1_2
Création d’un nouveau projet C++ vide Ajout du répertoire libs DirectX au projet Ajout du répertoire include DirectX au projet
  • La base d’une application DirectX : La fenêtre

Vous êtes maintenant prêt développer en DirectX. Pour cela, il est nécessaire d’avoir une fenêtre dans laquelle nous allons afficher les informations de notre jeu. Pour cela, plusieurs choix sont possibles. Dans ce tutoriel, je ferais ce que recommande Microsoft dans son tutoriel, l’utilisation de l’API Win32. Cette API complexe existe depuis que Windows existe et gère tout ce qui est fenêtre et gestion des entrées/sortie. D’autres méthodes de gestion de fenêtres plus simple existent (notamment QT) mais elles ne sont pas toutes compatible avec DirectX 10/11.

L’objectif de ce tutoriel va donc être de créer une fenêtre que l’on pourra fermer en appuyant sur la croix de la barre de titre ou en appuyant sur Echap.

Remarque : L’API Win32 et DirectX sont des API distinctes. Win32 est une API qu’il n’est pas spécialement nécessaire de maitriser parfaitement si votre objectif est uniquement de faire de la 3D ou du jeu vidéo. Dans le texte qui suit, je vais essayer de décrire au maximum chacune des propriétés. Il n’est pas nécessaire de mémoriser toute ces informations (ou alors uniquement pour votre culture général).

  • Le point d’entrée de notre programme : WinMain

Tout programme commence toujours par un point d’entrée. En C/C++ classique, ce point d’entrée est écrit ainsi :

#include <stdio.h>
int main(int argc,char **argv)
{
    printf("Hello World !!");
    return 0;
}

La même chose, en Win32, s’écrit de la façon suivante :

#include <stdio.h>
#include <Windows.h>
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    MessageBox(NULL,"Hello World","Titre",MB_ICONERROR);
    return 0;
}

Au niveau des changements entre les deux codes, on voit tout d’abord l’ajout d’un fichier windows.h. Ce fichier contient les fonctions principales de l’API Win32.

La signature du point d’entrée est plus complexe. Cette méthode retourne un nombre au système d’exploitation. La valeur 0 correspond à un programme qui s’est terminé correctement. Le mot APIENTRY signifie que la méthode est une méthode qui peut être appelé (CALLBACK). Le nom de la méthode WinMain est le nom du point d’entrée.Ce nom est défini dans windows.h et est obligatoire pour être reconnu comme un point d’entrée valide. Les deux paramètres suivants correspondent aux instances du programme. La première est l’instance pour le programme que le WinMain va lancer. La deuxième correspond à l’instance du programme qui appel ce WinMain. Dans notre cas, hPrevInstance sera NULL car nous lançons directement l’application (nous ne passons pas par un autre programme pour la lancer.). Le paramètre lpCmdLine correspond aux paramètres qui sont ajouté en arguement au lancement de l’application (équivalent du paramètre argv d’un main C/C++ classique).Pour récupérer la commande complète, vous pouvez aussi utiliser la méthode GetCommandLine. Enfin le dernier paramètre indique comment la fenêtre devra être affiché à son lancement (minimisé, plein écran …). Si vous voulez plus d’informations sur le WinMain, vous trouverez les informations voulues sur la MSDN.

Afin de tester que tout fonctionne correctement, nous allons demander l’affichage d’une boite de dialogue d’erreur. Celle ci est disponible gràce à la méthode MessageBox. Cette méthode prend en premier paramètre le handle (sorte de pointeur) sur la fenêtre qui l’a crée. Dans notre exemple, nous n’avons pas de fenêtre donc nous utiliserons la valeur NULL pour ce paramètre. Ensuite, il est nécessaire de fournir le texte puis le titre que l’on souhaite afficher. Enfin, on peut customiser les icones,boutons de la boite de dialogues avec le dernier paramètre. La liste et l’utilisation de ce paramètre est décrit en détail sur la MSDN.

Lorsque l’on lance ce morceau de code, on obtient le résultat suivant :

blog_dx_t1_3

  • Création d’une fenêtre de base

Nous allons maintenant chercher à créer une fenêtre normale. Pour cela nous allons rajouter 3 variables globales : Un Handle sur la fenêtre, deux nombre représentant la largeur et la hauteur de la fenêtre.

//Handle de la fenêtre
HWND hWnd;        
//Largeur de la fenêtre
int windowWidth = 800;    
//Hauteur de la fenêtre
int windowHeight = 600;

On modifie la méthode WinMain afin de créer et gérer un fenêtre.

//Point d'entrée du programme
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow )
{
    //Initialisation d'une fenêtre
    if(!InitWindow(hWnd,hInstance,nCmdShow)) return 0;

    // Boucle 
    MSG msg = {0};
    while (WM_QUIT != msg.message)
    {
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);            
        }    
    }
    return (int) msg.wParam;
}

Une première étape va être de créer et d’afficher la fenêtre. Cela va être fait par la méthode InitWindow que nous allons créer dans un moment.

Pour l’instant, nous allons nous concentrer sur la boucle. Windows gère ses fenêtres grâce à un système de message. Ainsi lorsqu’un ordre est donné à une fenêtre (minimiser la fenêtre, la fermer …), le système envoie un message à la fenêtre. Cette boucle regarde si des messages sont disponibles avec la méthode PeekMessage puis analyse le message et agit en conséquence (TranslateMessage) puis dispatch le message à d’autres fenêtres (les fenêtres enfants par exemples). On sort de la boucle lorsque le message de fermeture de l’application (Message WM_QUIT) est reçu par a fenêtre.

La valeur retour du WinMain dépend du wParam du dernier message. En effet, les messages peuvent aussi remonter des codes erreurs. Par conséquent, on utilise cette valeur plutôt que donner la valeur 0 en dur.

Concentrons-nous maintenant sur la création de la fenêtre proprement dite :

//Initialisation de la fenêtre Win32
bool InitWindow(HWND hWnd,HINSTANCE hInstance,int nCmdShow)
{
    //Structure d'information sur une fenêtre
    WNDCLASSEX wndStruct;
    ZeroMemory(&wndStruct,sizeof(WNDCLASSEX));
    wndStruct.cbSize        = sizeof(WNDCLASSEX);
    wndStruct.style            = CS_HREDRAW | CS_VREDRAW;
    wndStruct.lpfnWndProc    = (WNDPROC)wndProc;
    wndStruct.hInstance        = hInstance;
    wndStruct.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wndStruct.hbrBackground    = (HBRUSH)COLOR_WINDOW;
    wndStruct.lpszClassName    = TEXT("TutorialWindow");
    RegisterClassEx(&wndStruct);

    hWnd = CreateWindow( "TutorialWindow", 
                         "Tutorial 1 : Win32", 
                         WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                         CW_USEDEFAULT, 
                         CW_USEDEFAULT, 
                         windowWidth, 
                         windowHeight, 
                         NULL, 
                         NULL, 
                         hInstance, 
                         NULL);

    if(!hWnd) return false;

    ShowWindow(hWnd,nCmdShow);
    UpdateWindow(hWnd);

    return true;
}

Afin de créer une fenêtre, il est nécessaire de remplir une structure qui va décrire la fenêtre. Cette structure est WNDCLASSEX. Elle contient un grand nombre de propriété mais nous n’en utiliserons que 7 pour notre exemple.

cbSize Taille de la structure (en octets)
style Indique le comportement de la fenêtre. Dans notre cas, nous demandons à redessiner le contenu de la fenêtre lors qu’il y a  un changement vertical ou horizontal de la fenêtre. Les valeurs possibles sont disponibles ici.
lpfnWndProc Callback permettant de récupérer les évènements de la fenêtre (souris, clavier …). Nous allons créer cette méthode (wndProc) dans le paragraphe suivant.
hInstance Instance de l’application (celle fourni par le WinMain)
hCursor Curseur de souris lorsque celui ci passe sur la fenêtre (IDC_Arrow correspond au curseur de base).
hbrBackground Couleur de fond de la fenêtre
lpszClassName Nom pour la fenêtre. A ne pas confondre avec le titre de la fenêtre. Ce nom de fenêtre sera utilisé lors de la création de fenêtre pour identifier la structure WNDCLASSEX que l’on souhaite utiliser.

Plus d’informations sur la structure WNDCLASSEX sur la MSDN.

Une fois la structure de la fenêtre est définie, il est nécessaire de l’enregistrer dans le système afin de pouvoir la réutiliser. Cela se fait avec la méthode RegisterClassEx.

L’étape suivante est la création du handle de la fenêtre. On utilise pour cela la méthode CreateWindow. Le premier paramètre correspond au nom de la fenêtre comme décrit dans la structure WNDCLASSEX. Le deuxième paramètre est le titre de la fenêtre. Ensuite, on peut choisir le style de la fenêtre (barre de titre, bouton de fermeture …). La liste complète des possibilités se trouve ici. Les quatre paramètres suivants sont la position x,y du coin supérieur gauche de la fenêtre puis sa largeur et hauteur. La constante CW_USEDEFAULT est utilisé ici afin de laisser au système d’exploitation le choix de la position de la fenêtre. Le paramètre suivant indique si cette fenêtre doit être lié à une  autre fenêtre (ce n’est pas notre cas de figure donc NULL). Le NULL suivant correspond au handle d’un menu. Vu que nous ne souhaitons pas de menu, nous mettons une nouvelle fois NULL. Le paramètre qui suit n’est autre que l’instance à laquelle sera lié la fenêtre. Le dernier paramètre n’est pas utile dans notre cas (cas des fenêtres multiples MDI).

La description complète de cette méthode se trouve ici.

Une fois créé, on récupère un handle pour notre fenêtre. Si ce handle est NULL, c’est que la fenêtre n’a pas été créée. Dans le cas contraire, on peut l’afficher avec la méthode ShowWindow auquel on passe le paramètre nCmdShow reçu par le WinMain qui indique comment s’affiche la fenêtre (plein écran, minimisé…).

On appelle enfin la méthode UpdateWindow afin de forcer le rafraichissement de la fenêtre.

  • Réponses aux évènements de la fenêtre

Une fenêtre reçoit des évènements venant des périphériques (clavier,souris). Ce type d’évènements est à prendre en charge dans une méthode indiqué dans la propriété lpfnWndProc de la structure WNDCLASSEX (voir plus haut). Dans notre cas, elle s’appelle wndProc et à toujours la même signature.

LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_KEYDOWN    :    
            if(wParam == VK_ESCAPE)
                PostQuitMessage(0);
        break;        
        case WM_DESTROY    :    
            PostQuitMessage(0);
        break;
    }
    
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Cela fonctionne sur un système de message comme pour la boucle principale sauf que celle ci est spécifique à l’application. Lorsqu’un nouveau message pour la fenêtre arrive, la méthode wndProc est appelée. Nous gérons ici deux messages.

WN_DESTROY est reçu lorsque la fenêtre va être détruite. Cela peut provenir d’un clic sur la croix de fermeture de la fenêtre mais cela peut aussi venir d’autres facteurs comme le fermeture de l’application dans le gestionnaire de tâches ou encore l’extinction de l’ordinateur.

WM_KEYDOWN correspond à une touche du clavier qui a été enfoncée. Le code de la touche est disponible grâce au paramètre wParam.

Vous pouvez trouver ici la liste des messages pour wndProc.

On appelle la méthode PostQuitMessage avec un code retour 0 (aucun problème) pour envoyer un message de fermeture de l’application (donc de la fenêtre). Cela enverra un message WM_Quit à la boucle du WinMain qui sortira du programme.

  • La récompense

Nous sommes arrivés au bout de notre périple. Si vous lancez l’application, vous devriez vous retrouver avec la fenêtre suivante.

blog_dx_t1_4

Si ce n’est pas le cas, je vous donne mon projet de solution (VS 2010).

  • Conclusion

J’imagine votre première réaction à la fin de ce tutoriel : “Tout ça pour ….. ça Triste !!!“. Effectivement le code Win32 est assez indigeste, complexe (j’en connais qui aime ça Sourire) pour un résultat très médiocre. Il s’agit pourtant de la base de tout jeu vidéo DirectX et vous êtes obligé de passer par là pour commencer un projet. La bonne nouvelle est qu’une fois que c’est fait, on n’a plus à s’en préoccuper et on peut réutiliser le même code sans problème. Je vous conseille donc de  garder votre projet sous la main car vous pourriez en avoir besoin très bientôt.

17/09/2010

[XNA 4] Tutoriel 3 : Affichage de texte

Filed under: .NET, C# 4, Débutant, Jeux vidéos, XBOX 360, XNA — Étiquettes : , , , , , , , — sebastiencourtois @ 13:39

Après avoir appris à afficher et déplacer des images, nous allons apprendre à écrire du texte à l’écran. Pour ce tutoriel, nous partirons de ce projet (Solution des exercices du Tutoriel 2).

  • Création d’une police de caractère (font)

Afin de pouvoir afficher du texte, il faut indiquer quel police de caractères (aussi appelé font), on souhaite utiliser. Pour cela, on ajoute un nouvel élément au projet Content (Les fonts sont généralement stockés dans le projet Content comme l’ensemble des ressources XNA).

xna_tuto3_1

On sélectionne un élément de type “SpriteFont” en indiquant un nom et on clique sur OK.

xna_tuto3_2

A l’instar des images, les fonts ont aussi un assetName visible et modifiable par la fenêtre Propriétés. 

Si l’on ouvre le fichier contenant la police de caractères, on obtient le fichier XML suivant :

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
  <Asset Type="Graphics:FontDescription">
    <FontName>Kootenay</FontName>
    <Size>14</Size>
    <Spacing>0</Spacing>
    <UseKerning>true</UseKerning>
    <Style>Regular</Style>
    <CharacterRegions>
      <CharacterRegion>
        <Start>32</Start>
        <End>126</End>
      </CharacterRegion>
    </CharacterRegions>
  </Asset>
</XnaContent>

La balise FontName permet de définir le nom de la police utilisée. Les fonts disponibles sont uniquement des fonts de type TrueType (Liste). La communauté a créé des générateur de font personnalisé (Exemple : FontBuilder). Les plus utilisés sont souvent Arial ou Verdana.

La balise Size indique la taille du texte comme cela se fait dans les suites bureautiques.

La balise Spacing permet d’indiquer si l’on souhaite un espace entre les lettres (valeur en pixel).

La balise UseKerning permet de définir si l’on autorise le kerning. Pour faire simple, le kerning est le fait que deux lettres puissent “se superposer” (voir l’article et cette image qui décrit le kerning).

La balise Style indique si l’on souhaite mettre en gras/italique. Valeurs possibles : Regular, Bold, Italic ou Bold Italic.

La balise CharacterRegions défini les plages de valeurs ascii utilisable par la police (ici de 32 à 126 ce qui correspond au caractères de ‘ ‘ à ‘~’ en incluant les lettres en minuscules et majuscules, les chiffres ainsi que certains signes de ponctuations (cf tableau ASCII). Nous verrons qu’il est possible de rajouter d’autres plages.

  • Chargement de la police de caractères

Une fois défini, il est nécessaire de charger la police de caractères lors du lancement de l’application. Pour cela, on utilise le même principe que pour les textures.

SpriteFont textFont;

protected override void LoadContent()
{
    [...]
    this.textFont = Content.Load<SpriteFont>("MyFont");
}

Rappel : On utilise toujours le AssetName du fichier avec la méthode Load(). On doit donner aussi le type de sortie (SpriteFont pour les polices de caractères).

  • Affichage de texte

L’affichage du texte se déroule dans la méthode Draw() en utilisant le spriteBatch comme pour les textures.

Pour l’exemple, nous allons réutiliser l’exemple du chapitre précédent pour afficher en temps réel les coordonnées du tank, son angle de rotation ainsi que l’angle de rotation de son canon.

protected override void Draw(GameTime gameTime)
{
   GraphicsDevice.Clear(Color.CornflowerBlue);
   spriteBatch.Begin();

   string text1 = string.Format("Position du tank : X = {0}   Y = {1}",this.TankPosition.X,this.TankPosition.Y);
   spriteBatch.DrawString(this.textFont, text1, Vector2.Zero, Color.Red);
   string text2 = string.Format("Rotation du tank : {0}", TankRotation);
   spriteBatch.DrawString(this.textFont, text2, new Vector2(0,20), Color.DarkGreen);
   string text3 = string.Format("Rotation du canon : {0}", CanonRotation);
   spriteBatch.DrawString(this.textFont, text3, new Vector2(0, 40), Color.Yellow);

   spriteBatch.Draw(this.tank, TankPosition, null, Color.White, MathHelper.ToRadians(TankRotation), TankOriginPoint, 1.0f, SpriteEffects.None, 0);            
   spriteBatch.Draw(this.canon, TankPosition, null, Color.White, MathHelper.ToRadians(CanonRotation), new Vector2(canon.Width / 2 - 4.0f, canon.Height / 2), 1.0f, SpriteEffects.None, 0);
            
   spriteBatch.End();
   base.Draw(gameTime);
} 

On utilise la méthode DrawString () de spriteBatch. Cette méthode prend 3 paramètres :

  1. Police de caractères (SpriteFont)
  2. Position (en pixel à l’écran)
  3. Couleur du texte.

Le résultat en image :

xna_tuto3_3

  • Si je veux rajouter le ° à coté de l’angle.

Si vous tentez de faire le changement suivant :

string text2 = string.Format("Rotation du tank : {0} °", TankRotation);
spriteBatch.DrawString(this.textFont, text2, new Vector2(0,20), Color.DarkGreen);

Vous allez rencontre l’erreur : Argument Exception : The character ‘°’ (0x00b0) is not available in this SpriteFont. If applicable, adjust the font’s start and end CharacterRegions to include this character. Parameter name: character

Cela est dû au fait que la balise CharacterRegions ne contient pas ce  caractère dans sa liste de caractère disponible. (‘°’ a le code hexa 0xb0 => 176 en décimal). Il suffit de rajouter une range dans le fichier xml pour ajouter le caractère.

<CharacterRegions>
      <CharacterRegion>
        <Start>32</Start>
        <End>126</End>
      </CharacterRegion>
      <CharacterRegion>
        <Start>176</Start>
        <End>176</End>
      </CharacterRegion>
 </CharacterRegions>

Si vous recompilez et lancez l’application, le symbole apparait.

xna_tuto3_4

  • Conclusion

Vous pouvez maintenant écrire du texte à l’écran. Dans le prochain article, nous verrons l’utilisation du clavier/souris/manette XBOX 360 et Touch sur Windows Phone.

Code de ce tutorial

09/06/2010

[.NET 3.5] Attention lors de l’utilisation des lambdas/closures

Filed under: .NET, C#, C# 4, Hors Catégorie, Intermediaire — Étiquettes : , , , , — sebastiencourtois @ 13:05

Afin de commencer cette article, commençons par une petite devinette.

Quel sont les résultats de ces deux morceaux de code :

var actions = new List<Action>();
for (int i = 0; i < 10; i++)
    actions.Add(() => Console.WriteLine(i));

foreach (var action in actions)
    action();
var actions = new List<Action>();

for (int i = 0; i < 10; i++)
{
    int j = i;
    actions.Add(() => Console.WriteLine(j));
}

foreach (var action in actions)
    action();

La première réponse que vous avez du vous faire est que ces deux codes ont le même résultat : afficher les nombres de 0 à 9. En effet, on met des méthodes délégués dans une liste de délégués puis on parcourt cette liste afin d’exécuter le code de ces délégués. La seule différence entre les deux codes est l’utilisation d ‘une variable local j.

Et pourtant la réponse est :

  • Exemple 1 : 10 10 10 10 10 10 10 10 10 10
  • Exemple 2 : 0 1 2 3 4 5 6 7 8 9

Autre exemple dans le même genre :

var actions = new List<Action>();
string[] urls = 
{ 
   "http://www.url.com", 
   "http://www.someurl.com", 
   "http://www.someotherurl.com", 
   "http://www.yetanotherurl.com" 
};

for (int i = 0; i < urls.Length; i++)
{
    actions.Add(() => Console.WriteLine(urls[i]));
}
var actions = new List<Action>();
string[] urls = 
{ 
   "http://www.url.com", 
   "http://www.someurl.com", 
   "http://www.someotherurl.com", 
   "http://www.yetanotherurl.com" 
};

for (int i = 0; i < urls.Length; i++)
{
    int j = i;
    actions.Add(() => Console.WriteLine(urls[j]));
}
Résultat : IndexOutOfRangeException (Index was outside the

bounds of the array
Résultat : les liens du tableaux sont affichés
  • Les lambdas peuvent être nuire gravement à la santé … mentale des développeurs 

Etrange non ? Décompilons le tout première exemple afin devoir ce qui est effectivement exécuté.

Note : Pour une raison inconnue, je n’ai pas réussi à obtenir ceci depuis Reflector. J’ai donc récupérer le code de l’article original.

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    // Fields
    public int i;

    // Methods
    public void <Main>b__0()
    {
        Console.WriteLine(this.i);
    }
}
[...]
var actions = new List<Action>();

<>c__DisplayClass2 localFrame = new <>c__DisplayClass2();
for (localFrame.i = 0; localFrame.i < 10; localFrame.i++)
{
	actions.Add(localFrame.<Main>b__0);
}

foreach (var action in actions)
{
	action();
}

Comme on peut le voir, le fait de créer une lambda en utilisant une variable extérieure à celle ci à entrainer la génération d’une classe (<>c__DisplayClass2") qui est utilisé pour stocker la valeur de cette variable (dans la varaible i) et une méthode (<Main>b__0) qui contient le code de la lambda.  Si on regarde le code du premier for, on s’aperçoit que cette boucle incrémente la variable i de la classe DisplayClass2. Donc, à la fin de la première boucle, la classe DisplayClass2 contient une variable i = 10 et la liste actions contient une référence vers la méthode de cette classe. Ainsi lors de la boucle d’appel actions, on se retrouve a appeler le code Console.WriteLine(i) <=> Console.WriteLine(10) !!!!

Si on résonne de la même façon sur l’exemple 2, on obtient un appel Console.WriteLine(urls[4]) ==> Exception.

  • Pourquoi l’utilisation d’une variable locale résout le problème

Si on regarde le code ci dessous, on s’aperçoit que le problème vient du fait que la classe DisplayClass2 est créé en dehors de la boucle for et que celle ci utilise la variable i comme compteur. Le fait d’utiliser une variable temporaire à l’intérieur de la boucle for, oblige le compilateur à créer plusieurs instance de DisplayClass (une par itération). On obtient ainsi quelque chose comme le code suivant.

for (int idx = 0; idx < 10; idx++)

{

    <>c__DisplayClass2 localFrame = new <>c__DisplayClass2();

    localFrame.i = idx;

    actions.Add(localFrame.<Main>b__0);

}

Remarque : On pourrait penser que chaque instance est dispose lors de la sortie du scope. Cela n’’est pas vrai car la liste actiosn conserve une référence sur l’objet (en l’occurence sur une de ses méthodes) donc le garbage collector ne prend pas cet objet.

  • Conclusion

Les lambdas sont aujourd’hui utilisé dans de nombreuses technologies .NET (LINQ,PFX…) et sont une bonne alternative aux délégués classiques. Toutefois, on peut voir que cela peut engendré une certaine confusion voire des bugs lorsqu’on les utilise.

Je vous mets le lien vers l’article dont je me suis fortement inspiré pour cet article : Lambdas – Know your closures

09/05/2010

[Nouveautés C# .NET 4] Code Contracts

Filed under: .NET, C# 4, Débutant, DevLabs — Étiquettes : , , , , , , , — sebastiencourtois @ 16:12

Code Contracts est une fonctionnalité ajouté à .NET 4 cette année mais qui a, pendant des années, été un projet DevLabs sous le nom de Spec# ou Code Contracts. L’idée est de permettre au développeur de fournir des informations sur son code au travers du code lui même.

  • Un exemple concret vaut mieux que de long discours

Prenons un exemple d’une classe utilitaire.

public class Division
{
    public Division(decimal _num,decimal _denom)
    {
        this.Denominator = _denom;
        this.Numerator = _num;
    }

    public decimal Numerator { get; set; }
    public decimal Denominator { get; set; }

    public void Invert()
    {
        decimal tmpNum = this.Numerator;
        this.Numerator = this.Denominator;
        this.Denominator = tmpNum;
    }

    public decimal Compute()
    {
        return this.Numerator / this.Denominator;
    }

    public static decimal StaticCompute(decimal numerator, decimal denominator)
    {
        return numerator / denominator;
    }
}

public class Absolu
{
    public static decimal StaticCompute(decimal value)
    {
        return Math.Abs(value);
    }
}

La première classe de division permet de créer des fractions et d’éxécuter le résultat. Un méthode statique permet de faire la même opération sans créer une instance de la classe. Une deuxième classe qui permet, au travers d’une méthode statique, de récupérer la valeur absolu d’un nombre.

Si vous vous rappelez vos cours de mathématiques, vous savez surement qu’une valeur interdite dans le calcul d’une division est la valeur 0 pour le dénominateur. Si on regarde la méthode division, on voit que la valeur Dénominator peut être changé par le constructeur, par la propriété elle même (public get;set;), par la méthode Invert (si le numérateur = 0, alors l’invert donne un dénominateur = 0. Une solution avec .NET 3.5 serait de mettre un condition dans le set du numérateur afin de lancer une exception si une valeur = 0 tente d’être assigné. Toutefois cette façon de faire peut entrainer des codes dans les set assez long car il peut y avoir d’autres processus dans ce set ( NotifyPropertyChanged en WPF par exemple).

  • Contract Invariant / PréConditions (Requires)

Afin de sortir le code de validation des propriétés, Code Contract propose de créer une méthode dite “Invariant” où l’on mettra toute les conditions à vérifier lorsqu’une méthode ou un constructeur est exécuté. Pour la classe Divsion, on pourrait créer la méthode “Invariant” suivante :

[ContractInvariantMethod()]
protected void DivisionInvariant()
{
   Contract.Invariant(this.Denominator != 0, "Le dénominateur doit toujours être différent de 0.");
}

Ainsi si on tente de créer une instance Division d = new Division(2,0), on obtient l’exception suivantes

cc1jpg

Il est possible de faire cela sur des méthodes pour vérifier les paramètres par exemple.

public static decimal StaticCompute(decimal numerator, decimal denominator)
{
    Contract.Requires(denominator != 0, "Le dénominator doit être différent de 0.");
    return numerator / denominator;
}

Remarque : Le Contract.Requires fonctionne aussi pour les méthodes non statique.

  • PostConditions (Ensures)

Les méthodes ci-dessus permettent de vérifier les entrées. Il serait aussi intéressant de définir les sorties. Toujours grâce à vos rappels de mathématiques, vous savez que la méthode absolu renverra toujours une valeur supérieure ou égale à 0. Cela est une spécification d’une sortie de méthode. Ainsi la classe absolu ci-dessus, devient :

public class Absolu
{
    public static decimal StaticCompute(decimal value)
    {
        Contract.Ensures(Contract.Result<decimal>() >= 0);
        return Math.Abs(value);
    }
}

La méthode Ensures() indique que la condition qui suit est satisfait lors de la sortie de la méthode. La méthode Result() récupère le résultat afin de l’analyser. Il est aussi possible de vérifier l’état d’un paramètre au début de la méthode gràce à la méthode Contract.OldValue().

Si on remplace return Math.Abs(value) par return –1;, on obtient l’exception suivante :

cc2

  • Installation et Configuration

Afin d’utiliser les code contracts, il est nécessaire d’installer les outils Code Contracts : http://msdn.microsoft.com/fr-fr/devlabs/dd491992(en-us).aspx

Une fois installé, il est nécessaire d’aller dans les propriétés du projet pour activer Code Contracts :

cc3

L’écran ci-dessus dépend de votre version de Visual Studio.

    • VS Express 2010 : Aucune possibilité de d’activer Code Contracts
    • VS Pro 2010 : Runtime Checking  (Standard Edition)
    • VS Team System 2008 ou VS Premium / Ultimate 2010 : Runtime + Static Checkin.  (Premium Edition)

Juusqu’à maintenant, nous avons vu le Runtime Checking. Les vérifications se font lors de l’éxécution et génère des exceptions. Le Static Checking permet de voir ces problèmes dès la compilation lors d’une analyse statique de code.

  • Fonctionnement de Code Contracts

Les codes de vérifications Code Contracts ne sont pas utilisé uniquement à l’exécution mais aussi à la compilation. Lors de la compilation ,le compilateur analyse les codes et génère du IL pour l’insérer aux endroits nécessaire. Ainsi la méthode Compute de Division est représenté comme suit :

cc4 En Haut : Code généré sans code contract / En Bas : Code généré  avec Code Contracts

Il est donc possible de mettre ses codes Contracts.Requires/Contracts.Ensures dans n’importe quel ordre dans les méthodes car tout est reclassé lors de la compilation.

08/05/2010

[MS Research / DevLabs] Accelerator v2 : Exploiter vos GPU facilement en .NET

Filed under: .NET, DevLabs, Hors Catégorie — Étiquettes : , , , , , , , , — sebastiencourtois @ 17:47

Suite à ma visite sur les sites Internet de Microsoft Research et DevLabs, j’ai décidé de vous parler de ces projets de Microsoft qui seront dans les prochaines versions de .NET (ou pas). Dans les anciens project MS Research que l’on retrouve aujourd’hui au grand public, on pourrait cité WPF/Silverlight,Surface,PFX…

De ce post, nous allons parler d’une technologie dont le nom de code est “Accelerator”.

  • Présentation du contexte

De nos jours, les fabricants d’ordinateur nous fournisse des processeurs toujours plus puissants que ce soit en fréquence d’horloge qu’en nombre de cœurs. Ces nouvelles possibilités commencent à être exploiter correctement (notamment grâce à PFX de .NET 4).

Mais ce qui est moins exploiter est la puissance de calcul des cartes graphiques (GPU). Il faut savoir qu’une carte graphique peut avoir plusieurs processeurs dont chacun peut avoir de nombreux coeurs (jusqu’à 500 pour les plus récentes). Tout ce potentiel n’est utilisé que pour les jeux afin de faire des rendus et des calculs mathématiques afin de rendre des scènes 3D.

Le gros avantages des GPU est sa rapidité de calcul et sa gestion optimisé du parallélisme.

En revanche, les défauts des GPU par rapport aux CPU :

  • Les instructions sont limités aux opérations mathématiques 3D alors que le CPU peut avoir des opérations plus génériques
  • Les instructions dépendent beaucoup du constructeurs de la carte graphique (beaucoup plus que pour un CPU)
  • Il y a très peu d’API permettant d’accéder aux GPU directement (sans passer des API 3D).
  • Qu’est ce qu’Accelerator ?

L’idée du projet Accelerator est de fournir une API (Managé et C++) permettant d’envoyer du code sur ces GPU qu’il l’exécute.

D’autres ont déjà eu l’idée avant Microsoft :

  • Nvidia avec CUDA
  • ATI avec ATI Stream
  • Appel avec OpenCL
  • “Pixel Shaders”

Bien que ces API existent (encore au stade BETA pour la plupart), Microsoft a décidé de créé le projet Accelerator afin de fournir une API de développement pour les GPU indépendant de la plateforme et extensible.

  • Comment cela fonctionne ?

Accelerator se compose de deux gros blogs : Accelerator Library et Target Runtime.

image Structure général d’une application Accelerator

L’”Accelerator Library” est une API de développement pour préparer les données afin d’être exécutés sur le GPU. Cela se base sur un système abstrait à base d’arbre d’expression (nous le verrons plus loin). Cette API est composé d’une partie Native (C++) et d’une partie Managé (wrapper léger sur l’API native).

Le “Target Runtime” est une sorte de driver permettant de faire communiquer l’API avec les couches matériels (GPU … mais aussi CPU Multicore). Ce driver lit les arbres d’expressions d’”Accelarator Library” et envoie les instructions correspondantes aux GPU. Le driver est aussi en charge de la gestion de la charge sur les processeurs/cœurs. Microsoft fournit deux target runtime par défaut : Un DirectX9 (permettant d’accéder indépendamment à l’ensemble des carte graphiques du marché) et un pour les multi-coeurs CPU 64bits. Si vous souhaitez utilisé Accelerator sur d’autres types de processeurs, il suffit de développer un target runtime pour l’architecture cible.

Afin de comprendre le fonctionnement de Accelerator Library, prenons un exemple. Prenons deux tableaux contenant des valeurs à virgules flottantes (float). On souhaite additionner les cases une à une dans un troisième tableaux. Sur un CPU, on aurait une boucle for qui ferait les opération dans l’ordre des cellules et une cellule à la fois.

acceleratorv2 Exécution d’une addition sur un tableau (Sequential Program = CPU / Data Parallel Program = GPU)

Sur le GPU, le tableau est divisé et chaque cœurs à son propre lot de données à exécutés. Après exécution, le tableau est réassemblé à partir des résultats des tableaux de chacun des cœurs.

Afin que les opérations soient abstraites par rapport aux matériels sous jacent, Accelerator fournit un système d’arbre permettant de faciliter la création des instructions GPU. Par exemple, le graph suivant représente l’addition case par case de deux tableau puis la division de chacun des cases résultat par 2.

image Arbre d’expression Accelerator

  • Comment installer Accelerator ?

Accelerator v2 est actuellement disponible sur https://connect.microsoft.com/acceleratorv2.

Une fois installé, vous avez accès à  un document d’introduction et un document contenant l’ensemble des classes du projet. Je vous conseille le document d’introduction qui est très bien fait (j’en reprend un partie dans ce post).

Afin de développer, vous devez ajouter en référence l’assembly Microsoft.Accelerator.dll (contenu dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\Managed).

Attention : Pour l’exécuter, il est nécessaire de placer la dll Accelerator.dll (se trouvant dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\x86 ou C:\Program Files\Microsoft\Accelerator v2\bin\x64 selon votre architecture ) dans le répertoire où s’exécute l’application sous peine d’avoir l’exception : “DllNotFoundException : Unable to load DLL ‘Accelerator.dll’: Le module spécifié est introuvable. (Exception from HRESULT: 0x8007007E)”

  • Un Hello world “Accelrator”

En reprenant l’exemple d’addition case à case puis d’une division par 2, on obtient un code C# suivant (le reste du code a été enlevé afin de pas alourdir le code).

On commence par simplifier le code en utilisant des using.

using FPA = Microsoft.ParallelArrays.FloatParallelArray;
using PA = Microsoft.ParallelArrays.ParallelArrays;

FloatParallelArray représente un tableau de float sur la cible (carte graphique dans notre cas).. ParallelArrays regroupe les instruction disponible pour la cible.

DX9Target evalTarget = new DX9Target();

On crée le driver qui sera utilisé pour exécuter les opérations. Afin de faire simple, nous utiliserons le driver DirectX9 afin d’accéder aux GPU.

float[] inputArray1 = new float[arrayLength];
float[] inputArray2 = new float[arrayLength];
float[] stackedArray = new float[arrayLength];
/* Ajout de données dans les tableaux  inputArray1/inputArray2 */

On crée trois tableaux. Deux tableaux contenant les données d’entrée (inputArray1,2) que l’on remplit de valeurs diverses et un tableau pour les résultats.

FPA fpInput1 = new FPA(inputArray1);
FPA fpInput2 = new FPA(inputArray2);

On “transforme” les tableaux afin qu’il soit utilisables pour la cible.

FPA fpStacked = PA.Add(fpInput1, fpInput2);
FPA fpOutput = PA.Divide(fpStacked, 2);

On crée les opérations entre les tableaux. On utilise le résultat de l’addition pour la division. Cela génère l’arbre d’expression en copie d’écran plus haut.

evalTarget.ToArray(fpOutput, out stackedArray);

C’est la partie la plus importante du programme. En effet, jusqu’à maintenant nous avons préparé l’arbre d’expression et les données et c’est sur cette méthode, que l’arbre est évalué et que la cible (GPU dans notre cas) réalise les opérations. Le résultat est ensuite convertir et stocké dans un tableau de float .NET.

  • Et les performances alors ?

Il  est vrai que tout ça est un peu contraignant alors on est en droit de se demander si les performances sont aux rendez vous. Sur l’exemple ci dessus par exemple sur ma machine, les performances étaient meilleures en .NET qu’en GPU. La raison est, selon moi, que le temps de copie et d’éxécution est long pour le peu d’opérations exécutées.

Pour des données plus claires sur le domaine, je vous renvoie sur le PDF de MS Research parlant des performances : Document PDF . On peut voir que les performance peuvent être jusqu’à 70x plus rapide sur certains code par rapport au C++ sur CPU.

Le kit de développement contient aussi des samples (dont un jeu de la vie) qui permettent un peu de se rendre compte de ces performances.

Il est nécessaire de rappeler que ce projet est en alpha et donc loin d’être optimisé.

  • Conclusion

Ce projet est plein de promesses même s’il reste en dessous de technologies comme CUDA qui se rapproche du langage C alors que Accelerator est basé sur des arbres d’expressions / opérations mathématiques de bases. De plus Accelerator étant basé sur le parallélisme, il est nécessaire de rappeler que, comme PFX, tous les algorithmes ne sont pas faits pour ces technologies.

  • Quelques liens utiles :

Site du projet sur Microsoft Research (Attention c’est la v1 sur ce site)

Téléchargement de la v2

Article d’un français sur le sujet : Accelerator & F# / Accelerator : F# vs C#

03/05/2010

[.NET] Immutabilité et chaines de caractères en .NET

Filed under: .NET, C#, Débutant, Hors Catégorie, Intermediaire, Optimisation — Étiquettes : , , , , , , , — sebastiencourtois @ 15:31

Nous avons vu brièvement lors d’un post précédent un objet “immutable” (en anglais, immuable en vrai français) : Le Tuple”. Toutefois, nous ne sommes pas rentrés dans les détails de ce type d’objet et nous n’avons pas étudié le plus utilisé de tous : La classe String.

  • Définition de l’immutabilité

Un objet “immutable” est une objet dont les propriétés sont définies à la création de l’objet puis ne peuvent plus changer durant la vie de l’objet.

Une des raisons de la création de ce type d’objet (c’est d’ailleurs aussi un design pattern) : la gestion de la mémoire. En effet, vu que les données sont créées à la création de l’objet, il suffit d’allouer l’ensemble de la mémoire d’un coup et de ne plus y toucher jusqu’à la destruction de l’objet.

Un autre avantages est le multithreading. En effet, si l’objet n’évolue pas au cours de sa vie, tous les threads peuvent y accéder quand ils veulent sans avoir besoin de se synchroniser. Cela permet une programmation plus simple et un programme plus rapide.

  • Exemple concret d’immutabilité : String

Un objet “immutable” que nous utilisons tous les jours sans le savoir spécialement : la classe string.

Prenons l’exemple C/C++ suivant :

#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
    char *test = new char[7];
    strcpy(test,"Bonjour");
    printf("%c",test[2]);
    test[2] = 'N';
    printf("%c",test[2]);
}

Le programme copie une chaine (“bonjour”) dans un tableau de caractère puis affiche le 3ème caractères (‘n’) puis décide de modifier ce troisième caractères et de le réafficher. Tout cela se passe normalement, on obtient “BoNjour” à la fin de l’éxécution du programme.

Prenons le même type de programme en C# avec une string.

static void Main(string[] args)
{
    string test = "Bonjour le monde !!!";
    Console.WriteLine(test[2]);
    test[2] = 'N';
    Console.WriteLine(test[2]);
}

Rien ne laisse présager un problème, on utilise System.String comme une tableau de caractère et on modifie sa troisième valeur. Et pourtant on obtient l’erreur suivante :

immutable1

Un petit tour sur le code de la classe System.String et on voit en effet que l’indexer est uniquement en GET :

// Summary:
//     Represents text as a series of Unicode characters.
[Serializable]
[ComVisible(true)]
…….
    // Summary:
    //     Gets the character at a specified character position in the current System.String
    //     object.
    //
    // Parameters:
    //   index:
    //     A character position in the current string.
    //
    // Returns:
    //     A Unicode character.
    //
    // Exceptions:
    //   System.IndexOutOfRangeException:
    //     index is greater than or equal to the length of this object or less than
    //     zero.
    public char this[int index] { get; }

On peut donc voir que la classe String est bien immutable car sa valeur est fixé à sa création et après la chaine devient read only.

  • Les méthodes de la classe System.String

Vous pourriez vous dire, à raison, que la classe string contient des méthodes permettant modifier la chaine après création (ToUpper/ToLower). Or si vous remarquez bien, l’ensemble de ces méthodes retournent un type string. La chaine en paramètre est la chaine d’entrée et restera toujours à la même valeur alors que la valeur de retour sera une nouvelle chaine traité à partir de la première.

Un petit exemple tiré de la MSDN démontrant cela :

class Program
{
  static void Main(string[] args)
  {
     string testString = "A TEST STRING"; Console.WriteLine("testString: " + testString + Environment.NewLine);
     Console.WriteLine("Performing 'testString.ToLower();'");
     // Does not alter the string. Returns a new lowercase string, but           
     // we do not assign it to anything, so it is discarded            
    testString.ToLower();
    Console.WriteLine("testString: " + testString + Environment.NewLine);
    Console.WriteLine("Performing 'string lowerString = " + "testString.ToLower();'");
    // Assign the returned string to a new variable           
    string lowerString = testString.ToLower();
    Console.WriteLine("lowerString: {0}{1}testString: {2}{1}",
    lowerString, Environment.NewLine, testString);
    // Finally convert the test string to lowercase by assigning the             
    // result of ToLower back to itself. Note that this still doesn't            
    // result in the original testString object changing; instead, the            
    // old testString is discarded and the variable is set to the newly-            
    // created lowercase string object           
    Console.WriteLine("Performing 'testString = testString.ToLower();'");
    testString = testString.ToLower();
    Console.WriteLine("testString: " + testString);
    Console.ReadLine();
  }
}

  • Cas de la concaténation de chaines

L’utilisation du caractères ‘+’ pour la concaténation de chaines s’est énormément répandu depuis la sortie du .NET. Toutefois, il faut savoir que cela peut être désastreux pour les performance. En effet, chaque utilisation de ‘+’ entraine la création d’une nouvelle chaine.  Ainsi l’ajout de 1000 caractères un à un avec le caractère ‘+’ va entrainer la création de 1000 chaines de caractères allant de 0 à 999 caractères (999! caractères pour les matheux).

Dans ce cas, on préconise l’utilisation de la version “mutable” (comprendre : modifiable) de System.String : StringBuilder. On peut voir l’intérêt dans le test suivant :

static void Main(string[] args)
{
    for (int nb = 1; nb < 1000000; nb *= 10)
    {
        Stopwatch sw = Stopwatch.StartNew();
        string s = "";
        for (int i = 0; i < nb; i++)
            s += "T";
        sw.Stop();
        Console.WriteLine("Temps string \t {0} \t: {1}", nb, sw.Elapsed);

        Stopwatch sw2 = Stopwatch.StartNew();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < nb; i++)
            sb.Append("T");
        sw2.Stop();
        Console.WriteLine("Temps SB \t {0} \t: {1}", nb, sw2.Elapsed);
    }
    Console.ReadLine();
}

Résultat du test de performance :

Nombre Concaténation Temps String Temps StringBuilder
1,00 00:00:00.0000015 00:00:00.0000034
10,00 00:00:00.0000076 00:00:00.0000007
100,00 00:00:00.0000138 00:00:00.0000026
1 000,00 00:00:00.0011658 00:00:00.0000211
10 000,00 00:00:00.0388231 00:00:00.0001182
100 000,00 00:00:05.4556546 00:00:00.0011412

Le résultat est sans appel en faveur de StringBuilder qui est jusqu’à 500x plus rapide que la concaténation avec le caractère +.

  • Conclusion

Il faut donc faire attention lorsque l’on utilise des objets du framework afin de savoir s’ils sont “mutables” ou non car, bien qu’un objet “immutable” soit là pour permettre des meilleurs performances, cela peut s’avérer le contraire lorsqu’il est mal utilisé (exemple string VS StringBuilder).

26/04/2010

[Nouveautés .NET 4] Task Parallel Library : Gestion des architectures multicores/multiprocesseurs

Filed under: .NET, C# 4, Débutant, Hors Catégorie, Optimisation — Étiquettes : , , , , , , , , — sebastiencourtois @ 09:55

De nos jours, on ne trouve plus dans le marché de la vente d’ordinateurs que des architectures avec des processeurs double,quad,octo…. cores (et parfois même multi processeurs). Malgré cette débauche de puissance de calculs, il est triste à noter que nos applications ne sont, pour la plupart, pas tellement plus rapide qu’avant. La cause :

  • Les développeurs ne savent généralement pas coder des applications gérant plusieurs threads

PFX1 Ca fait mal d’avoir 4 coeurs et d’en utiliser qu’un seul… à moitié !!!

C’est un fait. Lorsque l’on apprend la programmation, on nous enseigne généralement beaucoup de choses mais rarement comment faire des applications gérant correctement plusieurs threads. L’une des raison est simple : Les API de développement sont généralement complexes et demande des efforts d’architecture/programmation/débug afin de rendre le tout utilisable. WaitHandle,Process,Mutex,lock,Interlocked … autant de mot clé et concepts barbare que le développeur doit maitriser afin de pouvoir décupler la puissance de ses applications. “On a déja du mal à faire une application correspondant aux spécifications dans le temps imparti, on va pas en plus passer des heures à gérer les multi coeurs”.

Partant de ce constat, Microsoft a créé Parallel FX afin d’aider le développeur à gérer facilement les threads au sein de ses applications.

  • Organisation de TPL

TPL se compose de 3 namespaces NET 4 :

  1. System.Threading.Tasks : Ce namespace contient la base de Task Parallel Library (TPL), il contient la classe principale de la technologie : la classe Tasks (sorte de “super Thread”)
  2. System.Collections.Concurrent : Ce namespace contient les collections pouvant être utilisé dans des contextes multithreads (thread safe).
  3. System.Linq : LINQ a été amélioré pour intégrer des aides au multithreading. Connu sous le nom de PLINQ (Parallel LINQ), il permet de paralléliser certaines étapes des requêtes LINQ afin de les rendre plus rapide.
  • Le namespace System.Threading.Task

Ce namespace contient une classe Task. Cette classe représente une opération asynchrone. Ainsi on défini notre tache et, lorsqu’on la démarre, celle-ci est assigné par le TaskScheduler à un des coeurs disponibles de la machine. Ce système marche sous la forme d’une  file (first in / first out) de priorité (il y a des VIP :)). Ce Taskscheduler est aussi capable de distribuer un grand nombre de tâches aux différentes cœurs et de balancer la charge si un cœur est plus occupé qu’un autre (un peu à l’instar des load balancing dans les serveurs web).

La création et l’utilisation des Task est simple :

Task t1 = Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < 200; i++)
        {
            if(i % 2 == 0)
                Console.WriteLine(i+" ");
        }
    });

Task t2 =new Task(() =>
{
    for (int i = 0; i < 200; i++)
    {
        if (i % 2 == 1)
            Console.WriteLine(i + " ");
    }
});
t2.Start();

Il y a,au moins, deux méthodes pour créer une Task. La première en utilisant une fabrique d’objet fournit par la classe Task. La seconde est de créer soi même une classe Task et de lui fournir la méthode à éxécuter puis de le lancer en appelant la méthode Start (un peu de la même façon qu’un thread. Il est à noter que l’on ne peut fournir de méthode retournant de valeurs. De plus, la méthode doit être sans paramètre ou alors avec un seul paramètres de type Object (Action, Action<object>).

L’autre nouveautés très sympathique est la possibilité de paralléliser facilement ses boucles. Si on part des exemples suivants :

foreach (string filename in Directory.GetFiles(MyDirectory, "*"))
    Console.WriteLine(filename);

for (int i = 0; i < 200; i++)
{
    if (i % 2 == 1)
        Console.WriteLine(i + " ");
}

Il est possible d’exécuter les itérations en parallèle en remplacement le for et le foreach par Parallel.For et Parallel.Foreach.

Parallel.ForEach(Directory.GetFiles(MyDirectory, "*"), (filename) =>
{
    Console.WriteLine(filename);
});

Parallel.For(0, 200, (i) =>
{
    if (i % 2 == 1)
        Console.WriteLine(i + " ");
});

Et c’est tout !!! Le taskscheduler s’occupe de la répartition des opérations sur le processeur. On peut ainsi passer d’un code compliqué de copie de fichier multithreadée :

public void SpeedCopyFolder(DirectoryInfo source, DirectoryInfo target, bool overrideExisting)
{
    using (ManualResetEvent mre = new ManualResetEvent(false))
    {
        int threadCount = 0;
        foreach (FileInfo fi in source.GetFiles())
        {
            Interlocked.Increment(ref threadCount);
            // Create a thread for each file
            FileInfo file = new FileInfo(fi.FullName); // Created for the delegate scope
            ThreadPool.QueueUserWorkItem(delegate
            {
                if (File.Exists(Path.Combine(target.ToString(), file.Name)) && !overrideExisting)
                    return;
                fi.CopyTo(Path.Combine(target.ToString(), file.Name), overrideExisting);
                if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
            });
        }
        if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
        mre.WaitOne();
    }
}

à un code beaucoup plus simple :

public static void CopyFiles(string fromFolder, string toFolder)
{
    Parallel.ForEach<string>(Directory.EnumerateFiles(fromFolder, "*"), f =>
    {
        File.Copy(f, toFolder + @"\" + Path.GetFileName(f), true);
    });
}

  • Le namespace System.Collections.Concurrent

Ce namespace contient des collections utilisables dans les cas de multithreading. Ces collections utilisent une interface créé spécialement pour l’occasion : IProducerConsumerCollection<T> qui permet la manipulation de ces collections.

Les collections ainsi ajoutés :

Les collections classiques fonctionnent toujours avec les tasks (sur les exemples que j’ai pu essayer.). Je pense qu’il doit y avoir des différences de performances/d’utilisations possibles qui doivent indiquer quel type utiliser. Je publierais surement un post quand j’en saurais un peu plus sur le sujet.

  • Parallel LINQ

PLINQ est une technologie qui permet de paralléliser vos requêtes LINQ.

Prenons l’exemple suivant : On souhaite récupérer les fichiers dont la première lettre est un ‘a’ puis classer ce résultat selon la dernière lettre du nom du fichier. On aurait là requête LINQ suivante  :

Directory.GetFiles(MyDirectory, "*").Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

Si l’on étudie un peu la façon dont la requête s’éxécute, LINQ va d’abord parcourir la liste des fichiers en recherche de ceux commençant par a puis il va les classer par ordre. Cela est logique pour un traitement mono-core. Toutefois, on pourrait imaginer faire le classement en parralèle du Where. Ainsi lorsque le Where trouve un élément commençant par un ‘a’, il est envoyé au Orderby (se trouvant dans un autre thread) qui réalise un classement en temps réel. Vous devez vous dire que cela doit être complexe à coder. Pourtant voici là façon de faire.

Directory.GetFiles(MyDirectory, "*").AsParallel().Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

Il suffit de rajouter un AsParallel() afin que toutes les expressions suivantes s’exécutent “simultanément”. “C’est pas wonderful ?!?!”

  • C’est bien beau tout ça mais … et les performances ?

Sur un exemple qui est très parlant pour PLINQ (BabyNames disponible sur le training Kit VS 2010), j’obtiens les résultats suivants (sur une machine 4 cœurs avec une sélection du nombre de cœurs utilisés pour l’application).

    Nombre de cœurs LINQ (temps en secondes) PLINQ (Temps en secondes) Amélioration
    1 37,71 38,57 x 0.98
    2 37,46 20,68 x 1.81
    3 37,14 12,82 x 2.90
    4 37,36 10,60 x 3.53

Remarque : La raison pour laquelle on n’arrive pas à 4x plus de performance avant un quad core est tout simplement du au fait que l’OS plus d’autres applications tourne en arrière plan avec des pics imprévisibles. De plus, les mécanismes de synchronisation entre thread prend aussi un peu de la puissance de calculs.

Pour terminer, un petit graph qui fait plaisir :

pfx2

  • Conclusion

Cet technologie va surement réconcilier de nombreux développeurs avec la gestion des applications multithreadés. Toutefois, il est à noter que, dans certains cas, le parallélisme peut nuire au performance de l’application (car cela implique toujours en tâches de fond des techniques de synchronisation). A utiliser avec parcimonie et toujours en testant les performances avec et sans TPL.

Remarque : TPL est disponible en C++ aussi.

Note : Ce post a été fortement inspiré du post de Gal Ratner : http://galratner.com/blogs/net/archive/2010/04/24/a-quick-lap-around-net-4-0-s-parallel-features.aspx

Older Posts »

Créez un site Web ou un blog gratuitement sur WordPress.com.