🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Resource Manager - how do you implement it (efficiently)?

Started by
6 comments, last by DerTroll 4 years, 9 months ago

Hi there,

recently I read in one of the treads about data-oriented programming vs. object-oriented programming. While I am generally avoiding to follow such principals dogmatically, it got me thinking again about a quite common problem: Resource Managers (in OpenGL). To get a little bit more specific, let's consider a Texture Manager. I have currently implemented them in the following way:

When starting the program, the manager creates a texture class for each texture of my game and stores them together with the name in a map or unordered map. The texture class itself has a reference counter and the OpenGL handle of the texture. The value of the handle is set to a specific "texture not loaded" texture during construction. Now if an object is loaded, that requires a certain texture, the texture is requested from the manager. It searches the textures name and returns a special reference class, that stores a reference to the texture class and increases and decreases its reference counter. Obviously, the reference counter has the purpose to initiate the loading and unloading of the texture. The problem here is, that the texture handle, which is just an integer, is not stored in a cache-friendly way, as are other managed objects that I might need during rendering  (Buffer objects for meshes, etc). All handles are stored in the texture managers memory location, which might be far away from the buffer object handles and the other object data.

So I was thinking that I should probably simply store references to all reference classes (I know that sounds weird) in the texture class itself. The purpose of this is, that I can then simply copy the current OpenGL texture handle to the reference class. In case that the handle changes for any reason (reloading/replacing texture), I update the handle in each reference class using a loop. Sure, this is a little bit more work during an update of the texture and if a reference is added/removed, but how often does that happen? As a benefit of this method, a copy of my texture handle is now stored at the same memory location as the rest of the object's data, which should result in reduced cache miss count.

Now I am interested in what you think about these two approaches and how you implement your resource mangers as efficient as possible?

 

Greetings

 

Advertisement

