Resources handling - part 2
To recap from previous post:
1) There's a central hub that I call ResourceHub (resHub) that acts as the main interface from the app to the resources.
2) You can add to the hub many ResFamily (like shape family, image family, shader family and so on)
3) Every family can have many ResProvider which are used to load and to "specialize" a resource
So, when it's time to add a resource, all you have to do is to call resHub->addResource (family, resourceName, &resID) in order to obtain an unique resource ID that will identify this resource (and all its specializations) forever.
The key point here is the word "forever". You have no way to remove a resource once it's added. This may sounds a little strange, but it has to do with the lifetime management of the resource and its ownership.
First of all, an unloaded resource, once added, has a memory footprint of only 8 bytes (plus the resource's name string which is usually short, something like 8 - 12 characters as it does not include any path or extension).
I call this 8 byte the "resource header".
Resource headers are stored in tables of 256 headers each (named TableU8). A single table needs 8 * 256 bytes = 2KB which is not that much.
A resource provider, store resource headers in a table of tables (named TableU16), which is a table that can hold a max of 256 TableU8.
The maximum number of resources that a single provider can hold is 256 * 256 = 65536, which is a not bad for a game I guess.
This limit is also limiting the number of resources you can add to a family.
In short: you can have no more than 65536 textures, 65536 shaders, 65536 materials, 65536 whatever.
Usually you don't need that much available at the same time, so TableU16 can load/unload a whole TableU8 at any time, just like a pagination algorithm.
When you add a new family, TableU16 will allocate only one TableU8 (ie 2KB) and will allocate further TableU8 only when needed. It will also deallocate any TableU8 that contains only unloaded resources that are not used anymore (more on this later).
So, keeping any added resources forever will not cost that much in memory, giving that a single resource header need only 8 bytes and that resources are paginated in blocks of 256 headers.
What comes handy, is that once you obtain a resourceID, you can use it forever, at anytime. So maybe you have added a bunch of textures for your terrain and than you warped 10.000 km away and have to load a whole new set of textures... well, going back to the starting point (maybe after 10 minutes), will not require you to add again the textures and the meshes, you already have all the resourceID that you need.
In fact, you could even add all the textures/meshes you will ever need for your terrain, get the resourceID and then forget about it and keep on using only the resourceID. The ResourceSystem will take care of loading/unloading every resource.
So, who own a resource? Resource ownership is always a delicate argument. There's a very good post from swiftcoder that gives you a nice introduction; you can find the post here
My solution is: the ResourceHub own the resource, and is the only responsible for loading/freeing it. Actually it's the family inside the hub that own the resource, but from a user point of view, the magic is inside the hub.
So you can not delete a resource, you can't even free or unload it. You can maybe give hints to the hub, but he will decide on what to do.
When you need access to a resource, you call resHub->getResource (resID) and the hub will give you back a status and a pointer.
The status tell you if the resource is loaded or not. If it's loaded, you can use the pointer, otherwise you can't.
The next time you'll need that resource, you'll do the same thing, call getResource() and check the status. If a resource is not loaded, calling getResource() will return a status of "loading" and will schedule an asynchronous load so it's quite possible that the first time you call getResource() you will recevice a "loading" status, and the second/third time you will receive a "loaded" status.
Internally the hub use a Last Recently Used (LRU) schema to keep tracks of used/unused resources.
Any resource lies in one of 8 LRU level . Level 0 is the most recently used, level 7 is the last.
It's your responsibility to call resHub->onLRUTick() once every 1 second or so; it will "move" resources from Level 0 to level 1, from level 1 to level 2 and so on. Once a resource hits level 7, it will unloaded and will stay there on level 7.
A TableU8 that holds only resources at level 7, will be unloaded as it contains only unused resources.
Calling getResource() will always move a resource from whatever LRU level actually lies, to LRU level 0, just to indicate that this resource has been used very recently so it should not be unloaded very soon.
This is how the hub can automatically unload unused resource and free some memory.
From a performace point of view, the whole LRU thing is highly optimized and does not involve "memcpying resources from level to level" which will surely be a bad thing to do once every second...
Also, "moving" a resource to level0 is just a matter of setting a byte in the resource header so it's not that costly, since you have to access the resource header anyway in order to getResource() and retrieve the resource status.
So far so good, it's seems to work pretty well but, I admit, I still have to stress it to see how really performs under heavy load. It will take sometimes before I can stress test it, I'm now involved in writing with the GUI library which does not need much resources to work.
I've also to refine and simplify some aspects of the main interface, but I'm satisfied of the results so far.
See you next time