The ideal System.Windows.Forms 3D Gameloop, Take 15.

I thought I'd take a break from my book to talk about some of the trickier points of using System.Windows.Forms for gaming.  If you read Tom Millers's blog, you've probably seen him address similar issues in his lengthy discussions on game loops in Managed.  The goal is to have no level 2 garbage collections at all during the execution of a real-time action game.  That means minimizing ref-type allocations.

First I’ll digress a bit by discussing how I handle input events.  Overriding OnMouse- OnKeyboard- functions is often better than attaching event handlers to the mouse events.  Currently, I have an InputHandler subsystem that is triggered by these overrides and generates input messages which are added to an input buffer.  Attaching input handlers directly to the form-defined events actually incurs a small allocation penalty in the form of enumerators (I assume are used by the event system).

Now for the good stuff:  the ideal game loop.  As in all frame-based 3D games, we needed a “heartbeat” that works with the windows message pump to process frames.  There are two things we took into consideration: processing overhead and memory overhead.  Memory overhead is the most devastating, causing the allocations that can lead to generation 2 collections that can cause hiccups that can be measured in tenths of seconds (or deciseconds if you’re a total Latin-masochist).  If you’re trying to guarantee 60 frames per second or better, these collections during the normal course of play are unacceptable.

 

The first loop I used was the old standby loop: 

 

while (Created)

{

   Frame();

   Application.DoEvents();

}

 

Sadly, this loop is mortifyingly bad about allocations.  To give you an example, if you do nothing else in a frame, this kind of loop can generate over 50MB of garbage in 100000 frames.  Under CLR profiler, you’ll see a shark tooth pattern of allocation and collection emerge in the timeline.  Lots of collections == bad long-term performance.

 

 

There are lots of gameloops I didn’t try because I’ve seen the results firsthand.  There’s a PeakMessage loop.  There’s a timer-based loop.  Tom and I have discussed these options and many others at length, and we have found something we didn’t like in every case.

 

On a hunch, I decided to place my Frame() function in the WndProc override.  This means I am not overriding or hooking any events that might cause allocations in EventArgs.  However, triggering this event without blocking other message types was a little trickier.  I decided to trigger my frame on WM_PAINT (0x000F) so that I would redraw on any kind of validation as well as my own invalidation.

 

First, here’s some setup code I put in the constructor so that I would handle all painting:

 

SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);

Next, the ultra-simplistic WndProc override:

 

protected override void WndProc(ref Message m)

{

   if (m.Msg == 0x000F)

   {

   Frame();

   this.Invalidate();

   }

  else

  base.WndProc(ref m);

}

 

Wow, was this slow.  The processing overhead made this technique 3 to 4 times slower than the DoEvents() gameloop.  When I pulled things apart in CLR profiler, I discovered correlating numbers of heap creations for InvalidateEventArgs and some sort of synchronization reference type in excess of 3 or 4 MB per 100,000 frames.  So this was bad on both fronts.

 

However, with a simple DllImport, I can do everything I wanted to do via Invalidate() using SendNotifyMessage.

 

[DllImport("user32.dll")]

public static extern int SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

protected override void WndProc(ref Message m)

{

if (m.Msg == 0x000F)

{

   Frame();

   SendNotifyMessage(this.Handle, 0x000F, IntPtr.Zero, IntPtr.Zero);

}

else

   base.WndProc(ref m);

}

 

 

I know this seems insanely simple, but of the dozens of ways to make a game loop, this is the best one I’ve ever seen.  The timeline graph in CLR profiler was almost perfectly flat – so much so that memory overhead was undetectable over 1,000,000 frames when compared to the Form setup and cleanup code.  And processing overhead?  This was consistently 10 to 15 times faster than Invalidate() and 3 to 6 times faster than DoEvents() over 100,000 frames.

 

I now have my “ideal” solution for the game that will appear in my upcoming book.  Additionally, I will suggest this for DXMUT in upcoming releases of the DirectX SDK (since it’s kinda my job J).  This may not be the perfect solution, but it solves the most significant problems I’ve encountered when developing a gameloop using System.Windows.Forms.