Apologies if the answer to this is obvious, but is the idea that the objects store texture reference objects by value, and the texture objects reference their associated texture reference objects by pointer or reference? (Maybe I'm misunderstanding, but it seems like that would have to be the case in order to get the contiguousness you seem to be after.)

Okay, I feared it might be confusing without code. So I will give you some pseudo-code to illustrate what I mean. Just wrote it in a hurry, so I hope I have not forgotten anything and didn't make too terrible mistakes ?

Version 1:

Texture class


class Texture
{

friend class TextureReference;

GLuint mHandle; // <- Texture handle that you get from the OpenGL API and that you need for rendering
U32 mRefCount = 0;
std::string mFileName; // Or whatever you need to acess the texture data on your hard drive                      
                      
                      
public:
                      
Texture(std::string fileName)
: mHandle{GetHandleNotLoadedTexture()}                      
  mFileName{fileName}                      
{}
                      
TextureReference GetReference()
{
	IncreaseRefCount();                      
 	return TextureReference(*this);                     
}                      

                      
private:

void IncreaseRefCount;
{
	if (mRefCount == 0)
		LoadTexture();
	++mRefCount;                      
}

void DecreaseRefCount;
{
	if (mRefCount == 1)
		UnloadTexture();
	--mRefCount;                      
}                     

void LoadTexture();                      
void UnoadTexture();
  
void UpdateTextureHandle(...)
{
  // load new texture and update handle
   ...
  
 	mHandle = newHandle;
  
  // delete old texture
  ...
}
                      
};

Texture reference class:


class TextureReference
{
  
friend class Texture;  

Texture& mTexture;
  

TextureReference(Texture& texture)
: mTexture{texture}
{}
  
public:
  
~TextureReference
{
 	mTexture.DecreaseRefCount(); 
}
  
GLuint GetHandle()
{
	return mTexture.mHandle;  
}

};

 

Version 2:

 


class Texture
{

friend class TextureReference;

GLuint mHandle; // <- Texture handle that you get from the OpenGL API and that you need for rendering
std::vector<TextureReference*> mReferences
std::string mFileName; // Or whatever you need to acess the texture data on your hard drive                      
                      
                      
public:
                      
Texture(std::string fileName)
: mHandle{GetHandleNotLoadedTexture()}                      
  mFileName{fileName}                      
{}
                      
TextureReference GetReference()
{    
 	return TextureReference(*this);                     
}                      

                      
private:

void AddReference(TextureReference* referencePtr);
{
	if (mReferences.size() == 0)
		LoadTexture();
	mReferences.push_back(referencePtr);
}

void RemoveReference(TextureReference* referencePtr);
{
	if (mReferences.size() == 1)
		UnloadTexture();
  
    // find and remove reference from mReferences
	...
      
}                     

void LoadTexture();                      
void UnoadTexture();      
  
void UpdateTextureHandle(...)
{
  // load new texture and update handle and reference handles
   ...
  
 	mHandle = newHandle;
  	for(TextureReference* texRef : mReferences)
        	texRef->mTextureHandle = newHandle;
  
  // delete old texture
  ...
}  
                      
};

 

TextureReference

 


class TextureReference
{
  
friend class Texture;  

U32 mTextureHandle;
Texture& mTexture;
  

TextureReference(Texture& texture)
: mTextureHandle{texture.mHandle}
, mTexture{texture}
{
	mTexture.AddReference(this);
}
  
public:
  
~TextureReference
{
 	mTexture.RemoveReference(this); 
}
  
GLuint GetHandle()
{
	return mTextureHandle;  
}

};

 

The texture references are stored in the objects you want to render together with references to buffer objects etc.

As you can see, the first version has an indirect access pattern. To get the texture handle for rendering, it has to be fetched from the texture class, that is stored in the texture manager -> potentially a lot of cache misses during rendering. The second version has a local copy, therefore there shouldn't be any cache misses. The drawback here is, that updates get a little bit more complicated

I haven't implemented this particular kind of system, and maybe there are standard solutions that someone else could point you toward. That said, I think I understand what you have in mind. The idea, as I understand it, is that object data (including texture handles) is stored contiguously by value (for good cache behavior), and the texture objects themselves are responsible for modifying the object's copy of the texture handle as appropriate if the texture handle changes. This certainly seems like it could work in principle.

I know what you posted is just example code, but looking at it, I suspect the details of the implementation is where things might get interesting. The example code brings to mind various potential issues, such as the 'rule of 3/5', the possibility of dangling pointers/references, the technical and semantic implications of storing references as members, the implications of reallocating the object data storage or creating and destroying objects, and so on. Maybe you've already thought about all that, but I suspect that in actually implementing such a system fully, some of those issues might come into play.

Also, just out of curiosity, is there any particular reason you want to unload textures whenever they become even temporarily unused? Maybe you have a good reason, but that seems a little atypical (in a games context at least), and it seems like the resource management might be simpler without that requirement.

6 hours ago, Zakwayda said:

Maybe you've already thought about all that, but I suspect that in actually implementing such a system fully, some of those issues might come into play.

You made some good and valid points here. As you said I have already thought about those problems and they are not that big of an issue because the system is more or less encapsulated. Both classes only interact with each other. Sure, you have to implement everything exception- and thread-safe but I guess that is manageable. Of course, there are always ways in C++ to break code/encapsulation but let's assume the "user" knows what he does. ?

Just to address some of your points: The copy and Move constructors of the reference class have to increase decrease the reference count (version 1) or add and delete pointers to them in the Texture class (Version 2). It's basically a little bit like a fancy std::shared_ptr. In this case, copying and deleting objects shouldn't be an issue.

 

6 hours ago, Zakwayda said:

Also, just out of curiosity, is there any particular reason you want to unload textures whenever they become even temporarily unused? Maybe you have a good reason, but that seems a little atypical (in a games context at least), and it seems like the resource management might be simpler without that requirement. 

You are correct, that this system makes things a little bit more complicated but think of a game, without any hard level boundaries, that uses a lot of data which is too large to be stored completely in memory (some UHD textures add up quickly). If you want to run such a game, you have to load and unload data permanently. Of course, you separate the world into chunks and do the loading and unloading per chunk, but that still needs a system, that identifies which data is needed and which not. So the whole idea is to load a chunk and all its containing objects. The objects request their resources in the form of those reference classes from the corresponding management systems, which take care of the loading/unloading.

Maybe there is a smarter approach to that. Would be happy to read about it.

 

Greetings

4 hours ago, DerTroll said:

You are correct, that this system makes things a little bit more complicated but think of a game, without any hard level boundaries, that uses a lot of data which is too large to be stored completely in memory (some UHD textures add up quickly). If you want to run such a game, you have to load and unload data permanently. Of course, you separate the world into chunks and do the loading and unloading per chunk, but that still needs a system, that identifies which data is needed and which not. So the whole idea is to load a chunk and all its containing objects. The objects request their resources in the form of those reference classes from the corresponding management systems, which take care of the loading/unloading.

Got it. For something like a simple casual game with relatively few textures it seems like the system might be overkill, but it makes sense for what you're describing.

You know the specifics of what you have in mind of course, and I'm just speculating here. But, I can imagine cases even in the scenario you present where a texture might be unloaded and then reloaded unnecessarily - due to, for example, a texture being unused, but only for a few seconds (in which case the unloading/reloading might not be beneficial).

Variations on your approach that come to mind include manually flagging textures as purgeable or non-purgeable (you could then flag e.g. large or high-resolution textures as purgeable), flagging textures as purgeable automatically based on how much memory they use, or making purging a manual step that you'd run at times when you know a lot of textures are going from used to unused or vice versa. I'm not saying that any of these is better than what you're doing now - I'm just throwing out ideas.

As for your original question about how best to dynamically update resources while maintaining contiguous object data, my only other thought is to look at existing ECS implementations or references on that topic to see if such questions are addressed (I don't think you specifically mentioned ECS, but what you're doing seems to have that flavor). But maybe you've already done all that research :) In any case, maybe someone else will jump in with a suggestion on how to handle that particular issue. (And your current idea seems workable, provided of course that the various implementation pitfalls are avoided.)

16 minutes ago, Zakwayda said:

You know the specifics of what you have in mind of course, and I'm just speculating here. But, I can imagine cases even in the scenario you present where a texture might be unloaded and then reloaded unnecessarily - due to, for example, a texture being unused, but only for a few seconds (in which case the unloading/reloading might not be beneficial).

Yeah, that is a problem especially if the player moves along the boundary of some chunks that differ much in the used resources.

18 minutes ago, Zakwayda said:

Variations on your approach that come to mind include manually flagging textures as purgeable or non-purgeable

That's an option one should consider. It's a little bit like a garbage collection system. Well, I think in my special case, I would not flag the resources themselves but the objects that are composed of them.

Another option would be to have a larger offset between the player positions were a chunk is loaded and where it is unloaded. That would avoid loading-unloading ping pong if the player moves exactly along the border.

 

25 minutes ago, Zakwayda said:

As for your original question about how best to dynamically update resources while maintaining contiguous object data, my only other thought is to look at existing ECS implementations or references on that topic to see if such questions are addressed (I don't think you specifically mentioned ECS, but what you're doing seems to have that flavor). But maybe you've already done all that research :) In any case, maybe someone else will jump in with a suggestion on how to handle that particular issue.

No, I haven't yet looked at how other systems implement such a system. I just hope somebody who has already done something comes around and enlightens me. :D The problem is, that it is always hard to find information about such low-level systems since they are often hidden or not well documented. Until I find the relevant code pieces and actually understand them, I probably got 10k answers in this thread. :D

Greetings and thanks for the input so far :)

 

This topic is closed to new replies.

Advertisement