Go Back

ResourceManager.h

/*
  All Content is Copyright 2017 DigiPen Institute of Technology

  Author:
  Jake McLeman
*/

#ifndef RESOURCE_MANAGER_H
#define RESOURCE_MANAGER_H

#include <queue>
#include <deque>

#include "Resource.h"
#include "Threadpool.h"
#include "Engine.h"

#include "MutexHelper.h"

#include "DebugTools.h"

#include "System.h"

namespace AubergineEngine
{
  class ResourceManager : public System
  {
  public:
    /*
      Creates a resource manager

      Set should load value to true, and set shut down signal to false
      
      Creates a StaticJobQueue object to use as
      threadpool type with defined number of worker threads
      and initializes resource map and to load queue
    */
    ResourceManager(Engine* engine);

    /*
      Clean up the threadpool, after stopping the threads if necessary
    */
    ~ResourceManager();

    /*
      Add a Resource to the manager with a given load priority, then
      returns a pointer to that resource. Be sure to check the resource's
      GetIsLoaded() flag before using as it is being loaded on another
      thread

      Priority 0 is load as soon as there is a free thread,
      higher priority values indicate lower position in the queue

      If the requested resource is already loaded, returns a pointer
      to that resource. Be sure to check the resource's
      GetIsLoaded() flag before using as it is being loaded on another
      thread and may not have been loaded yet, it may simply be in the queue.

      There are two varieties of this function, the first enables the user
      to specify a callback function, to be called when a resource has been
      loaded, the second assumes no callback.
    */
    template <typename T>
    T* AddResource(std::string filename, unsigned int priority, LoadCallback callback = nullptr);

    /*
      Add a Resource to the manager with default maximum load priority, then
      returns a pointer to that resource. Be sure to check the resource's
      GetIsLoaded() flag before using as it is being loaded on another
      thread

      If the requested resource is already loaded, returns a pointer
      to that resource. Be sure to check the resource's
      GetIsLoaded() flag before using as it is being loaded on another
      thread and may not have been loaded yet, it may simply be in the queue.
    */
    template <typename T>
    T* GetResource(std::string filename);

    /*
      Load any resources that were requested that cannot be loaded on
      background threads
    */
    void LoadSyncLockedResources();

    /*
      Given a text file the resource manager will add all of the resources
      listed within it to the queue with a given type and priority. This enables
      the manager to start loading a set of assets before anything in the game
      has requested them to reduce wait time
    */
    void PreloadResourcesFrom(std::string filename);
    
    /*
      Spin down the thread pool and prepare for shut down
    */
    void Cleanup();

    /*
      Start up the ResourceLoading worker threads
    */
    void StartLoading();
    
  private:
    /*
      Function object to use with the toLoadQueue

      Handed to std::priority_queue to use as its sorting operator
    */
    struct ResourceCompare
    {
      bool operator() (const Resource* lhs, const Resource* rhs)
      {
        return lhs->GetPriority() > rhs->GetPriority();
      }
    };

    /*
      Implementation of the engine System interface
    */
    void UpdateSpace(Space* space, float DeltaTime);
    void UpdateSystem(float DeltaTime);

    /*
      Queue of resources to load
    */
    std::priority_queue<Resource*, std::vector<Resource*>, ResourceCompare> toLoadSyncQueue_;
    
    /*
      Map of all currently resources that have been requested

      NOTE that a resource being in here is NOT a guaruntee of
      that resource already being loaded, so be sure to check
      members before using
    */
    std::map<std::string, Resource*> resources_;

    /*
      Since the load queue will potentially  be accessed by multiple 
      threads simultaneously, declare a mutex variable that can be
      checked by each job using this list before using it to ensure
      that things don't get fucked up

      I know there is sometimes a red squiggle here. VS doesn't 
      understand that this is a macro call. The compiler likes 
      it though, so too bad VS.
    */
    DECLARE_MUTEX_VAR(toLoadSyncQueue_);
    DECLARE_MUTEX_VAR(resources_);

    /*
      Pool of worker threads to handle the loading
    */
    ThreadPool::ThreadPool* workers_;

    //Should the system currently be loading resources?
    bool shouldLoad_;

    DECLARE_JOB(ResourceManager, LoadResource, Resource* resource);

    /*
      Declare the FileWatcher job privately
    */
    DECLARE_JOB(ResourceManager, CheckFilesForUpdate, int delay);
  };

  template<typename T>
  inline T* ResourceManager::AddResource(std::string filename, unsigned int priority, LoadCallback callback)
  {
    //If the resource is already in the system, return it
    if (resources_.find(filename.c_str()) != resources_.end())
    {
      return dynamic_cast<T*>(resources_[filename.c_str()]);
    }
    //Otherwise, add it to the appropriate load queue
    else
    {
      Resource* toLoad = new T(filename.c_str(), priority, callback);
      toLoad->SetManager(this);
            
      if (T::ThreadSafeLoad)
      {
        LoadResource(workers_, toLoad);
      }
      else
      {
        //Gain mutex lock on the queue
        GET_LOCK(toLoadSyncQueue_);
        toLoadSyncQueue_.push(toLoad);
        //Release mutex lock on the queue
        RELEASE_LOCK(toLoadSyncQueue_);
      }

      //Gain mutex lock on the resource library
      GET_LOCK(resources_);
      resources_[filename.c_str()] = toLoad;
      //Release mutex lock on the resource library
      RELEASE_LOCK(resources_);

      return dynamic_cast<T*>(toLoad);
    }
  }

