viernes, 19 de julio de 2013

Segundo Proyecto: Manejando los recursos

Tras acabar de implementar todos los estados y comprobar que todo el flujo del programa está correctamente programado, es hora de empezar a cargar cosas del disco o, lo que es lo mismo, manjar los recursos.

Los Recursos y Sus Problemas

La principal problemática de los recursos es que ocupan memoria, y por lo tanto hay que hacer que se utilice la menor cantidad de memoria posible.
Pensareis que con un PC de 6GB de memoria RAM no es necesario. En parte tenéis razón, salvo por algunos pequeños detalles:
  • Las Texturas se cargan en la memoria de video y esta es mas limitada. Se pueden cargar en memoria principal (Los 6GB) , pero mover datos desde la CPU hacia la GPU tiene un coste de tiempo.
  • Aunque estamos programando para PC, es posible que en el futuro tengáis que programar para alguna consola o dispositivo móvil y estos tienen poca memoria de video y principal, así que si tenéis la buena costumbre de usar el mínimo imprescindible para vuestros programas lo agradeceréis en el futuro.
Otros problemas con los que nos encontramos a la hora de manejar los recursos son:
  • Si cargamos demasiados recursos el programa se parara, es decir que tendremos que hacer una carga en otro hilo de programación para que podamos seguir pintando en la ventana y no se produzca un pantallazo fijo.
  • En memoria solo debe estar aquello que sea imprescindible, es decir, tendremos que hallar una forma de cargar lo que se necesiten y descargar aquellos recursos que ya no se usen más.

Hilos de programación

Un hilo de programación es una pequeña parte de código que se ejecuta en “paralelo” al hilo principal del programa.
El hilo principal se crea con la función main de nuestro programa. Cada vez que “durmamos” el hilo principal, con una función sleep, otro hilo se encarga de ejecutar su parte de código que se le asigna al crearse el hilo secundario.
Hay un gran problema en la programación multihilo que es la coordinación. Ambos hilos pueden acceder a la memoria general, pero no deberían hacerlo a la vez, pues el resultado es incierto.
La programación multihilo es un mundo en si mismo que ocuparía libros bien gordos.

Tipos de recursos

Todo lo que se cargue desde el exterior del programa se considera un recurso. Pongo exterior del programa porque se puede cargar desde el disco o descargar desde internet, pero esto ultimo es poco frecuente.
En SFML2 tenemos los siguientes recursos:
  • Imágenes que son los archivos del imágenes metidas en memoria de CPU
  • Texturas que son los archivos de imágenes de nuestro juego en memoria GPU
  • Sonidos
  • Música que se carga y gestiona de forma diferente al sonido
  • Fuentes de texto
  • Archivos de shader para hacer efectos especiales con la tarjeta gráfica
Los recursos están cargados y manejados de forma individual por una clase, la cual tiene funciones para cargar el recurso desde el disco o desde la memoria.

CARGAR Desde MEMORIA

Cargar desde la memoria tiene tres objetivos.
Cargar desde un recurso embebido en el ejecutable, esto es un archivo que ha sido insertado de forma binaria junto al ejecutable
Cargar desde un archivo comprimido. Los archivos comprimidos se descomprimen en memoria, obteniendo una imagen binaria del archivo original en la memoria desde donde se puede cargar
Cargar desde internet. A través de protocolos HTTP o FTP se puede descargar un archivo a nuestra memoria local y desde ahí cargar el recurso.

Recursos:There only can be one

La forma para asegurarnos que solo hay una única copia de un recurso en memoria será a través de la “referenciación”. Es decir llevaremos la cuenta de cuantas veces se carga y sumaremos +1 a un contador asociado con este recurso, también llevaremos la cuenta de cuantas veces se descarga el recurso y restaremos 1 al contador. Si al cargar el contador esta a cero es que debemos cargar desde el disco, si al descargar el contador llega a cero habrá que borrar el recurso de la memoria.
Si nos fijamos en los distintas clases que contienen los recursos observamos que hay tres tipos de formas de cargar un recurso:
Con loadFromFile se cargan:
  • sf::SoundBuffer
  • sf::Font
  • sf::Image
  • sf::Texture
Con openFromFile se carga:
  • sf::Music
Y con un loadFromFile distinto del primero:
  • sf::Shader
Esto parece trabajo para los templates. Solo como recordatorio, un template o plantilla es un trozo de código “especial” que se adapta al todo tipo de dato que necesitemos cuando se compila el programa. Consultar un libro de C++ sobre templates para obtener mas información.

Y, por fin, Algo de Código

#ifndef _XF_RESOURCELOADER_H_
#define _XF_RESOURCELOADER_H_

#include <string>
#include <map>

#include <SFML/System/Mutex.hpp>
#include <SFML/System/Lock.hpp>
#include <SFML/Graphics/Shader.hpp>

namespace xf
{
 struct resource
 {
  std::string m_source;
  void* m_res;
  unsigned m_ref;
 };

 class resourceLoader
 {
 public:
  void unref( const char* source );
  void unref( void* res );
  void clear();

