a) make a generic message buffering system that you can use to store/sort/call any function.
b) make a buffering system specific to your renderer.
In my engine, I've got both -- I use (a) for general buffering of gameplay commands, but use (b) when it comes to drawing things, because I rarely add new basic drawing functions (new drawing functions are usually a composition of the basic commands).
For my drawing commands, I keep the system as simple as possible, and it boils down to something like:
struct DrawCommand
{
enum Type
{
Foo,
Bar
};
Type type;
int Size();
}
struct DrawFoo : public DrawCommand
{
static int Size() { return sizeof(DrawFoo); }
DrawFoo() { type = Foo; }
int foo;
}
struct DrawBar : public DrawCommand
{
static int Size() { return sizeof(DrawBar); }
DrawBar() { type = Bar; }
float x, y, z;
}
int DrawCommand::Size()
{
switch(type)
{
case Foo: return DrawFoo::Size();
case Bar: return DrawBar::Size();
}
return 0;
}
....
std::vector<char> bytes(1024);
int pointer = 0;
struct DrawItem
{
int byteOffset;
int layer;
};
std::vector<DrawItem> items;
...
void Push( DrawCommand& cmd, int layer )
{
char* out = &bytes[pointer];
DrawItem item = { pointer, layer };
items.push_back( item );
int size = cmd.Size();
pointer += size;
assert( pointer <= bytes.size() );
memcpy( out, &cmd, size );
}
...
std::stable_sort( items.begin(), items.end(), sortByLayer() );
foreach( items as item )
{
DrawCommand& cmd = *(DrawCommand*)&bytes[item.byteOffset];
switch( cmd.type )
...
}