Command passing is a handy way to execute pieces of code on specific threads. The idea is simple enough: Write a command into a buffer and that command then gets executed on some specific thread that monitors that command buffer. This paradigm is used in graphics APIs like DirectX and OpenGL, for instance. This allows you to split your data into working sets that are owned by a single thread and must maintain their state over a specified time period.
In the graphics engine I’m developing at Raccoon Interactive we’re using a couple of separate data sets that should change only at specific synchronization points but we don’t want the other threads to stall until that point is reached.
In our case the high-level graphics rendering thread has it’s own data set that controls the graphical representation of the scene, i.e. the scene graph. The scene graph consists of a node tree with leaves referencing scene primitives like meshes and lights. Meshes use low-level renderer primitives like vertex buffers, materials and textures that are bound to a low-level rendering API, such as OpenGL, through an abstraction layer. Operations against this low-level API are executed on a dedicated thread.
Each different level abstracts thread communication with the help of wrapper classes, so from the point of view of the developer you’re just calling methods on objects. This makes writing algorithms that operate on the scene graph or the graphics primitives much easier because you don’t need to lock your resources as they are modified only at pre-specified points in time.This also means that different sub-systems can continue to run in parallel even though, from a user’s point of view, everything seems to happen synchronously.
The command buffer can be found at the heart of all this communication. I’ve designed a custom implementation that simply consists of a block of memory into which function parameters and the function to call, itself, are stored. The reader of the command buffer then reads this block of memory and calls the associated function with the parameters found in the memory block reserved for the command.
The command buffer implementation in our engine is very C-like, I must admit, but it serves our purpose more than well. To write a command into the buffer you do the following.
sizeof( RRInt_t )> cmd( &CmdMyCommand, pCommandBuffer );
cmd.Set<RRInt_t>( 1 );
The RRCommand class abstracts mapping and unmapping of the command buffer as well as writing the command arguments into the buffer. Its constructor takes a pointer to the command function and the command buffer into which the command should be written. We also pass the size required by the command arguments as a template argument, so the class knows how much space to allocate for this command. All commands are written into a byte aligned memory buffer. Because assignment operators cause us trouble on systems that expect memory to be properly aligned we’re forced to copy the arguments into the command buffer by using a simple memory copy (aligning the arguments according to the worst case scenario would also work, but it would require significantly more memory).
To execute the commands we simply do the following.
pCommandBuffer->ExecuteCommand( true );
Passing true to the method makes it execute all the commands in the buffer. Otherwise, only a single command would be executed by a single call to ExecuteCommand.
The command itself is defined by the function CmdMyCommand, as is shown in the next code snippet.
void CmdMyCommand( RRCommandBuffer::RRInputCommand &rCommand )
RRInt_t arg = rCommand.Get<RRInt_t>();
As you can see, the command receives a single argument rCommand that provides access to the command arguments. The convention is that the command should read out all it’s arguments with consequent calls to Get at the start of the function. This is to avoid issues with reusing the temporary argument store from which the arguments are read.
That’s about it. This simple command sending mechanism has helped me a lot in creating abstraction layers to hide tedious thread communication. The same could most likely be implemented by using lambdas but I haven’t tried it because we’re currently tied to the lowest common denominator that is the Android NDK compiler, not to mention other even more limited compilers that we might have to support in the future.