 protected:
  std::map<std::string, resource >::iterator add( const char* source);
  std::map<std::string, resource > m_resourceMap;
  sf::Mutex m_resourceMutex;
 };

 template<class T>
 class resourceLoadFromFile:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();

    //Load resource
    T* object = new T;    
    if( object->loadFromFile( r.m_source ) )
    {
     r.m_res = static_cast<void*>(object);
     r.m_ref = 1;
    }
    else
    {
     delete object;
     r.m_res = 0;
     r.m_ref = 0;
    }

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };

 template<class T,sf::Shader::Type V>
 class resourceLoadFromFile2:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();

    //Load resource
    T* object = new T;    
    if( object->loadFromFile( r.m_source, V ) )
    {
     r.m_res = static_cast<void*>(object);
     r.m_ref = 1;
    }
    else
    {
     delete object;
     r.m_res = 0;
     r.m_ref = 0;
    }

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };

 template<class T>
 class resourceOpenFromFile:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();

    //Load resource
    T* object = new T;    
    if( object->openFromFile( r.m_source ) )
    {
     r.m_res = static_cast<void*>(object);
     r.m_ref = 1;
    }
    else
    {
     delete object;
     r.m_res = 0;
     r.m_ref = 0;
    }

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };

 template<class T>
 class resourceParseFile:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();
    //Load resource
    T* object = new T;    
    object->ParseFile( r.m_source );
    r.m_res = static_cast<void*>(object);

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };

 template<class T>
 class resourceOpenBinary:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();

    //Load resource
    T* object = new T;
    object->open( r.m_source, std::ios_base::in | std::ios_base::binary );
    r.m_res = static_cast<void*>(object);

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };

 template<class T>
 class resourceOpenText:public resourceLoader
 {
 public:
  T* ref( const char* source )
  {
   sf::Lock lock(m_resourceMutex);

   std::map<std::string, resource<T> >::iterator F = m_resourceMap.find( std::string(source) );
   if( F == m_resourceMap.end() )
   {
    F = add(source);
   }
   resource<T>& r = F->second;
   if(r.m_ref < 1 )
   {
    xf::loader::instance()->lockForLoading();
    //Load resource
    T* object = new T;
    object->open( r.m_source, std::ios_base::in );
    r.m_res = static_cast<void*>(object);

    xf::loader::instance()->unlockForLoading();
   }
   else
    ++r.m_ref;

   return static_cast<T*>(r.m_res);
  }
 };
}

#endif

Como podéis ver el sistema se basa en mantener un mapa de estructuras que contienen el nombre del archivo a cargar, un puntero al recurso que carga y un entero sin signo que nos dice cuantas veces se esta usando este recurso.

La clase base de manejo de recursos, básicamente implementa al referenciar (añadir), desreferenciar (borrar) y el limpiar todo para salir.

Las distintas clases templates que le siguen sirven para usarse con los distintos recursos de SFML2, y con recursos externos como el template  resourceParseFile o resourceOpenText que son genéricos.

También os daréis cuenta de la clase sf::Mutex m_resourceMutex; que nos servirá más adelante cuando implementemos la clase loader que cargara de forma asíncrona todos los recursos.

Y, ahora, la implementación:
#include "ResourceLoader.h"

namespace xf
{
 std::map<std::string, resource >::iterator resourceLoader::add( const char* source)
 {
  sf::Lock lock(m_resourceMutex);

  resource newResource;
  newResource.m_source = source;
  newResource.m_res = NULL;
  newResource.m_ref = 0;

  return (m_resourceMap.insert( std::make_pair<std::string, resource >( std::string(source), newResource ) )).first;
 }

 void resourceLoader::unref( const char* name )
 {
  sf::Lock lock(m_resourceMutex);

  std::map<std::string, resource >::iterator F = m_resourceMap.find( std::string(name) );
  if( F == m_resourceMap.end() ) return;
  resource& r = F->second;
  if( r.m_ref > 0 ) --r.m_ref;
  if(r.m_ref < 1 )
  {
   delete r.m_res;
   r.m_res = NULL;
  }
 }

 void resourceLoader::unref( void* res )
 {
  sf::Lock lock(m_resourceMutex);

  std::map<std::string, resource >::iterator I = m_resourceMap.begin();
  std::map<std::string, resource >::iterator E = m_resourceMap.end();

  while( I != E )
  {
   resource& r = I->second;

   if( r.m_res == res )
   {
    if( r.m_ref > 0 ) --r.m_ref;
    if(r.m_ref < 1 )
    {
     delete r.m_res;
     r.m_res = NULL;
    }
   }

   ++I;
  }
 }

 void resourceLoader::clear()
 {
  sf::Lock lock(m_resourceMutex);

  std::map<std::string, resource >::iterator I = m_resourceMap.begin();
  while( I != m_resourceMap.end() )
  {
   resource& r = I->second;
   if(r.m_res != NULL)
   {
    delete r.m_res;
    r.m_res = NULL;
    r.m_ref = 0;
   }
   ++I;
  }
  m_resourceMap.clear();   
 }
}

No hay comentarios: