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
93 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:

The Kernel

That's right, it's that moment you've all been waiting for.. the heart of the engine, the Kernel, and its jaw-dropping task-management ensemble.

The Kernel, which the more old-school game developers amongst you might know better as the Game Loop, is the beating heart of the engine - almost literally. It 'pumps' the various things the engine needs to do at any given time, looping through them in a round-robin. Each thing the engine is doing - a 'task' - gets a single Update() call every frame, in which it does whatever it needs to do. The Kernel exits the loop when there are no more tasks. It's quite a lot like multithreading, if you're familiar with that - in fact, the term 'Kernel' is borrowed from OS terminology. (Combining the game kernel with multithreading is something you should probably avoid; however, the concept of multiple game kernels for multiprocessor systems, where each processor runs one kernel in its own thread, is something I quite like).

The first thing to do is to define a 'task.' A task can be any object which supports the Update() function, along with a couple of others - so the sensible approach is to use a base ITask class as an interface to be implemented. (However, because I'm totally insane, I'm going to use....a herring. Or maybe not.)

class ITask : public IMMObject
{
public:
  ITask(){canKill=false;priority=5000;}
  virtual bool Start()=0;
  virtual void OnSuspend(){};
  virtual void Update()=0;
  virtual void OnResume(){};
  virtual void Stop()=0;

  bool canKill;
  long priority;
};

There's a little more there than just an Update() function. The interface provides for all stages in a task's life - creation, pause, resume, and destruction. The creation function - Start() - is boolean, allowing the task to say, 'No, can't start right now,' so we don't get confused tasks being updated. OnSuspend() and OnResume() are used when pausing and resuming tasks (for example, you'd pause the AI task while the user is accessing the in-game menu), just to notify the task that it should release any high-demand resources and get itself ready for a little wait; and, conversely, that it should pick up its resources and carry straight on again. The Update() function we've already covered; which leaves the Stop() function, called when a task is being removed from the kernel's list. Note that the Start(), Update(), and Stop() functions are pure virtual - forcing derived classes to implement them - but the OnSuspend and OnResume classes are only regular virtual, meaning that you can override them if you want the notification, but you don't have to. Some tasks won't need to do anything to pause or resume - they just stop getting Update() messages for a while.

The last two parts of the class, canKill and priority, are used to manage thread lifetime and execution order, respectively. The task sets canKill to true when it's ready to be Stop()'d; and the kernel calls Update() on the tasks in order of their priority numbers, with the lowest numbers happening first. The order can be very important - your custom rendering task should have a higher priority than the 'flip buffers' task, but a lower priority than the 'clear back buffer' task, for example.

Now we preset the Kernel itself. It's a singleton, and only really provides task management functions:

class CKernel : public Singleton<CKernel>
{
public:
  CKernel();
  virtual ~CKernel();

  int Execute();

  bool AddTask(CMMPointer<ITask> &t);
  void SuspendTask(CMMPointer<ITask> &t);
  void ResumeTask(CMMPointer<ITask> &t);
  void RemoveTask(CMMPointer<ITask> &t);
  void KillAllTasks();

protected:
  std::list< CMMPointer<ITask> > taskList;
  std::list< CMMPointer<ITask> > pausedTaskList;
};

You've got your standard constructor/destructor, an Execute() function - the function which runs the task loop and exits when the game is shutdown, a load of task-management functions, and then two protected lists for ITask pointers - one for running tasks, and one for paused tasks.

Why have two seperate lists? The alternative is to have a flag bIsPaused in the ITask class, which you set to true when the task should pause, and so on. The problem with such a system is that it's very prone to abuse - anything could pause any task at any time. There's no guarantee that the OnSuspend or OnResume functions would be called. We would also incur a cost each loop, in that we'd have to check the flag; given that the actions of pausing/resuming tasks will be fairly rare, the cost of moving the pointer from one list to another is negligable.

CKernel::CKernel()
{
  SDL_Init(0);
  SDLNet_Init();
}

CKernel::~CKernel()
{
  SDLNet_Quit();
  SDL_Quit();
}

So far so simple. These functions bring up and shut down SDL, without initialisng any of its subsystems. Seemed like a good enough place to me, at least; you're welcome to move them if you can think of some place better.

int CKernel::Execute()
{

  while(taskList.size())
  {
    {
      PROFILE("Kernel task loop");

      std::list< CMMPointer<ITask> >::iterator it;
      for(it=taskList.begin();it!=taskList.end();)
      {
        ITask *t=(*it);
        it++;
        if(!t->canKill)t->Update();
      }
      //loop again to remove dead tasks
      for(it=taskList.begin();it!=taskList.end();)
      {
        ITask *t=(*it);
        it++;
        if(t->canKill)
        {
          t->Stop();
          taskList.remove(t);
          t=0;
        }
      }
      IMMObject::CollectGarbage();
    }
#ifdef DEBUG
    CProfileSample::Output();
#endif
  }

  return 0;
}

And there you have it; the game loop. You can see the task system, the Memory Manager, and the Profiler all coming into play - this is where garbage collection gets called and the profiler gets told to output its statistics (as the "Kernel task loop" is the top-level profiler sample). All tasks are updated, and then any dead tasks are removed from the system; this is repeated until all tasks are dead, one way or another. You can see some slightly weird constructions with the STL iterators - that's because there's a risk of the task being removed from the list (either to be deleted, or to be moved to the paused list) in the Update() function, which would mess up the iterator.

bool CKernel::AddTask(CMMPointer<ITask> &t)
{
  if(!t->Start())return false;

  //keep the order of priorities straight
  std::list< CMMPointer<ITask> >::iterator it;
  for(it=taskList.begin();it!=taskList.end();it++)
  {
    CMMPointer<ITask> &comp=(*it);
    if(comp->priority>t->priority)break;
  }
  taskList.insert(it,t);
  return true;
}

void CKernel::SuspendTask(CMMPointer<ITask> &t)
{
  //check that this task is in our list - we don't want to suspend a task that isn't running
  if(std::find(taskList.begin(),taskList.end(),t)!=taskList.end())
  {
    t->OnSuspend();
    taskList.remove(t);
    pausedTaskList.push_back(t);
  }
}

void CKernel::ResumeTask(CMMPointer<ITask> &t)
{
  if(std::find(pausedTaskList.begin(),pausedTaskList.end(),t)!=pausedTaskList.end())
  {
    t->OnResume();
    pausedTaskList.remove(t);
    //keep the order of priorities straight
    std::list< CMMPointer<ITask> >::iterator it;
    for(it=taskList.begin();it!=taskList.end();it++)
    {
      CMMPointer<ITask> &comp=(*it);
      if(comp->priority>t->priority)break;
    }
    taskList.insert(it,t);
  }
}

void CKernel::RemoveTask(CMMPointer<ITask> &t)
{
  if(std::find(taskList.begin(),taskList.end(),t)!=taskList.end())
  {
    t->canKill=true;
  }
}

void CKernel::KillAllTasks()
{
  for(std::list< CMMPointer<ITask> >::iterator it=taskList.begin();it!=taskList.end();it++)
  {
    (*it)->canKill=true;
  }
}

That's the task-management API of the kernel. It covers all the basic things you can do to a task, including a KillAllTasks() function, which essentially causes the app to quit on the next task cycle. AddTask and ResumeTask keep the live tasks list sorted by priority, so that the highest-priority task (the one with the lowest number) is at the front and will therefore be executed first each cycle.

Th-th-th-that's all, folks

That's the whole of the foundation tier done. It's solid enough to support the rest of the engine, and if there are any problems it's centralised enough to make fixing things pretty simple. Next time we'll set up a few basic tasks (such as the video or input tasks), and look for the first time at how the engine integrates into a full program. Because what we've got so far is good, but doesn't do much without a main() function to start it all up...

Next time will also be the first time we do any significant work with SDL and FMOD, so make sure they're up to scratch as far as you're concerned. The addresses are http://www.libsdl.org/ and http://www.fmod.org/ for anyone who had forgotten.


Contents
  Runtime Profiler
  Settings
  The Kernel

  Source code
  Printable version
  Discuss this article

The Series
  Part I
  Part II
  Part III
  Part IV
  Part V