Make a custom resource provider

From CEGUI Wiki - Crazy Eddie's GUI System (Open Source)
Revision as of 19:16, 16 January 2016 by Iceiceice (Talk | contribs) (Created page with "Note: This page is a work in progress, targetted at CEGUI v0.8.4 The <code>ResourceProvider</code> is the interface that CEGUI uses to load data files of all kinds -- XML, im...")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Note: This page is a work in progress, targetted at CEGUI v0.8.4

The ResourceProvider is the interface that CEGUI uses to load data files of all kinds -- XML, images, fonts, etc.

If you followed the tutorial, then you are probably already vaguely familiar with the DefaultResourceProvider. The default resource provider requires you to specify certain folders for each kind of resource. When CEGUI needs to acquire a resource, the default resource provider simply tries to fetch it from disk, accounting for differences between Unix-like and Windows systems.

There are a few other resource providers that come with CEGUI, particularly, an OGRE resource provider, and Irrlicht one, which integrate with those systems more tightly.

However, maybe you want to write your own resource provider. Why might you want to do this?

  1. Maybe you want to limit how CEGUI accesses your filesystem. Perhaps you have some kind of virtual filesystem setup, and you prefer for security reasons to have all filesystem calls go through your interface rather than directly through the C / C++ filesystem API. Maybe you are trying to cope with bugs when handling unicode characters in filepaths on some platforms.
  2. Maybe you are targetting a platform where the filesystem is a bit unusual -- for instance, in emscripten.
  3. Maybe you are trying to optimize the resource acquisition process by making it cache all available on-disk resources up front for some scenario, and then serve them fast from buffers to CEGUI after that.

To do this, you simply need to derive from CEGUI::ResourceProvider, and start CEGUI using an instance of that instead of the default. This page is to help explain in greater detail what these overrided methods that you provide should do, what responsibilities they have, when they will be called, etc. And especially, to explain how the ownership of the various involved objects works, so that you can avoid leaking / creating undefined behavior.

ResourceProvider

Here is a synopsis of the CEGUI::ResourceProvider class and its most important methods:

   class CEGUIEXPORT ResourceProvider :
       public AllocatedObject<ResourceProvider>
   {
   public:
       ResourceProvider() { }
       virtual ~ResourceProvider() {}
   
       virtual void loadRawDataContainer(const String& filename, RawDataContainer& output, const String& resourceGroup) = 0;
       virtual void unloadRawDataContainer(RawDataContainer&)  { }
   };

The AllocatedObject template is essentially a CEGUI internal detail related to memory management. You don't need to do anything special as a result of that inheritance -- you can just new objects of this type, or derived from this type, and pass them to CEGUI as an appropriate argument to CEGUI::System::create. CEGUI will take ownership of them and delete them as necessary.

The two most important functions are the two that I picked out -- CEGUI calls these when it wants to load or unload a resource. It expects your resource provider to manipulate these RawDataContainer objects to pass it data.

RawDataContainer

RawDataContainer is somewhat like a stripped-down std::vector, except that it always points to an buffer of type unsigned char and there are no "resize" methods or similar. It is always easy to get / set the size / data pointer of the RawDataContainer, as it is intended that you should manipulate the class to get the semantics that you want.

Synopsis of RawDataContainer:

   class CEGUIEXPORT RawDataContainer :
       public AllocatedObject<RawDataContainer>
   {
   public:
   
       RawDataContainer() : mData(0), mSize(0) {}
       ~RawDataContainer() { release(); }
   
       void setData(uint8* data) { mData = data; }
       uint8* getDataPtr() { return mData; }
   
       void setSize(size_t size) { mSize = size; }
       size_t getSize() const { return mSize; }
   
       void release();
   
   private:
       uint8* mData;
       size_t mSize;
   };

The release function of a RawDataContainer calls operator delete[] on the buffer pointer, unless the buffer pointer is null. If that would cause a problem for you, then the unloadRawDataContainer function is your opportunity to empty the RawDataContainer before it is destroyed so that you can do cleanup in a different way.

Examples

For example: If you are using a C++ function like this to open files,

   typedef unsigned char uchar;
   
   std::pair<uchar *, size_t> open_file(const std::string & file) {
     if (FILE * source = fopen(file.str().c_str(), "rb")) {
       fseek(source, 0, SEEK_END);
       int size = ftell(source);
       fseek(source, 0, SEEK_SET);
   
       uchar * buffer = new uchar[size];
       size_t num_read = fread(buffer, size, 1, source);
       (void)num_read;
       fclose(source);
   
       return std::make_pair(buffer, size);
     }
     throw file_not_found_exception(file);
   }

you can overload loadRawDataContainer like this:

   void myResourceProvider::loadRawDataContainer(const String& filename, RawDataContainer& output, const String& resourceGroup) {
     std::pair<uchar *, size_t> result = open_file(filename);
     output.setData(result.first);
     output.setSize(result.second);
   }

You then don't need to override unloadRawDataContainer, as CEGUI will call delete[] later on its own. CEGUI owns the buffer that you loaded in this case. However, if you implemented open_file using malloc instead of new[], you will get undefined behavior. Or even, if you compiled your project with one compiler and CEGUI with a different compiler, you may have problems if their memory allocation implementations are not compatible.

If YOU want to own the buffer and not have CEGUI take ownership of it, you might do it like this instead:

   typedef unsigned char uchar;
   
   std::vector<uchar> open_file(const std::string & file) {
     if (FILE * source = fopen(file.str().c_str(), "rb")) {
       std::vector<uchar> buffer;
   
       fseek(source, 0, SEEK_END);
       int size = ftell(source);
       fseek(source, 0, SEEK_SET);
   
       buffer.resize(size);
       size_t num_read = fread(&buffer[0], size, 1, source);
       (void)num_read;
       fclose(source);
   
       return buffer;
     }
     throw file_not_found_exception(file);
   }

I'll assume, for sake of exposition, that as a member of the resource provider, or somewhere else available to you, you have a variable std::map<std::string, std::vector<uchar>> cache_ that keeps track of loaded files for the resource provider. Then your resource provider implementation might look like this:

   typedef std::map<std::string, std::vector<uchar>> file_map;
   
   std::vector<uchar> & myResourceProvider::load_helper(const std::string & filename) {
     file_map::iterator it = cache_.find(filename);
     if (it != cache_.end()) {
       return it->second;
     }
     cache_[filename] = open_file(filename);
     return cache_[filename];
   }
   
   void myResourceProvider::loadRawDataContainer(const String& filename, RawDataContainer& output, const String& resourceGroup) {
     std::vector<uchar> & data = load_helper(filename);
     output.setData(&data[0]);
     output.setSize(data.size());
   }
   
   void myResourceProvider::unloadRawDataContainer(RawDataContainer & output) {
     // Make sure cegui doesn't try to delete a buffer owned by our vector!
     output.setData(0);
     output.setSize(0);
   }