다음을 통해 공유


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

  • Anonymous
    March 30, 2005
    > 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
  • Anonymous
    March 30, 2005
    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.
  • Anonymous
    March 30, 2005
    The comment has been removed
  • Anonymous
    March 30, 2005
    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 ?
  • Anonymous
    March 30, 2005
    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 ?
  • Anonymous
    March 31, 2005
    The comment has been removed
  • Anonymous
    March 31, 2005
    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 :-)
  • Anonymous
    March 31, 2005
    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.
  • Anonymous
    March 31, 2005
    The comment has been removed
  • Anonymous
    March 31, 2005
    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.
  • Anonymous
    April 01, 2005
    The comment has been removed
  • Anonymous
    April 01, 2005
    I'm just wondering: Won't calling SendNotifyMessage from WndProc create recursiveness?
  • Anonymous
    April 01, 2005
    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).
  • Anonymous
    April 01, 2005
    If you are working in managed code on games you should check out this blog entry from Rick Hoskinson:...
  • Anonymous
    April 01, 2005
    "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.
  • Anonymous
    April 01, 2005
    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><br>
  • Anonymous
    April 01, 2005
    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>
  • Anonymous
    April 01, 2005
    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!
  • Anonymous
    February 13, 2006
    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).
  • Anonymous
    January 24, 2008
    PingBack from http://softwareinformation.247blogging.info/rick-hoskinsons-blog-the-ideal-systemwindowsforms-3d-gameloop-take-15/