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.

 

Comments (21)

  1. Don Alvarez says:

    > I now have my “ideal” solution for the game that will appear in my upcoming book

    Any chance you have an ISBN or some other way through which to pre-order the book? (I tried searching for hoskinson directx on amazon.com, but that didn’t work…)

    thanks,

    -Don

  2. Heh, we’re still pretty far out on the book (we’re due to print sometime after Visual Studio 2005 ships). I’m hoping my publisher rectifies the author names and Title soon. In the interim, the ISBN is:

    ISBN 0-672-32695-7

    Sadly, that will still bring up a book by Tom Miller. I beleive I have a post from January that has the correct name and authors of the book listed. SAMS hasn’t listed the new information yet, so I’ll definately be asking them about it next time I talk to them.

  3. ShadowChaser says:

    Sending a message (WM_PAINT) per frame still seems like a bit of a hack to me – it’s like driving in a circle for a mile when you need to go next door.

    It’s basically hacking the messaging system into an infinate loop so that a virtual hook can be placed into it.

    That said, there’s not much we can do. I’ve given up and manually create my windows and message loop from scratch. If only they had a way to attach a delegate into the main application loop. I imagine that although it would be slower than actually writing the code into your own app loop, it’s probably going to be faster than routing kazillion messages through windows 😉

    Probably too late for Whidbey 🙁

  4. bonk says:

    Tom Miller told us recently at #manageddx on efnet that in the April SDK he will be having the infamous Doevents() introduced back in for the sampleframework because he recieved a lot of requests to avoid win32 completely and use windows.forms instead. He told us further that in one of the sdk updates after the april one he is going to eliminate doevents() (but still stay with windows forms).

    Rick,

    how does your solution relate to the one Tom Miller is planning. Are you discussing that issue with each other ?

  5. bonk says:

    Ooops, seems I did not read carefully enough. Looks like you have been discussing that with Tom Miller.

    So let me rephrase my question:

    Rick,

    will your gameloop be the same as the one we will see in the sampleframework of the next sdk updates ?

  6. Hi Bonk, the April SDK has already been released and does in fact use a WinForms implementation and DoEvents.

    Of course this technique will be reseached further before we can commit to puting it into DXMUT. You may find that we subtley change the idea to using a user event rather than a paint message, or a managed create message rather than a p-invoke. So we are working on an alternative to DoEvents() in the SDK, but the April release will be using it.

    And ShadowChaser, I’m not really sure what you mean. Are you giving up on winforms altogether (which is the speediest solution, no doubt, but you lose a lot of .Net comforts)? Or are you implementing a PeekMessage-style loop?

  7. ShadowChaser says:

    I am using a combination approach.

    For apps that require "Rich UIs" (like map editors) I am using Windows Forms. Then the ‘core’ game engine is constructed I pass in an IntPtr to the "drawing" window so that the game knows where to initialize Direct-X Graphics.

    For the actual game itself I gave up on WinForms and I’m using a very low level approach – CreateWindowEx, etc. 🙁

    Your solution is definately the best one I’ve seen so far, but it’s still a little frustrating to need to do workarounds 🙂

    One change I might make when I implement your architecture into my WinForm version would be to use a custom message instead of WM_PAINT. (ie/ send a WM_USER+(whatever) and respond on both that and WM_PAINT). It’s probably irrational, but WM_PAINT is so common I have this strange fear that other apps are going to hook into it and change the behavior and performance of the game in a bad way. Skinning applications might be an example.

    Take a look at: http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackid=bd3ecd25-1bdb-4ca2-bcde-313f86876d4c

    The suggestion probably will be cancelled or delayed, but it’s worth a try 🙂

  8. Mike Potter says:

    I am certainly not an expert on the issue but:

    Have you tried putting your Frame call in OnIdle()?

    I found this to have very smooth animation but, I was just toying with MDX at the time. I did not analyize it with profiler. It allowed me to avoid PInvoke calls.

    Just curious if this has been tried by someone else.

  9. Heya ShadowChaser, you actually hit the nail on the head with the user event, and doing it that way was my first instinct. I agree, I’m not sure how people are going to try to use my game code, and I want it to be as flexible as possible.

    One of the ideas I’m playing with is pausing both my world update when the game window loses focus. Now the SDK samples handle this situation by throttling back thier update loop by inserting a mid-stream sleep. That keeps the app from starving other foreground applications. I wanted to take this a step further by pausing the world update and the render update completely, with the option to call just the rendering code if any part of the window is invalidated. That should drop the CPU usage in the background to a true 0% while giving me a more elegant looking WndProc. So to pseudocode:

    case WM_PAINT:

    if(Foreground) WorldUpdate();

    RenderWorld();

    if(Foreground) SendNotify(this.Handle, WM_PAINT);

    Since the code is part of the teaching tool, this should be a realy straightforward way of acheiving the intended effect without abstracting away the Forms usage with my own code.

    And Mike, I was wondering where you’re catching the OnIdle event — are you using the Application.Idle event or something else?

  10. Manuel says:

    Why this cannot be put in a Windows.Forms 2.0 method by the Winforms team? I don’t believe this is a dx only issue.

  11. Mike Potter says:

    I just reviewed my old code. I was using the Application.Idle and calling Invalidate() to keep it circulating. It was much smoother then DoEvents() when I first started learning.

    I switched the code to use the loop you specified above. Your method added about 50 to 100 FPS to the app in a windowed mode. This is most likely the overhead for Invalidate that you mention. The test app averages about 3400 fps on my box.

    I also have the following in my OnIdle():

    if (!this.Focused)

    {

    Thread.Sleep(10);

    }

    When I don’t have focus, It drops my app down to about 100 FPS but, the other apps seem to run at normal speed.

  12. Mashiharu says:

    I’m just wondering: Won’t calling SendNotifyMessage from WndProc create recursiveness?

  13. Looks like it’s the Control.NotifyInvalidate method that’s creating InvalidateEventArgs when it calls OnInvalidated. It’s virtual, so I’m wondering if nulling it out could solve the problem.

    Spelunking a little further, OnInvalidated calls OnParentInvalidated on the children and then fires the Invalidated event. So you’d lose those two things.

    The nice thing about invalidating the window after rendering is that it keeps the message queue clean (since WM_PAINT isn’t sent until all messages have been processed).

  14. If you are working in managed code on games you should check out this blog entry from Rick Hoskinson:…

  15. "I’m just wondering: Won’t calling SendNotifyMessage from WndProc create recursiveness?"

    SendMessage will, SendNotifyMessage does not block.

    Mike, I’m glad to hear the technique had positive results in your loop. I agree though that things will be trickier when you have children Control objects taht also need paint notification. Though I think that by using SendNotifyMessage you won’t starve out other messages since it will be in-ordered with everything else that happened in the previous frame. I suppose Invalidate() gives a bit more deterministic state at the start of your next WM_PAINT handler.

  16. Zman says:

    I posted a summary of *all* the Render loop posts over the past couple of years
    <br>
    <br><a target="_new" href="http://www.thezbuffer.com/articles/185.aspx">http://www.thezbuffer.com/articles/185.aspx</a&gt;
    <br>

  17. Bernhard says:

    Hello
    <br>
    <br>I have put your renderloop in my app too and the rendering works find. Unfortunately i have problems with the mouse events now!
    <br>The game runs with appr 300 fps on my machine and some mouse events are lost (eg. if i do a short click).
    <br>On another machine i only have appr. 100 fps (because if an old graphics card). Here the problem is much bigger, sometimes i do not get mouse events for up to one second!
    <br>
    <br>Does anyone see this problem too?
    <br>

  18. Bernhard says:

    hello
    <br>
    <br>some hours ago i wrote you that i have problems with the mouse events. the information was not correct, the mouse events are fired perfectly – the problem is my mouse handling functions that are working completly different now. i have to find the problem here. sorry for that!

  19. Paul Stubbs says:

    Now that the XBOX 360 controller works on windows I thought it would be fun to hook it up to a windows forms application using Visual C# Express and Managed DirectX (MDX).

Skip to main content