La dernière fois nous avons parlé des concepts de base des meshes. Dans ma tête, on avait fait le tour des bases de Direct3D. Maintenant qu'on a les fondements, j'aimerais traiter des détails qu'on a omis en introduction. Cette fois je parelrai de la perte du device.
La perte du device arrive lorsque quelquechose d'externe prend le contrôle du matériel vidéo. Direct3D ne fait qu'agir intimement avec le materiél vidéo, alors pour certaines choses, il n'y a de la place que pour une application en même temps (ndt : je vois pas le rapport). Quand Windows décide qu'une application différente - ou Windows lui même - doit avoir l'exlusivité de la carte graphique, nous sommes kickés et "perdons" le device.
Le device Direct3D peut être perdu suite à certains évenements : la mise en route des économiseurs d'écran, le verrouillage de l'ordi, ou la mise en veille de la machine, tout cela causera la perte du device. Sur mon portable, ça arrive aussi quand je ferme et reouvre le couvercle. Une des première choses qui peut nous indiquer la perte du device est l'appel à Device.Present.
Lorsque nous appelons Device.Present après la perte du device pour une quelconque raison, Direct3D levera une DeviceLostException. Ce qui est intéressant ici est que Direct3D fait en fait un tas de travail pour que les autres méthodes ne lèvent pas cette exception. Elles ne feront pas ce qu'elles étaient censées faire, mais elle ne vont pas echouer non plus. Ceci nous permet de centraliser la gestion de la perte du device, plutot que de s'embêter à capturer une DeviceLostException à chaque fois qu'on fait quelquechose avec Direct3D.
Que faire quand le device est perdu? Et bien probablement rien! Vous voyez, la cause de la perte est peut être toujours en cours : l'économiseur d'écran tourne encore, l'utilisateur n'a pas encore déverrouillé l'ecran, etc. Alors la seule chose qu'on a vraiment besoin de faire est d'enregistrer le fait que le device a été perdu. Nous pourrions mettre le code suivant à la fin de notre méthode Render :
try { // Copie le back buffer à l'écran device.Present(); } catch (DeviceLostException) { // Indique que le device a été perdu deviceLost = true; // Envoyer ce message à la fenêtre du debugger Debug.WriteLine("Device was lost"); }
La seule petite chose que ceci fait est de mettre deviceLost à vrai lorsque DeviceLostException est levée.
deviceLost est juste un champ booléen privé que j'ai défini dans la classe, comme ceci :
// Le device a t il été perdu et non-reinitialisé? private bool deviceLost;
Notez que j'ai rajouté un appel à Debug.WriteLine. Ceci est pratique car si vous faites votre application avec le symbole DEBUG défini (ceci devrait se faire automatiquement si vous sélectionnez la configuration Debug dans VS.NET), vous obtiendrez un message dans l'onglet Output du debugger.
L'autre chose que nous voudrions peut être faire dans ce bloc catch est de mettre en pause le jeu/simulateur/application. Etant donné que nous n'allons pas être capable de dessiner à l'écran pendant un petit moment, ça serait pas bête de mettre en pause jusqu'à ce que le device soit récupéré. L'utilisateur ne serait pas trop heureux d'apprendre qu'un monstre s'est déplacé et a mangé ses personnages parcequ'il aurait malencontreusement verouillé l'ordinateur et ne l'a pas déverouillé suffisament rapidement!
OK, nous savons à présent que le device a été perdu. Maintenant nous voulons savoir quand on pourra reprendre le dessin et que faire à ce moment là. La méthode qui nous permet de reprendre le dessin est Device.TestCooperativeLevel. Nous pouvons appeler cette méthode au début de notre procédure Render et ça continuera à lancer des DeviceLostException jusqu'à ce que le device ne soit plus perdu. Une fois qu'il arrête de lever cette exception, le device n'est plus perdu et nous sommes presque prêts à redessiner.
Il y a un truc à noter sur ce point. Etant donné que le device a été utilisé par une autre application (ou probablement Windows lui meme) lorsqu'il fut perdu, nous n'avons aucune idée de l'état du matériel. En fait il n'est pas garanti que les VertexBuffers alloués sont toujours là, ils auront pu être détruit entre temps.
Direct3D sait que les choses pourraient ne pas être disposés comme nous le voulons et ainsi ne nous permettra pas de re-afficher des choses jusqu'à ce que le device soit réinitialisé. En l'occurence, la reinitialisation du device est presque identique que la première initialisation : nous devons configurer les bons "RenderStates", mettre en place les matrices de monde, de projection et de vue, re-créer chaque VertexBuffer dont on a besoin, etc...
En fait, il y a 2 choses qu'on doit faire différemment. Nous devons libérer toutes les ressources allouées avant que le device soit perdu et nous devons faire un appel à Device.Reset. Autrement, nous pouvons réutiliser le code d'init qu'on a déjà.
Pour faciliter la reprise d'une perte de device, l'API Managed Direct3D propose une paire d'évenements (au sens C# du terme) sur l'objet Device. Chacun de ces évenements est provoqué par la méthode Device.Reset. Le premier, DeviceLost, est provoqué tôt dans la méthode Reset, ce qui est le bon moment pour faire du nettoyage, comme appeler Dispose sur chaque VertexBuffer existant. Le second, DeviceReset, est appelé plus tard ce qui est le bon moment pour faire la réallocation.
C'est assez dur de rester clair donc laissez moi montrer ce que je veux dire. Nous avons déjà vu les chagements faits à la fin de la méthode Render. Voici les changements que j'ai fait au début :
protected void Render() { if (deviceLost) { // Essaye de reprendre le device AttemptRecovery(); } // Si nous avons pas pu ravoir le device, n'essaye pas de faire le rendu if (deviceLost) { return; }
Le reste de la méthode ne change pas.
Les modifications ici sont assez simples : si le device est perdu, tente de le récuperer. Si, apres tentative de récuperation, le device reste perdu, quitte la méthode sans rien afficher. Bien sûr j'ai concentré les détails de la reprise du device dans la méthode AttemptRecovery que voici :
protected void AttemptRecovery() { try { device.TestCooperativeLevel(); } catch (DeviceLostException) { } catch (DeviceNotResetException) { try { device.Reset(pres); deviceLost = false; // Lance un message à la fenêtre du debugger Debug.WriteLine("Device successfully reset"); } catch (DeviceLostException) { // Si c'est toujours perdu ou que ca a encore été perdu, ne fait rien } } }
AttemptRecovery est assez évident. Nous appelons TestCooperativeLevel pour voir si on peut essayer de reinitialiser le device. Si le device est toujours perdu on obtiendra uneDeviceLostException, que nous ignorons simplement. Si par contre, le device n'est plus perdu mais n'a pas encore été reinitialisé nous obtiendrons une DeviceNotResetException à la place. Dans ce cas, nous appelons simplement la méthode Reset (en passans les memes PresentParameters qu'on a utilise à la création du device) et positionnons deviceLost à faux. Bien sûr le device aura pu être perdu encore entre le moment où on a appelé TestCooperativeLevel et Reset, alors nous traitons explicitement ce cas et ignorons simplement l'exception DeviceLostException.
Maintenant, souvenez vous que j'ai dit qu'on ne doit pas qu'appeler Reset sur le device, mais on doit aussi réallouer les VertexBuffers et autres resources qui auraient pu être créées. Ca arrive vraiment, mais ça arrive car lorsque j'ai créé le device, j'ai accorché des methodes aux évenements DeviceLost et DeviceReset mentionné plus haut. Voici la méthode InitializeGraphics que j'ai utilisée pour cela :
protected bool InitializeGraphics() { // Configure nos paramètres de présentation comme d'habitude pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Accroche la méthode OnDeviceReset à l'évenement DeviceReset // afin qu'elle soit appelée a chaque appel de device.Reset() device.DeviceReset += new EventHandler(this.OnDeviceReset); // Pareillement, OnDeviceLost sera appelé à chaque appel de // device.Reset(). La seule différence est que DeviceLost est appelée // plus tôt, nous donnant une chance de faire le nettoyage // necessaire avant qu'on puisse appeler Reset() avec succès device.DeviceLost += new EventHandler(this.OnDeviceLost); // Fait la configuration initiale de nos objets graphiques SetupDevice(); return true; }
Notez le code d'enregistrement des évenements. C'est malheureux de constater que les concepteurs de Managed Direct3D aient décidé d'utiliser un délégué de type EventHandler plutot qu'un truc plus spécifique à Direct3D, mais c'est comme ça.
L'appel à SetupDevice est là où on crée le VertexBuffer et configurons les "RenderStates", comme tout ce qu'on a fait jusqu'à present. Il n'y a rien de bien nouveau, mais la voici :
protected void SetupDevice() { // Met en place les RenderStates du device device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; // Et crée le VertexBuffer vertices = CreateVertexBuffer(device); }
CreateVertexBuffer est la même méthode qu'on a déjà utilisé pour créer le triangle tricolore double face.
Nous devons mettre en place 2 évenements différents car ils sont appelés à des moments différents. Ils sont tous les deux appelés pendant l'exécution de Device.Reset, mais DeviceLost est appelé au début, nous donnant la chance de libérer chaque resources allouée avant d'en réallouer d'autres.
Voici à quoi ressemble le gestionnaire OnDeviceLost :
protected void OnDeviceLost(object sender, EventArgs e) { // Libère le VertexBuffer vertices.Dispose(); }
Vu que la seule resource qu'on gère dans cet exemple est un simple VertexBuffer, nous n'avons qu'une chose à libérer. Si nous gérions plus de trucs, nous devrions les faire Disposer aussi.
L'autre évenement - DeviceReset - est appelé plus tard dans la procédure Reset, à un stade où c'est ok pour recréer les resources nécessaires. Comme je l'ai dit, c'est typiquement la même chose que ce qu'on fait à l'initialisation de l'application, donc notre méthode OnDeviceReset ne fait qu'appeler SetupDevice, comme ceci :
protected void OnDeviceReset(object sender, EventArgs e) { // Nous utilisons le même code de configuration pour réinitialiser que pour initialiser SetupDevice(); }
Et voilà. Evidemment, plus l'appplication est compliquée et plus il y a de ressources à gérer, plus le code de gestion des ressources dans le cycle de Reset sera compliqué. Mais rappelez vous que vous pouvez enregistrer plus d'un gestionnaire d'évenement pour un évenement donné, ainsi un code comme celui ci :
device.DeviceLost += new EventHandler(object1.OnDeviceLost); device.DeviceLost += new EventHandler(object2.OnDeviceLost); device.DeviceLost += new EventHandler(object3.OnDeviceLost);
pourrait vous aider à écrire un code plus propre.
Le code que je montre ici marche bien en mode fenêtré, mais il y a plein de gens qui me demandent comment faire marcher cela en mode plein écran. Nous n'avons pas encore parlé du mode plein écran, mais j'inclus le code dont vous aurez besoin pour épargner des problèmes à pas mal de gens. Assurez vous juste d'ajouter cette ligne à InitializeGraphics :
device.DeviceResizing += new System.ComponentModel.CancelEventHandler(this.CancelResize);
Et ensuite ajoutez cette méthode à la classe :
protected void CancelResize(object sender, System.ComponentModel.CancelEventArgs e) { e.Cancel = true; }
Nous verrons pourquoi ceci est nécessaire dans un futur tutoriel.
Comme d'habitude, j'ai fourni une application qui fonctionne à la fin de l'article. Essayez donc, et peut être comparez la à une des applications précédentes qui ne gère pas la reprise du device. Regardez ce qu'il se passe lorsque vous verrouillez l'écran ou interrompez l'ordinateur.
La prochaine fois, nous parlerons des IndexBuffers, une optimisation intéressante utilisée par les Meshes pour économiser de la mémoire et accélérer le rendu.
using System; using System.Drawing; using System.Windows.Forms; using System.Diagnostics; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; private VertexBuffer vertices; // Le device a t il été perdu et non re-initialisé? private bool deviceLost; // Nous aurons besoin de ceux la pour appeler Reset, donc gardons les ici private PresentParameters pres = new PresentParameters(); protected bool InitializeGraphics() { // Configurons nos paramètres de présentation comme d'hab pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Accroche la méthode OnDeviceReset à l'évenement DeviceReset // afin qu'elles soit appelée a chaque appel de device.Reset() device.DeviceReset += new EventHandler(this.OnDeviceReset); // Pareillement, OnDeviceLost sera appelé à chaque appel de // device.Reset(). La seule différence est que DeviceLost est appelée // plus tôt, nous donnant une chance de faire le nettoyage // necessaire avant qu'on puisse appeler Reset() avec succès device.DeviceLost += new EventHandler(this.OnDeviceLost); // Fait la configuration initiale de nos objets graphiques SetupDevice(); return true; } protected void OnDeviceReset(object sender, EventArgs e) { // Nous utilisons le meme code pour la reinitialisation que pour l'initialisation SetupDevice(); } protected void OnDeviceLost(object sender, EventArgs e) { // Libère le VertexBuffer vertices.Dispose(); } protected void SetupDevice() { // Configure les "RenderStates" du device device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; // Crée le VertexBuffer vertices = CreateVertexBuffer(device); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // Type de sommets 3, // Combien device, // Le device 0, // Utilisation par défaut CustomVertex.PositionColored.Format, // Format de sommets Pool.Default); // Pooling par défaut CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( 0, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 5.0F); } protected void Render() { if (deviceLost) { // Tente de reprendre le device AttemptRecovery(); } // Si le device n'a pas pu être récupéré, n'essaye pas de faire le rendu if (deviceLost) { return; } // Efface le back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Prépare Direct3D à dessiner device.BeginScene(); // Configure les matrices SetupMatrices(); // Nous allons dessiner des sommets colorés en 3D device.VertexFormat = CustomVertex.PositionColored.Format; // Dessine la scène - Instructions 3D ici device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); // Indique à Direct3D qu'on a fini de dessiner device.EndScene(); try { // Copie le back buffer à l'écran device.Present(); } catch (DeviceLostException) { // Indique que le device est perdu deviceLost = true; // Affiche un message dans la fenêtre du débugger Debug.WriteLine("Device was lost"); } } protected void AttemptRecovery() { try { device.TestCooperativeLevel(); } catch (DeviceLostException) { } catch (DeviceNotResetException) { try { device.Reset(pres); deviceLost = false; // Affiche un message dans la fenêtre du débugger Debug.WriteLine("Device successfully reset"); } catch (DeviceLostException) { // Si il est toujours perdu ou qu'il a encore été perdu // ne fais rien } } } } }