Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
100 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

To jump back and forth between our management code and the script itself, we take advantage of the standard C library setjmp. We can use this library to do the dirty work of saving and restoring the current CPU state. There are only two functions in the library that we need to be concerned with:

setjmp - Saves the current state of the program and the stack environment

longjmp – Restores the saved state of the program and the stack environment

With these two functions we can safely pause and continue each script. While setjmp and longjmp does the nasty work of saving our CPU states, it only works for jumping to a previous point in our call stack. If we thought of it as a time machine you would only be able to travel to the past with it, never the future. In order for us to a longjmp in to function that was previously running, we have some further work to do.

To continue a script just before we do a longjmp back we need to manually restore the contents of our stack to match where it was left off. A basic memory copy will work fine for our purposes. We just need to copy the stack's memory into a buffer when we leave any script mid-execution.

To do all of this we only need to implement two additional key commands. One to get the address pointed to by the stack pointer, and another to modify the stack pointer directly.

// Copy to the stack pointer to local variable
#define TG_SAVE_STACK(var) __asm {mov var, ESP}

// Restore the stack with our own variable
#define TG_RESTORE_STACK __asm {sub ESP, stack}

The size you wish to assign the buffer is arbitrary but it constrains how many things you can store on the stack. We can even be real conservative with our memory and give our scripts a measly 128 bytes for stack space. This does not mean you can only use 128 bytes on the stack, you can use as much as you want, it just means at the point where you are pausing execution, that is the maximum amount at that point that can be in use. If all your scripts do is call functions and do a bit a match, you probably won't even use much memory at all. With this type of memory setup, even after 1000 co-routines, we will just be using about 125K of memory plus any internal structures.

The following are the steps we will need to take in order to run a script and then for the script to return control mid execution.

  • Call setjmp just before start our script / function. This lets us to take a snapshot of the current CPU state.
  • Mark the size of the stack at that point
  • Call the C script (function) that we want to run

When ready to return control back to the program:

  • The script calls setjmp to save the state of the CPU at that point of the script
  • Make a copy of the stack contents that we've used
  • Call longjmp to return the CPU to where it was before we called the script

A frame later, we are ready to give some processing time back to the script and allow it to continue.

  • Call setjmp to save our current CPU state again.
  • Restore the values on to the stack that we have saved from earlier.
  • Call longjmp to return the CPU to the state that the script was last in

The following is some simplified code from the sample code. It checks whether to start a script if it is fresh, and what to do if it needs to continue an old script.

// Mark where are current stack pointer is at
TG_SAVE_STACK(stack);
a_pRoutine->m_StackBegin = stack;

if (setjmp(*(a_pRoutine->m_pExecSystem)) == 0) 
{
  // New script, spawn
  if (a_pRoutine->m_RunState == routineProcedure::RS_START)
  {
    a_pRoutine->m_RunState = routineProcedure::RS_RUN;
    a_pRoutine->m_pFunction(a_pRoutine);
    a_pRoutine->m_RunState = routineProcedure::RS_INVALID;
    return;
  }
  else          // old script, continues
  {
    stack = a_pRoutine->m_StackUsed;
    TG_RESTORE_STACK;
    TG_SAVE_STACK(stack);

    // Copy contents of stack for long jumping forward.
    memcpy((char*)(stack), a_pRoutine->m_Stack,
           a_pRoutine->m_StackUsed);
    longjmp(a_pRoutine->m_pExecCurrent, 1);
  }
}

The pause macro implements the stop in mid-execution and returns back to the calling function, and the setup to be continued later.

#define PAUSE(time) if (setjmp(a_pProc->m_pExecCurrent) == 0) { \
   int volatile stackEnd; \
   TG_SAVE_STACK(stackEnd) \
   a_pProc->m_PauseTime = time; \
   a_pProc->m_StackUsed = a_pProc->m_StackBegin-stackEnd; \
   assert (!(a_pProc->m_StackUsed >= STACK_SIZE)); \
   memcpy(a_pProc->m_Stack, (void*)stackEnd, a_pProc->m_StackUsed); \
   longjmp(*(a_pProc->m_pExecSystem), 1); }

So you are thinking "is this really fast enough to run in my game engine?" The scripts themselves are compiled C or CPP code so they will run at the same speed as the rest of your game engine. The overhead for calling one is about the same as a function call plus an additional memory copy. (See the notes for a way to optimize further by avoiding the memory copy) In terms of raw speed, you will not see much in wasted over-head. You would probably be able to run thousands of these scripts on a typical game without seeing a performance penalty.

Part of this article is a set of sample code that shows an implementation that runs two scripts concurrently. You can uncomment a test case that will run approximately 20,000 co-routines that count from 0 to 15. I think that's far more anyone would reasonably use, but serves an interesting stress test on how little time the routine management actually incurs. The sample code also shows that co-routines are defined in a cross platform manner. It compiles and runs perfectly under Linux GCC for the Playstation2, a platform that is quite different from the standard Intel machine.

To extend on this you could load up additional C – Scripts using a re-locatable code solution such as DLL's, or use a tiny run-time C compiler. The co-routine mechanism has worked very well for me in some extremely complex cases, with hundreds of scripts. Try it in your own game, I would love to hear how it works out for you.

Notes

1) Declaring C++ objects on the stack using this method will not be safe unless you know exactly what you're doing. Depending on your compiler flags with exception handling, the destructor for C++ objects may get called during the longjmp out.

2) The performance of the system can be improved by just adjusting the stack pointers to point to memory on the heap instead of copying the stack information around although it raises quite a few other restrictions and issues that will result a in a much more convoluted implementation.

3) On the PS2 the script manager or the jump off point where you call your routines must be compiled with no optimizations in release mode. It is not a big performance issue as you just move the call function to its own file. If anyone figures out a way around this, I would love to know. =)

Related links

http://fabrice.bellard.free.fr/tcc/ - Tiny C Compiler

http://www.lua.org - LUA

http://xmailserver.org/libpcl.html - Portable Co-Routine Library

Thanks to Joseph Millman for the editing assistance and feedback.




Contents
  Page 1
  Page 2

  Source code
  Printable version
  Discuss this article