Gestion intelligente des objets 3D à l’écran : Le Caching System (Partie 1)

Série de quatres tutoriaux consacré à la gestion intelligente des objets à l’écran.


  1. Le premier article sera consacré au caching (réduction de la charge mémoire).
  2. Le second à une gestion intelligente des objets à l’écran.
  3. Le troisième au culling pour améliorer les performances.
  4. Le quatrième à un système de messages pour décharger le traitement CPU

Le but étant au final de créer un moteur de jeu sur lequel nous allons nous déplacer.


Pour les débutants : Je ne saurais trop vous conseiller de lire les articles que j’ai traduit sur la MSDN fait par Derek Pierson. Si vous arrivez à suivre ces articles sans trop de difficultés, vous pouvez lire ce qui suit sans problèmes, pour les autres, posez vos questions ici.


 Au commencent était un moteur vierge sans optimisation


Encore une fois, je pars pour le programme d’exemple d’un sample du sdk DirectX que j’ai vidé pour ne garder que le framework. A partir de là j’ai utilisé le code de mon propre moteur (que je développe en parallèle) pour créer une version épurée, lente, mal conçue, qui ne demande qu’à être améliorée.


Le cahier des charges au départ est le suivant :


  • Créer et afficher un terrain.
  • Afficher 640 arbres
  • Afficher 5760 herbes

Rien de bien compliqué en somme. Environ deux heures de développement en Managed DirectX/C# (contre plusieurs jours en DirectX/C++ Standard [:#] ).


 


Si vous lancez le moteurs vous remarquerez trois énormes lacunes (voir image ci-dessous) :


  • Le jeu freeze au lancement le temps du chargement.
  • Le jeu est lent (4 à 5 fps sur mon PC).
  • Il consomme une place mémoire très importante.

 


Injouable !


 


Il s’agit là d’un problème typique de tout bon débutant en 3D : on veut afficher le maximum, épater la gallerie pour finir malheureusement sur un programme lent, inexploitable. 


A la fin de cet article nous aurons un chargement bien plus rapide (et asynchrone), une consommation mémoire réduite au maximum. Nous utiliserons pour cela un système de cache.


Il faut profiter de cette série d’articles pour bien assmiler le fonctionnement du moteur 3D afin de ne pas être trop perdu au fil du temps. Nous verrons étapes par étapes les différentes classes qui composent le framework sur lequel il repose (le moteur au final compte à peu pres 400 classes …).


 


Cache ?


Le problème actuel


Quel avantage va nous fournir un système de cache ?


Si vous avez déjà développé en 2D vous devez savoir qu’un jeu du type Mario est composé de petites cases ou “tiles” qui sont placées à l’écran pour former un monde de jeu. Bien souvent une partie de ces tiles sont répétées plusieurs fois à l’écran. Dans un moteur 3D la done est à peu près identique. Si vous prennez l’image du jeu ci dessus vous vous rendrez compte que je n’affiche que deux sortes d’arbres, un très “feuillu” et un peu “feuillu”. Chacun d’eux est répété à peu près 300 fois. Chaque arbre de compose d’un tronc et d’un feuillage. Pour chacun d’eux est associé un IndexBuffer et un VertexBuffer. Le moteur se voit donc obligé d’afficher à chaque Frame :


300* 2 Arbres = 600 objets 3D = 600 Feuillages et 600 troncs = 1200 VertexBuffers et 1200 IndexBuffers !!


(bon certes j’ai exagéré la nullité du moteur …)


Il y’a une solution beaucoup plus efficace en terme de mémoire : Nous pourrions juste instancier deux vertexbuffers/indexbuffers correspondant aux models feuillu et  non feuillu et répéter leur affichage dans le monde aux différentes positions et tailles voulues.


Ainsi, chaque classe Arbre ne possederai pas son propre le vertexbuffer et l’indexBuffer pour le tronc et le feuillage mais pointerai vers une classe du genre Mesh ou ProgressiveMesh mutualisée.


Solution


Un classe devra servir de manager afin de servir d’interlocuteur à tous nos objets métiers (arbre, Herbe, etc.) dans leur demande d’allocation de données 3D. Ce maager recevra une demande sous la forme d’un identifiant. S’il possède déjà un objet avec cet identifiant (l’identifiant peut être un path par exemple vers un fichier X) il le renvoie, sinon il le créé, puis le renvoie.


Une autre classe ItemCache devra servir d’encapsuleur vers les données 3D. Ces données peuvent être comme déjà précisé un objet de type Mesh ou ProgressiveMesh. Tous les objets métier demandant au manager un lien vers un même identifier pointeront vers le meme objet ItemCache.