  template<typename T>
  inline T* ResourceManager::GetResource(std::string filename)
  {
    return AddResource<T>(filename, 0);
  }
}

#endif
   

ResourceManager.cpp

/*
  All Content is Copyright 2017 DigiPen Institute of Technology

  Author:
  Jake McLeman
*/

#include "stdafx.h"
#include "ResourceManager.h"
#include "DynamicJobQueue.h"

#include "Font.h"
#include "LuaScript.h"
#include "Texture.h"
#include "Shader.h"
#include "SpineSkeleton.h"

#include "SystemReport.h"

#define LOADER_THREADS 2

#define LOADER_THREAD_DELAY 1
#define UPDATE_THREAD_DELAY 1

namespace AubergineEngine
{
  /* 
    Constructor, implements Engine System Constructor 
  */
  ResourceManager::ResourceManager(Engine* engine) : System(engine, "ResourceManager"), shouldLoad_(true)
  {
    workers_ = new ThreadPool::DynamicJobQueue(SystemUtilReport::GetNumCores() - 1);
    toLoadSyncQueue_ = std::priority_queue<Resource*, std::vector<Resource*>, ResourceCompare>();
    resources_ = std::map<std::string, Resource*>();
  }

  /*
    Destructor, Cleans up threads and removes worker pool
  */
  ResourceManager::~ResourceManager()
  {
    Cleanup();
    delete workers_;
  }

  /*
    Spin down all worker threads and delete resources
  */
  void ResourceManager::Cleanup()
  {
    workers_->ForceStop();
    workers_->WaitForComplete();

    for (auto& resource : resources_)
    {
      delete resource.second;
      resource.second = nullptr;
    }
  }

  /*
    Load the resources that must be loaded on the main thread
  */
  void ResourceManager::LoadSyncLockedResources()
  {
    while (!toLoadSyncQueue_.empty())
    {
      Resource* load = toLoadSyncQueue_.top();
      toLoadSyncQueue_.pop();

      load->Load();
    }
  }

  /*
    Read in a file of the form:
    ResourceType ResourceName ResourcePriority

    Add these resources to the load queues and library to start loading
    assets before any objects have requested them
  */
  void ResourceManager::PreloadResourcesFrom(std::string filename)
  {
    std::ifstream list(filename);

    if (list.is_open())
    {
      while (!list.eof())
      {
        std::string type, name;
        unsigned int priority;

        list >> type >> name >> priority;

        if (type == "Texture") AddResource<Texture>(name, priority);
        else if (type == "Font") AddResource<Font>(name, priority);
        else if (type == "Shader") AddResource<Graphics::Shader>(name, priority);
        else if (type == "LuaScript") AddResource<LuaScript>(name, priority);
        else if (type == "SpineSkeleton") AddResource<SpineSkeleton>(name, priority);
        else
        {
          engine_->PushWarning("Unrecognized resource type in preload list");
        }
      }
    }
    else
    {
      engine_->PushWarning("Resource Preload File Failed to Open");
    }
  }

  /*
    Start the loader threads spinning
  */
  void ResourceManager::StartLoading()
  {
    CheckFilesForUpdate(workers_, UPDATE_THREAD_DELAY);

    workers_->Start();
  }

  /*
    Intentionally empty, necessary to implement the Engine System interface
  */
  void ResourceManager::UpdateSpace(Space * space, float DeltaTime)
  {
  }

  /*
    Necessary to implement the Engine System interface
  */
  void ResourceManager::UpdateSystem(float DeltaTime)
  {
    LoadSyncLockedResources();
  }

  /*
    Job that runs on another thread that checks files for update
    and then adds itself to the back of the job queue
  */
  DEFINE_JOB(ResourceManager, CheckFilesForUpdate, int delay)
  {
    for (auto& resource : resources_)
    {
      try
      {
        //If a resource is loaded but has changed since it was loaded
        if (resource.second->GetIsLoaded()
          && resource.second->NeedsUpdate())
        {
          //If it is safe to do so, add it to the load queue
          if (resource.second->IsLoadThreadSafe())
          {
            LoadResource(workers_, resource.second);
          }
          //Otherwise, add it to the synchronus load queue
          else
          {
            GET_LOCK(toLoadSyncQueue_);
            toLoadSyncQueue_.push(resource.second);
            RELEASE_LOCK(toLoadSyncQueue_);
          }
        }
      }
      /*
        In order to prevent this process from being stopped, catch any possible
        error and report it to the user.
      */
      catch (...)
      {
        engine_->PushWarning(resource.first + " failed to check for update");
      }

      /*
        Sleep to prevent using all the CPU while idling
      */
      std::this_thread::sleep_for(std::chrono::duration<int, std::milli>(delay));
    }


    //Add this job back into the job queue
    CheckFilesForUpdate(workers_, UPDATE_THREAD_DELAY);
  }

  /*
    Job to load a resource
  */
  DEFINE_JOB(ResourceManager, LoadResource, Resource* resource)
  {
    resource->Load();
  }


}