stridecolossus
stridecolossus

Reputation: 1561

Design or pattern for handling background consumer/producer tasks

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:

There are a few problems to resolve here:

  1. 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.
  2. 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.
  3. 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 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.

The problem:

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

Answers (1)

stridecolossus
stridecolossus

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 Executors, Runnable tasks, etc.

Upvotes: 0

Related Questions