La classe ItemCache possède une propriété Item qui pointe vers les données à mutualiser (un objet de type Mesh ou Texture par exemple). A ce stade Item vaut null. Deux possibilités pour l’allouer :


Soit en appellant spécifiquement LoadInCache pour ItemCache


Soit en accédant à la propriété Item pour la première fois. Au premier accès à l’accesseur Get de la propriété Item est créé, aux accès suivant l’objet créé est simplement renvoyée. Un système de délégate a été créé pour cela. Au premier accès à Item le delegate pointe vers une méthode de création, aux appels suivants le delegate pointe vers une méthode qui renvoie l’objet.


Là encore, ce système permet un chargement asynchrone : les traitements non synchrones sont placés dans le constructeur d’ItemBase, et les traitements synchrones (liés au device) dans une méthode spéciale nommée AllocateValue (vers laquelle pointe justement notre fameux delegate au départ).


La classe ItemBase est générique. L’argument T de specificité correspond à l’objet à mettre en cache (correspondant à la propriété Item dont je parlais plus haut). De même pour la classe manager.


Le projet contient une classe nommée OptimizationEngineCache (à télécharger ici) qui vous montre ce que donne le nouveau moteur : un chargement à la fois avec une fenêtre de progression et beaucoup plus rapide : seuls 5 modèles 3D sont chargés en mémoire en plus des données du terran : 2 types d’arbres et 3 types d’herbe (contre plusieurs milliers de vertexbuffers et indexbuffers auparavant je le rappel :) ).


 


Etude du code


 


Classes liées au cache 


 


 


Tout d’abord commandez par télécharger les samples associés à ces articles (lien en fin de post). Ouvrez le premier projet.


Etudions tout d’abord la classe qui correspond à la classe mère générique de notre manager.


 


 


 

    /// <summary>

    /// <para>Base caching system class.</para>

    /// </summary>

    /// <typeparam name=”T”></typeparam>

    public abstract class CachingSystemManagerBase<T>

    {

 

        +#region Private members

 

  

        #region Properties

 

 

        public Device Device

        {

            get

            {

                return this._device;

            }

            set

            {

                this._device = value;

            }

        }

 

        public List<CachedItemBase<T>> Items

        {

            get

            {

                return this._cache;

            }

        }

 

        internal class LastUtilisationComparer : IComparer

        {

 

            #region IComparer Members

 

            public int Compare(object x, object y)

            {

                CachedItemBase<T> c1 = x as CachedItemBase<T>;

                CachedItemBase<T> c2 = y as CachedItemBase<T>;

 

                if (c1.LastUtilisation < c2.LastUtilisation)

                    return 1;

                if (c1.LastUtilisation > c2.LastUtilisation)

                    return -1;

                else

                    return 0;

            }

 

            #endregion

        }

 

 

        #endregion

 

 

        #region Constructor

 

 

        /// <summary>

        /// <para>Hidden constructor.</para>

        /// </summary>

        protected CachingSystemManagerBase()

        {

            this._dictionnary = new Dictionary<string, CachedItemBase<T>>();

            this._cache = new List<CachedItemBase<T>>();

            this._sortedList = new SortedList(new LastUtilisationComparer());

        }

 

 

        #endregion

 

 

        #region Public methods

 

        /// <summary>

        /// <para>Allate space for a specified item.</para>

        /// </summary>

        /// <param name=”identifier”>Identifier of the item to allocate.</param>

        public CachedItemBase<T> AllocateItem(string identifier)

        {

            CachedItemBase<T> item;

            //if the item is already in cache

            if (this._dictionnary.ContainsKey(identifier))

            {

                //return it.

                item = this._dictionnary[identifier];

            }

            else

            {

                item = this.AllocateNewItem(identifier);

                this._dictionnary.Add(identifier, item);

                this._cache.Add(item);

                item.IndexedValue = this.currentIndex++;

            }

            item.NumberOfObjectsCurrentlyUsingThisItem++;

            return item;

        }

 

 

 

        public static void Clean()

        {

            //_sortedList.sor

        }

 

 

        #endregion

 

 

        #region Protected Methods

 

 

        /// <summary>

        /// <para>To be implemented in order to create the item in cache.</para>

        /// </summary>

        /// <returns></returns>

        protected abstract CachedItemBase<T> AllocateNewItem(string identifer);

 

        #endregion

 

    }

 

 

