Reputation: 1561
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).
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.
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.
interface Task implements 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.
This issue is to how are 'resources' that are returned by 'producer' tasks (e.g. the LoadImageTask
) passed to the 'consumer' tasks (e.g. UploadTextureTask
) when those classes are not coupled in any way?
One (rubbish) way would be to have some custom code that calls loadImage.getImage()
when that task completes and pass it up the 'chain' using uploadTask.setImage()
, but this pretty much makes the whole approach pointless if one has to write roughly the same task-management code for each use-case.
I have tried defining consumer and producer interfaces with generic getter/setter methods and linking dependant tasks that have the correctly matching data type, but this approach breaks down if a task consumes more than one resource (as the upload task does above).
I also considered using Future
and Callable
(the background task queue uses an Executor
thread pool anyway) but they are really orientated towards blocking an entire thread rather than resource management per-se. The number of tasks in this system can be in the thousands, most of which are pending tasks that are currently running or are themselves queued. Blocking tasks that have not yet even been started seems pretty pointless. Also the only way (?) to know when a future is completed is by calling isDone
which implies another layer of complexity to poll finished tasks.
I have researched this problem here and on OpenGL developer and gaming 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. (And apologies for the wall-o-text)
Upvotes: 1
Views: 456
Reputation: 1561
Some of the new goodies in Java 8 appear to handle exactly this sort of problem, e.g. CompletableFuture
, which integrates nicely with the existing design described above using Executor
s, Runnable
tasks, etc.
Upvotes: 0