Wednesday, September 25, 2013

Query commands execution

So like always start with problem description:

I have some pool of command represented as enumerator. Each of command can have unique data that size can be different. I wanted to create system that allow me in easy way iterate over them and execute. 

After some time I created this implementation:
    template<ECommands::enum cmd>
    bool execCommandTemp(ECommands::ENUM a_cmd, void* a_data)
    {
        if (a_cmd == cmd)
        {
            SCommand<cmd>::execute((SCommand::SData*)a_data);
            return false;
        }
 
        return execCommandTemp<(ECommands::ENUM)(cmd+1)>(a_cmd, a_data);
    }
 
    template<>
    bool execCommandTemp<ECommands::WRAP>(ECommands::ENUM a_cmd, void* a_data)
    {
        return true;
    } 

    bool execCommand( ECommands::ENUM a_cmd, void* a_data )
    {
        return execCommandTemp<(ECommands::ENUM)0>(a_cmd, a_data);
    }
where SCommand look in example such a way:
    template <> struct SCommand<ECommands::BUILD_SHADERPROGRAM>
    {
        struct SData
        {
            CShaderProgram*    program;
        };
 
        static ECommands::ENUM cmdType() { return ECommands::BUILD_SHADERPROGRAM; }
 
        static void execute( SData* a_data )
        {
            a_data->program->build();
        }
    }; 
So why to bother with templates when you can use simple switch :
  • Adding new command is very simple create new enum and it's SCommand implementation.
  • If you forget about any function it will return error in meantime of compilation.
  • You can create very similar function to i.e return size of SCommand::SData
For some people this may be not enough for me it's really good solution because thanks to it adding new command will be quicker and my implementation will be validated in meantime of compilation.