Deux méthodes importantes ici. Tout d’abord AllocateItem(string). Cette méthode renvoie un item mutualisé. Elle consulte un dictionnaire contenant tous les items mutalisés pour savoir si l’item dont on fourni l’identifiant est déjà présent. Dans ce cas elle le renvoie. Dans le cas contraire elle créé l’objet, place une préférence dans le dictionnaire et le renvoie. La propriété NumberOfObjectsCurrentlyUsingThisItem permet de détemriner le nombre de références faites vers un item en cache. De cette manière il est possible de savoir quand libérer un objet si celle-ci vaut 0 (à la manière du GarbageCollector). Vient ensuite la méthode AllocateNewItem qui a pour charge de créer un nouvel item et de le renvoyer. Cette méthode est bien entendu abstraite afin de laisser la possibilité au développeur d’implémenter lui-même la création des Items mutualisées. L’argument de spécificité T correspond au type des données à mutualiser.


Rien de bien compliqué ici. Voyons maintenant la classe CachedItemBase.


 


 


 


 

    /// <summary>

    /// <para>Base cached item class.</para>

    /// </summary>

    /// <typeparam name=”T”></typeparam>

    public abstract partial class CachedItemBase<T> : IDisposable

    {

 

        #region Private members

 

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private string _accessPath;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private CachingSystemManagerBase<CachedItemBase<T>> _cacheSystem;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private T _item;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private int _indexedValue;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private bool _disposed;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        private DateTime _lastUtilisation;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        protected bool _loaded;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]

        protected int _numberOfObjectsCurrentlyUsingThisItem;

 

        #endregion

 

 

        #region Properties

 

        /// <summary>

        /// <para>Index value if the current item is not in cache.</para>

        /// </summary>

        public const int NotInCache = -1;

 

        /// <summary>

        /// <para>Called when the current item is disposed.</para>

        /// </summary>

        public event EventHandler Disposed;

 

        /// <summary>

        /// <para>Called when the current item is allocated.</para>

        /// </summary>

        public event EventHandler Allocated;

 

        /// <summary>

        /// <para>Shunter to the value.</para>

        /// </summary>

        private ItemPeekerHandler<T> ShuntingToValue;

 

        /// <summary>

        /// <para>Gets the number of reference to the current item.</para>

        /// </summary>

        public int NumberOfObjectsCurrentlyUsingThisItem

        {

            get

            {

                return this._numberOfObjectsCurrentlyUsingThisItem;

            }

            internal set

            {

                this._numberOfObjectsCurrentlyUsingThisItem = value;

            }

        }

 

        /// <summary>

        /// <para>Gets the last item’s utilisation.</para>

        /// </summary>

        public DateTime LastUtilisation

        {

            get

            {

                return this._lastUtilisation;

            }

        }

 

        /// <summary>

        /// <para>Gets or sets the item’s path.</para>

        /// </summary>

        public string Path

        {

            get

            {

                return this._accessPath;

            }

            set

            {

                this._accessPath = value;

            }

        }

 

        /// <summary>

        /// <para>Gets the Indexed value of the current item inside the cache array.</para>

        /// </summary>

        public int IndexedValue

        {

            get

            {

                return this._indexedValue;

            }

            internal set

            {

                this._indexedValue = value;

            }

        }

 

        /// <summary>

        /// <para>Gets or sets the item.</para>

        /// </summary>

        public T Item

        {

            get

            {

                this._lastUtilisation = DateTime.Now;

                return this.ShuntingToValue();

            }

        }

 

        public CachingSystemManagerBase<CachedItemBase<T>> CachingSystem

        {

            get

            {

                return this._cacheSystem;

            }

            set

            {

                this._cacheSystem = value;

            }

        }

 

        public bool Loaded

        {

            get

            {

                return this._loaded;

            }

        }

 

        #endregion

 

 

        #region Constructor

 

        /// <summary>

        /// <para>Instanciate a sub class inherited from CachedItemBase.</para>

        /// </summary>

        /// <param name=”CachingSystem”></param>

        /// <param name=”path”></param>

        public CachedItemBase(string path)

        {

            this._accessPath = path;

            this._indexedValue = NotInCache;

            this._disposed = true;

        }

 

        /// <summary>

        /// <para>Destructor.</para>

        /// </summary>

        ~CachedItemBase()

        {

            // Simply call Dispose(false).

            Dispose(false);

        }

 

        #endregion

 

 

        #region IDisposable Members

 

        public void Dispose()

        {

            Dispose(true);

            GC.SuppressFinalize(this);

        }

 

        protected virtual void Dispose(bool disposing)

        {

            if (!this._disposed)

            {

                if (disposing)

                {

                    // Free other state (managed objects).

                }

                if (this._item is IDisposable)

                {

                    ((IDisposable)this._item).Dispose();

                    this._item = default(T);

                }

                this.ShuntingToValue -= new ItemPeekerHandler<T>(this.GetValue);

                this.ShuntingToValue += new ItemPeekerHandler<T>(this.AllocateValue);

            }

            this._loaded = false;

            this._disposed = true;

 

            if (this.Disposed != null)

                this.Disposed(this, EventArgs.Empty);

        }

 

        #endregion

 

 

        #region Private methods

 

 

        /// <summary>

        /// <para>Gets the cached value</para>

        /// </summary>

        /// <returns></returns>

        private ()

        {

            return this._item;

        }

 

        /// <summary>

        /// <para>Instanciate the objet by a calling to the implemented version of <see cref=”CachedItemBase.AllocateValue”/> and make a switch to made a direct access to the item.</para>

        /// </summary>

        /// <returns></returns>

        private T Instanciate()

        {

            this._item = this.AllocateValue();

 

            this.ShuntingToValue += new ItemPeekerHandler<T>(this.GetValue);

            this.ShuntingToValue -= new ItemPeekerHandler<T>(this.AllocateValue);

 

            if (this.Allocated != null)

                this.Allocated(this, EventArgs.Empty);

 

            return this._item;

        }

 

        #endregion

 

 

        #region Public methods

 

        /// <summary>

        /// <para>Allocate the cached value.</para>

        /// </summary>

        /// <returns></returns>

        public abstract T AllocateValue();

 

        public void LoadInCache()

        {

            this.Instanciate();

        }

 

 

        public void Release()

        {

            this.NumberOfObjectsCurrentlyUsingThisItem–;

        }

       

        #endregion

 

    }


 


 


 


