Introduction
This is the framework of a Managed DirectX render loop. These loops are used to cause a game or simulation to render graphics and update state with the highest possible performance while still being responsive to user input and system events, without bogging down the entire system. This article does not explain how to use DirectX or Direct3D, only how to best arrange the main engine loop of your game or simulation for the best performance and responsiveness.
Background
This implementation comes directly from Tom Miller's blog entry on May 05, 2005. Tom Miller is a well known author and the lead developer on the Managed DirectX team at Microsoft. After many iterations, he has discovered that this pattern will provide the best possible performance that a .NET managed application can attain without resorting to WinAPI tricks. A more complete framework is included in the June '05 version of the DirectX SDK, but I'm going to show you the simplest possible implementation and explain why/how it works and why it is the best. Using C++ and avoiding the .NET WinForms base classes can be even faster, but there are many published techniques already available. The purpose here is to create a 100% managed solution using only .NET code.
Using the code
I'm going to analyze the whole class by looking at snippets at a time. The downloadable source includes a complete MainForm
class.
When the application starts up, while still in the Main
method, we go ahead and bind to an odd event: Application.Idle
.
[STAThread]
public static void Main(){
MainForm myForm = new MainForm();
Application.Idle += new EventHandler(myForm.Application_Idle);
Application.Run(myForm);
}
If you are unfamiliar with Windows messages, they are simple notifications of events like key-presses and mouse movements, but can also cover things like system shutdown, minimizing your application, etc. Almost everything that happens within your computer ends up as a Windows message.
The Application.Idle
event is fired whenever our application is done processing all incoming Windows-generated messages. Our goal here is to allow our application to process as much as possible as quickly as possible, but we don't want to stop the flow of incoming Windows messages.
Here is our Application_Idle
event handler:
protected void Application_Idle(object sender, EventArgs e){
while(AppStillIdle){
}
}
public bool AppStillIdle{
get{
PeekMsg msg;
return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
}
}
The AppStillIdle
property makes use of a simple Win32 API function (PeekMessage
) which checks to see if there are any pending Windows messages that our application needs to process. The reason we go into the while
loop is that Application.Idle
only gets fired once when the application is finishing up processing all possible Windows message and the queue of pending messages is empty. So we want to continue looping until Windows happens to give us another message to process (when PeekMessage
returns true
). Then we'll drop out of the loop and leave Application_Idle
. The normal .NET WinForms Windows message handler will pick up the pending messages that we saw when PeekMessage
returned true
.
According to the Win32 API documentation, PeekMessage
passes out a structure different from the one that .NET defines in System.Windows.Forms.Message
. We have to redefine our own version of the Message
class called PeekMsg
:
[StructLayout(LayoutKind.Sequential)]
public struct PeekMsg {
public IntPtr hWnd;
public Message msg;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public System.Drawing.Point p;
}
The StructLayout
attribute is required when using .NET structs to pass into Win32 or COM.
Here's the native method definition for PeekMessage
:
[DllImport("User32.dll", CharSet=CharSet.Auto)]
private static extern bool PeekMessage(out PeekMsg msg, IntPtr hWnd,
uint messageFilterMin, uint messageFilterMax, uint flags);
Making it Faster
There are a few enhancements we can make to this application. Some of them aren't hard to do, but the reasons for them aren't very clear. The first, is setting the ControlStyle
of our form. This has to be done in the form constructor:
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
These lines cause the form to not use the standard .NET and Windows form-drawing code, as well as bypass extra drawing-related messages and events. Obviously if we are going to be using DirectX to draw our game or simulation, we don't need Windows and GDI+ to be doing a whole bunch for us that will never be visible. This is the case for Full-Screen as well as Windowed games and simulations.
Another thing we can do is to modify our native method definition for PeekMessage
:
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("User32.dll", CharSet=CharSet.Auto)]
private static extern bool PeekMessage(out PeekMsg msg, IntPtr hWnd,
uint messageFilterMin, uint messageFilterMax, uint flags);
By adding the SuppressUnmanagedCodeSecurity
attribute to the call, .NET will skip the security-related checks that it would make before allowing your application to make a call out to a native Windows method. This is extremely dangerous and should not be done without carefully considering the security ramifications of making the call. In this case, making the method declaration private is good enough, especially since the call we are making is merely going to tell us if there is a message waiting for us or not. Unfortunately, the method in question passes back the next Windows message if there is one. Malicious code could use this to intercept keystrokes, watch network traffic, or interpret and redirect mouse clicks. Be sure not to leave this method call somewhere where malicious code can access it by keeping it private.
Points of Interest
Did you notice that we did nothing at all with the Control.Paint
event? Well, the reason is that the event is not needed. Since the UserPaint style is set, .NET's control painting code gets disabled. Windows will still send the WM_PAINT
message, but as soon as your application gets done doing ... nothing, then Application_Idle
will fire and you'll be refreshing your DirectX surface anyways. If you really wanted do, you could make an additional call to render your scene from within the Control.Paint
event handler, but I do not know what this will do to performance or responsiveness.
History
- Released on July 14, 2005.