15 comments:

  1. Hey Greg. Hope you are doing fine in the maple leaf country ;)

    Have you heard about CRTP pattern? I think you could use it in this scenario some way. Maybe it will make your code a little bit clearer ;)

    First let's define an interface for all your commands:

    class ICommand
    {
    public:
    virtual ~Command() {}
    virtual void executeCommand() const = 0;
    };

    Then create class that will implement interface above.
    Since CRTP pattern allows to make a method call from future derived classes you can simply static cast to known template argument to call appropirate implementation of execute.

    template
    class SCommandBase : public ICommand
    {
    public:
    void executeCommand(T* pCommand)
    {
    static_cast(this)->execute();
    }
    };

    Now lest make some commands that inherit from our CRTP base class.

    class SCommand_BuildShaderProgram : SCommandBase
    {
    public:
    void execute()
    {
    program->build();
    }


    private:
    // This can be particulary anything you want
    CShaderProgram* program;
    };

    Now you can store collection of ICommand derived classes an simply call their executeCommand method to implicitly call execute method from your Command concrete classes.
    And everythng without unneccery data bloat and polymorphism overhead.

    Good day :)

    ReplyDelete
  2. P.S. Miss our tech talks ;)
    Concider chananging your blog engine to something more user friendly. Posts reply are terrible right now ;)

    ReplyDelete
  3. Oups forgot to add template argumant in the SCommand_BuildShaderProgram class. Should be:
    class SCommand_BuildShaderProgram : SCommandBase
    {
    (...)
    }

    ofc. ;)

    ReplyDelete
  4. Heh, it wasn't me its just template brackets are considered to be HTML tag. Woot!

    SCommand_BuildShaderProgram is a template argumant for SCommandBase ;)

    class SCommand_BuildShaderProgram : SCommandBase(SCommand_BuildShaderProgram)
    {
    (...)
    }

    ReplyDelete
  5. Hi :] right now I'm still in Poland.

    About this solution for sure we lose validation on compilation level and complicate a little adding new commands this facts I'm sure. But I think about the rest.

    Maybe I will give some little more detailed background : this queue is very low level and use continues memory allocated on initialization. You can see it as ring buffer with format [cmd_id][cmd_data]...... Right now I think how efficient and nicely build usage of your case and if it would look better than actual implementation use it in code.

    About comments this is standard blogspot which I think isn't prepared for advance C++ code. I will look if anything can be done about it.

    ReplyDelete
  6. Btw, paste some more code to your tech posts (even pseudo one). This makes more sense to programmers like myself :)

    What's inside [cmd_data], is it a pointer to some object? I assume that cmd_id is a identifier (ENum) of particular command. Does it all mean that you have unified data size for all possible commands? (i.e. union like?)

    ReplyDelete
  7. Why are you still in Poland? Something went wrong?

    Jesus, disable that damn captcha :O

    ReplyDelete
  8. No everything is fine I waited for work permit. But yesterday I got it so everything is in move again.

    About code this is usage of queue:

    bool CCommandBuffer::execute( void )
    {
    // There is nothing to do so exit.
    if (isEmpty())
    {
    return false;
    }

    uint8* readTo = m_MemoryWritePtr;

    while(m_MemoryReadPtr != readTo)
    {
    uint8* readMem = m_MemoryReadPtr;

    ECommands::ENUM cmd = (ECommands::ENUM)*(m_MemoryReadPtr++);

    if (execCommand(cmd, m_MemoryReadPtr))
    {
    // WRAP COMMAND
    m_MemoryReadPtr = m_Memory;
    }
    else
    {
    m_MemoryReadPtr += getCmdSize(cmd);
    }
    }

    return true;
    }

    As you see [cmd_id] is single byte containing enum. [cmd_data] is SCommand<T>::SData (example in post).

    Captcha disabled :]

    ReplyDelete
  9. Now i get it :) Put some notes to that code or you will not remember a thing in years to come ;)

    So your memmory layout would be something like this:

    [cmd_id][cmd_data][cmd_id][cmd_data]

    and its raw. You rely on data size to point to propper memory point and read data of desired size based on cmd_id. So I assume when you create a command you are actually doing memcpy to push it to that memmory block. Right?

    ReplyDelete
  10. Code isn't exactly the same as in engine :] there I have some comments/events/descriptions of classes and how they work. Here I removed them so code would be shorter.

    As you said: on create of command memory is fill with data. This solution is nice because it minimize memory usage and allow me for work on static pool of memory. Combined with ring buffer functionality it's really powerful tool :]

    ReplyDelete
  11. One more thing:

    // if command cmd is executed with data under m_MemoryReadPtr
    if (execCommand(cmd, m_MemoryReadPtr))
    {
    // don't get it what m_Memory stands for? Can you explain?
    // is it memory holding results of that command or what?
    // WRAP COMMAND
    m_MemoryReadPtr = m_Memory;
    }
    else
    {
    // ok so if it faild you simply skip to next cmd_id
    m_MemoryReadPtr += getCmdSize(cmd);
    }

    ReplyDelete
  12. Ok so it look like that :

    // execute command (return true if its WRAP)
    if (execCommand(cmd, m_MemoryReadPtr))
    {
    // WRAP COMMAND - return to begin of memory on which commands work (functionality of ring buffer).
    m_MemoryReadPtr = m_Memory;
    }
    else
    {
    // this was normal command and we used it [cmd_data] so we jump over it (getCmdSize(..) return size of SCommand<T>::SData )
    m_MemoryReadPtr += getCmdSize(cmd);
    }

    I see that I need to change some names so they would be easier to understand.

    ReplyDelete
  13. Ok, now its all clear! :) Thanks for explaination!

    Yet another thing, when WRAP command is issued to the ring buffer? Is there possibility to everride unexecuted command?

    ReplyDelete
  14. Yes wrap is ring buffer: return to begin. And there is no option to override because I use m_MemoryWritePtr and m_MemoryReadPtr. So when I add new command there is check if there still enough space for this new command. If there is not return assertion.

    Of course there is possibility to wait on release of memory but on this point I decided that there should always be space for new command if not then one of bellow options:
    - it's mean that something is wrong and you need to check it
    - everything is ok you only need to increase size of memory size.

    ReplyDelete