Le premier élément à étuder est le delegate ShuntingToValue qui permet à la propriété Item de pointer sur la méthode qui créer l’objet mutualisé quand celui-ci n’existe pas ou sur celle renvoyant l’objet déjà créé. Ces deux métodes sont respectivement AllocateValue et GetValue. est abstraite afin de laisser toute liberté au développeur dans la création des données de l’item mutualisé. Le reste du code se déduit facilement à la lecture.


 


 


Reste du code


 

La classe Terrain est là pour gérer les regions qui sont des portions de monde de jeu. Elle possède un ensemble de constantes, propriétés et méthodes utiles pour la gestion du monde. La classe Region represente une portion carrée du terrain. La classe BadSample est la classe principale du programme (dans les samples DirectX Microsoft, c’est cette classe qui porte le nom du projet). La classe ObjectBase et M2ObjectBase sont les classes utilisées pour l’affichage des arbres. M2Reader est une classe utilisée pour la lecture des fichiers M2 (equivalent des fichiers X). Enfin pour terminer TerrainGeneratorCircleAlgorithm est une classe Utilitaire pour créer un relief réaliste. Nous reviendrons dans un prochain post sur toutes les méthodes existantes pour la création de reliefs réalistes.


 

 

Modification et améliorations entre le projet “BadSample” et “FirstOptimizationWithCache”


 


 


 


Un répertoire “Caching” a été ajouté contenant les deux classes liées au cache et deux implémentations de celles-ci. Ces dernières nommées


CachingSystemManagerBase et CachedMesh mutualisent un objet de type RenderBuffers. Cette classe se trouvant dans un autre nouveau répertoire nommé “Rendering” contient un ensemble de SubSet. Un subset est tout simplement une association d’un ProgressiveMesh et d’un Material. (voir post sur les ProgressiveMesh ici).


 


La classe Region est donc modifiée : elle ne contient plus de VertexBuffer et d’IndexBuffer mais un ProgressiveMesh. La classe M2ObjetBase ne contient plus de VertexBuffer et d’IndexBuffer mais un CachedMesh.



 



Samples 


 


Le moteur affiche environ 180 FPS à ce jour (preuve ici) en 800*600. Le caching que nous venons de voir n’y est pour rien : c’est principalement le culling et l’affichage intelligent des objets qui permet cette performance. Par contre le caching aide à un chargement rapide et libère surtout la mémoire vidéo.


Click here to increase image Size


telecharger Vous pouvez télécharger le moteur ici.

telecharger Vous pouvez télécharger la version mauvaise ici.


Conclusion


[Soon]


Valentin Billotte

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>