Constructing / Loading resources asynchronously

Started by
3 comments, last by PhillipHamlyn 11 years, 2 months ago

Hi,

I've started using async / await as I'm taking a look at WinRT. But Im having a terrible time getting to grips with async usage. Suddenly all of my objects have 3 'constructor' methods ...


public someModel(string modelAsText) // the constructor
 : base(modelAsText, VertexElementTypes.Position, VertexElementTypes.Normal, VertexElementTypes.Texcoord)
{
    // now often empty
}
 
public override void Initialize(DeviceManager devices) // called when graphics data needs to be loaded and bound to the device
{
    // setup any programmatic/fast data
    ....
    // setup any loadable/slow data
    LoadData(devices) // calls the async method
}
 
private async void LoadData(DeviceManager devices) // the async method to do the actual loading and binding
{
    ...
    IsReady = true; // flag indicating that all data is now loaded.
}

One thing that's starting to annoy me about async is that all my constructors are emptying. And all the construction code is having to be moved into the Initialize and async LoadData methods. I can't understand why a constructor can't be asynchronous. In my mind, it absolutely should be because like any object being constructed, it is immediately identfiable but isn't ready to be used until all its parts are in place. It seems to me that the language isn't designed correctly to handle this. I've read comments on the topic of async in constructors and I understand the reasoning .. early disposal causing leaks and object usage before its ready, but I would argue that the reasoning is only necessary because the model is flawed. That discussion isn't really helping me solve the problem, but I think it's a part of the topic.

Another problem I am seeing is that even though the file accesses are asynchronous, the awaits in LoadData cause the LoadData method to be synchronous (don't they ?) and therefore the thread has to wait anyway for LoadData to complete, so what's the point. The initialize method in this case is synchronous, so all the asyncronous code in the world can't make the drive load data any faster.

I'm really not sure about this, because when I run the app, the GUI does come up and the model pops into existence when it's loaded. So it actually seems to be doing what I want. And if i remove the IsReady flag I get a few frames where the states and shaders aren't set yet. So it seems to be running asynchronously !

Q : does the LoadData call create it's own thread and does execution immediately continue ( in this case, returning from the Initialize method immediately ) while the LoadData method contiues on it's way ?

I'd like to know what patterns people are using to initialize models and resources asynchronously.

Thanks for any help or advice. Gavin

Advertisement

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.

C++ guy here... But I'm sure some of it applies.

The only part of the loading process that really needs to be async is the file reading and parsing. Pack your file data in a ready-to-use format so that after loading you can quickly just fix-up the data into a structure you can pass to your objects. If you want to support a format that needs parsing, split out the parse step so the async loader can call some arbitrary "fixData" function pointer that returns your ready-to-use format.

It also helps if you abstract away the usage pattern for your resources by hiding the resource behind a handle. That way your objects don't have to deal with any of the complexity, they just say "Handle h = resourceMan.findResourceByName<Model>("foo.model");" when you need to manipulate it you can later "Model *myModel = resourceMan.acquireResource<Model>(h);" You'll likely end up with a lot of if-valid / if-not-null type checks, It might seem like a hassle checking validity all over the place, but it lets you shuffle resources around without your code caring. Meaning you can have the resource manager watch for file changes, update the data once, and all the objects holding a handle to the resource get the updated data.

Thanks both for the ideas. I have been using a resource manager, really just a resource loader (in the project that I am now rewriting from scratch), but not exclusively, I also have some resources loaded and maintained by certain classes. I think it's messy to have both, but my opinion shifts on which is better as I explore different patterns.

@ y2kia - what's interesting is how you extend the idea of the resource manager to have a queue and and to be able to report progress / availability and deal with that if need be. Those are good extensions to the resource loader. And i can see how that's going to clear up my view of resource loading verses game-object creation. They can be, and are different processes. I was thinking last night about using an enumeration { init1, init2, ... , initN, IsReady } to track cumulatively object data loading states. But again, a resource manager might just make all that business easier to understand and will certainly tidy up my game-objects classes.

Some great suggestions. I'll play around tonight with some of these ideas.

Gavin,

In my commercial programs (not games) disk access is passed off to a single thread of its own, and I just use Synch access within that thread. The Disk cant read two things at once, but you obviously dont want that disk access on your UI thread, so I just place the read into a Queue<>() on the DiskIO thread with a callback when its ready by passing somehting like this; (psuedocode)

struct FileRequest

{

public string FileName;

public void delegate CallBack;

}

My disk IO thread checks to see if a request is already queued for that file name - if it is I add the new CallBack to a list of callbacks for that request, then discard the new request, otherwise I add the new request to the queue.

Making this code synch inside the disk manager thread makes it easier to unit test as well, because it doesn't rely on asynch semantics or code structures. I can also batch together multiple calls into a batch request and only get a callback when they've all completed.

Maybe this will help your code be cleaner; maybe not.

Phillip

This topic is closed to new replies.

Advertisement