Background:
I have a 3D engine implemented using LWJGL aimed largely at rendering a large ‘world’ - terrain, vegetation, animated creatures, weather, day/night cycle, etc. The engine manages the resources that are required for rendering such as the various textures, meshes, models, etc.
As the player navigates this world the engine caches the various types of data discarding/releasing resources that are no longer required and loading/caching newly required data on demand. This is handled by a set of background threads that perform I/O tasks (loading images, loading a new terrain ‘chunk’, building a model mesh, etc) and a queue of tasks to be executed on the OpenGL context (allocating a new texture, uploading data to a VBO, deleting a texture, etc).
Current design:
This all works nicely with the exception of tasks that have dependencies. I’ll illustrate with an example:
Let’s say the engine needs to apply a new texture to an object in the scene, this consists of the following steps:
- load the texture image from the file system (I/O background task)
- allocate a texture on the GPU (OpenGL context task)
- upload the image to the texture (OpengL)
- attach the texture to the material for the object (OpenGL, not really but avoids rendering issues)
There are a few problems to resolve here:
-
Ideally I want these steps (or tasks) to be atomic and re-usable, for example the engine needs to load images for several other parts of the system - the code is exactly the same and therefore can (and should) be encapsulated and re-used.
-
Some of this can be run in parallel (e.g. load image and allocate texture), some of it must be sequential, e.g. cannot upload the image until we have loaded it and allocated the texture.
-
Some of the steps have ‘resource dependencies’ on previous steps, e.g. in particular the upload step requires the allocated texture ID and the image.
The first two turn out to be relatively straight-forward, it’s the last one is what I am struggling with - I cannot seem to come up with a decent design that allows re-usable and relatively atomic tasks to be linked together when there are inter-task dependencies.
Some pseudo-code:
interface Task extends Runnable {
TaskQueue getQueue();
}
// Generic load-an-image task
class LoadImageTask implements Task {
private final String path;
private BufferedImage image;
public LoadImageTask( String path ) {
this.path = path;
}
TaskQueue getQueue() { return TaskQueue.BACKGROUND; }
public void run() {
// load the image from the given location
}
}
// Uploads an image to a given texture
class UploadTextureTask implements Task {
private BufferedImage image;
private Texture texture;
...
TaskQueue getQueue() { return TaskQueue.RENDER_THREAD; }
public void run() {
texture.buffer( image );
}
}
// Example task manager for the scenario outlined above
class Example extends TaskManager {
...
// Load the texture image
final LoadImageTask loadImage = new LoadImageTask( ... );
add( loadImage );
// Allocate a texture
final AllocateTextureTask allocate = ...
add( allocate );
// Upload texture image
final UploadTextureTask upload = ...
add( upload, loadImage, allocate );
}
The add method in TaskManager registers a task on the relevant queue in the background. The manager gets notified when each task is completed. When all current tasks are finished (loadImage and allocate in this case) the manager starts the next task(s) in the sequence.
Note that the final add call for the upload task tells the manager that it has dependencies on the load and allocate tasks.
The problem:
As things stand the resources (in this case the image and the texture objects) are copied from the dependant tasks by reflection when those tasks are completed. This just seems dirty but I cannot think of a better method.
I’ve tried defining consumer and producer interfaces with generic getter/setter methods and linking dependant tasks that way but this approach breaks down if a task consumes more than one resources (as the upload task does above).
I have researched this problem here and on other sites but haven’t come across any good designs - they either seem to just consist of cut-and-paste code or some sort of shared state (usually a map) with lots of nasty casting everywhere. Maybe I am not searching for the correct terms? Or maybe my whole approach is rubbish?
Has anyone implemented or come across anything similar? Any suggestions, criticisms, pointers are welcome.
Apologies for wall of text, also posted this here: http://stackoverflow.com/questions/23542776/design-or-pattern-for-handling-background-consumer-producer-tasks
- stride