Below are the patterns I use in my resource loading system, I've been very happy with it, and maybe it can give you some inspiration or a new direction to follow.
One thing that would help is to separate the concerns. Loading raw bytes from disk in an asynchronous way is a different task and a different concern than constructing an object from those bytes. Don't mix your object construction with also implementing an async. loader. A generic loader can pull the bytes from disk (or network, or whatever) and deliver them to your object factory for deserializing and constructing objects.
When dealing with asynchronous stuff, it's also helpful to have a wrapper around your resource to keep track of the state of your loading process. You need to be able to request that a resource be loaded, and if it is already in cache, delivered immediately. If it's not already in cache, put it in the queue for your loader thread to read in the bytes, then construct the object. The wrapper object will keep track of the current state and let you poll each frame for a constructed object to use. I call this wrapper a Resource Handle.
First, an excerpt from the comments at the top of my ResHandle.h file...
/* ResHandle.h
Description:
Resources are loaded from IResourceSource derived classes via the
ResHandle interface described in the usage patterns below. The caching
system sits underneath this system and silently manages a least-
recently-used cache of resources. The system uses a virtual file path
to locate the resource, where a path such as "textures/sometexture.dds"
would consider "textures" the source name (of the registered
IResourceSource) and "sometexture.dds" as the resource name to be
retrieved from the source. Any additional path after the source name is
considered to be relative pathing from the root of the source. For
example "scripts/ai/bot.lua" would look in the source "scripts" for the
resource "ai/bot.lua".
Usage patterns:
-----------------------------------------------------------------------
Synchronous loading from ResourceSource
-----------------------------------------------------------------------
ResHandle h;
if (!h.load<TextureRes>("textures/texName.dds")) {
// handle error
}
TextureRes *tex = static_cast<TextureRes *>(h.getResPtr().get());
// use tex...
-----------------------------------------------------------------------
Asynchronous loading from ResourceSource
-----------------------------------------------------------------------
ResHandle h;
int retVal = h.tryLoad<TextureRes>("textures/texName.dds");
if (retVal == ResLoadResult_Success) {
TextureRes *tex = static_cast<TextureRes *>(h.getResPtr().get());
// use tex...
} else if (retVal == ResLoadResult_Error) {
// stop waiting, handle error
}
// keep waiting... try again later
-----------------------------------------------------------------------
Resource injection (manual instantiation) - note this pattern is for
creation only and does not automatically retrieve from a cache if the
resource already exists. ResourceSource and ResHandle not used for this
method. You must pass the specific cache to inject to. You can manually
retrieve from a cache with the second pattern.
-----------------------------------------------------------------------
ResPtr texPtr(new TextureRes());
TextureRes *tex = static_cast<TextureRes *>(texPtr.get());
if (tex->loadFromFile("texName.dds")) { // must internally set mName and mSizeB
if (!TextureRes::injectIntoCache(texPtr, ResCache_Texture)) {
// resource already exists with this name
// handle injection error
}
// you can now use tex... (though it may or may not be in cache)
}
... then somewhere else in your program, retrieve it from cache ...
ResHandle h;
if (!h.getFromCache("texName.dds", ResCache_Texture)) {
// not in cache, handle error
}
TextureRes *tex = static_cast<TextureRes *>(h.getResPtr().get());
// use tex...
*/
Next is a function from my unit test file with this system actually being used...
void TestProcess::onUpdate(double deltaMillis)
{
// don't have the resource yet, so try to get it
if (!mTexture.get() && !mTextureError) {
ResHandle h;
ResLoadResult retVal = h.tryLoad<Texture2DImpl>(L"textures/dirtnew.dds");
if (retVal == ResLoadResult_Success) {
mTexture = h.mResPtr;
Texture2D *tex = static_cast<Texture2D*>(mTexture.get());
debugWPrintf(L"SUCCESS loaded %s\n", tex->name().c_str());
} else if (retVal == ResLoadResult_Error) {
mTextureError = true;
debugPrintf("ERROR loading dirtnew.dds\n");
}
}
// test resource injection method
if (!mTexture2TestDone) {
mTexture2.reset(new Texture2DImpl());
Texture2D *tex = static_cast<Texture2D*>(mTexture2.get());
if (tex->loadFromFile(L"data/textures/palmtr.dds")) {
if (!Texture2D::injectIntoCache(mTexture2, ResCache_Texture)) {
debugPrintf("ERROR injecting palmtr.dds\n");
}
}
mTexture2TestDone = true;
} else if (mTexture2) {
mTexture2->removeFromCache();
mTexture2 = 0;
}
if (mTexture && !mTextureSaved) {
Texture2D_D3D11 *pTex = static_cast<Texture2D_D3D11 *>(mTexture.get());
pTex->saveTextureToDDSFile(L"data/dirtnew_saved.dds"); // test dds save
mTextureSaved = true;
}
// Load the effect that was just compiled through resource system
if (!mEffect.get() && !mEffectError) {
ResHandle h;
ResLoadResult retVal = h.tryLoad<EffectImpl>(L"data/shaders/Default.ifx");
if (retVal == ResLoadResult_Success) {
mEffect = h.mResPtr;
Effect *eff = static_cast<Effect*>(mEffect.get());
debugWPrintf(L"SUCCESS loaded %s\n", eff->name().c_str());
} else if (retVal == ResLoadResult_Error) {
mEffectError = true;
debugPrintf("ERROR loading Default.ifx\n");
}
}
}
This code above is really written as a unit test, and in the midst of your game loop might seem ugly. But the if/then boilerplate around each load call could be factored out of your code by hooking this loading routine into an event system, so you could just make the load request and give it a callback function, instead of handling the result inline with the tryLoad.
If you like this pattern and have questions, I will be happy to dig into some of the details of how the magic happens underneath the "ResHandle" layer.