Direct3D 11 - Basic Window

This tutorial is the start of a series explaining and demonstrating the use of Direct3D 11 via SlimDX. Readers are expected to know how to use their chosen .NET language and IDE before attempting to understand these tutorials, as language concepts will not be explained.

The first tutorial will deal with getting a SlimDX project set up and ready to go, and will then explain the process of initializing a window and launching a rendering loop.

Introduction

SlimDX is a free open source framework that enables developers to easily build DirectX applications using any .NET language, including C#, VB.Net, F#, and IronPython. This tutorial series will serve as both an introduction to rendering through Direct3D as well as a guide for peculiarities specific to accessing it through SlimDX. In this way, the tutorials will be useful both for complete beginners as well as DirectX pros who are simply new to either managed code, Direct3D 11, or perhaps both.

Prerequisites

Developing applications using SlimDX requires several prerequisites. First and foremost, you must have the SlimDX SDK installed. The SDK contains everything you need to get started writing applications:


The last point is of some interest. While it is possible to write fully featured applications using the SlimDX SDK only, additional samples, documentation, and debugging tools can be obtained by downloading and installing the full DirectX SDK from Microsoft. For any non-trivial application development, this is highly recommended.

You will also need an IDE with which to develop your applications. The SlimDX SDK supports both Visual Studio 2008 and Visual Studio 2010. If you do not have access to the full versions of these software packages, you can download the express editions from Microsoft for free.

Other tools, such as those provided by graphics card manufacturers, can be of immense use for debugging and development as well. NVIDIA’s PerfHUD tool is commonly used for debugging rendering issues, while FxComposer can be used to interactively write shaders and effects. AMD has similar offerings, with GPU PerfStudio being used debugging and analysis and RenderMonkey being used for shader authoring. Both manufacturers also provide a wealth of other tools, whitepapers, debugging information, and samples, so check out their developer sites for more information.

Finally, as we will be working with Direct3D 11, it’s required that you run either Windows Vista with Service Pack 2 installed, or Windows 7. It’s helpful to have a D3D11-capable card to take advantage of the new hardware features, but thanks to the new feature level system it’s possible to use any Direct3D 9 or later hardware alongside Direct3D 11.

Project Setup

After creating a new .NET Windows Forms Application project in your chosen IDE, the first step towards writing a SlimDX application is to add a reference to the SlimDX DLL itself. This process isn’t as straightforward as for most .NET libraries, as the issue of x86 vs. x64 needs to be taken into account. The SlimDX installer ships with binaries for.NET 2.0-3.5, which will be installed in the Global Assembly Cache (GAC).



Applications that run on the CLR are inherently instruction-set neutral, meaning the JIT will determine at runtime whether your process will run in 32-bit or 64-bit mode. However, native DLLs cannot be loaded into a process unless they match the instruction-set size, so SlimDX ships with both binaries which are each compiled against the corresponding DirectX DLLs. Chances are good that you’d like your application to run under both x64 and x86 automatically without having to compile specific versions, so following is the method for doing so:


Note that this technique will only work if SlimDX is installed into the GAC. If you are building from source or redistributing the SlimDX binaries alongside your application, you will have to target a specific version manually.

.NET Framework 4.0

The SlimDX SDK contains a set of binaries built against .NET 4.0, but as of the June release there is no end-user installer for them. This is due to changes in the VC redist format for VS 2010, which no longer contains both x86 and x64 redists in one package. We are still working on getting an installer package for .NET 4.0, but until then you will need to browse manually to the reference of choice and redistribute the binaries alongside your application. As mentioned above, doing this will require you to target a specific platform version manually instead of being able to take advantage of automatic processor architecture selection.

Window Creation

For a C++ / DirectX tutorial, this is normally a long and involved section detailing how to properly set up the window and ensure that it plays nicely with the operating system. Luckily, .NET provides a vastly simplified wrapper around all of this, and it is therefore quite easy to get a window visible and ready for rendering.

While it’s quite easy and perfectly valid to make use of the default provided Form object as your rendering surface (or any other type derived from Control), SlimDX also provides a specialized RenderForm class that overrides a few message handlers to reduce flickering and sets defaults for the size of the client area. Additionally, it automatically loads the SlimDX icon and uses it as the icon for the window. Use of this class is optional, but it will be employed throughout the tutorial series.

var form = new RenderForm("Tutorial 1: Basic Window");

Message Loop

All Windows applications make use of a message loop to pump operating system messages to the appropriate windows and handle the correct responses. For real time applications, rendering and game update logic is commonly performed whenever there is idle time between window messages. Since a C++ application performs all message pumping itself, it is easy to insert this code into the correct place.

For .NET code, however, this process is hidden away beneath the Application.Run method. In order to achieve continuous rendering, several techniques are often employed, with varying levels of success. These suboptimal methods are presented here along with their downsides, and then the preferred method for running a game loop in managed code is revealed. The SlimDX library contains code to support the last method, and so it will be the operation of choice for this tutorial series.

The Paint Loop

One of the most naive ways that people tend to use to get a continuous stream of render calls in .NET is to hijack the Windows paint messages and ensure that they are continuously generated. This could look something like this:

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    RenderFrame();    // do rendering
    Invalidate();     // ensure that OnPaint is called again right away
}

The problem with this is twofold. First, Invalidate ends up making several small allocations to handle the wiring of the events. Normally this isn't a problem for the .NET garbage collector, but when you're calling it continuously throughout the life of your application, the little extra garbage can add extra strain that isn't needed. The second problem is that you're appropriating an operating system process that is designed to handle painting of windows intermittently. It simply isn't designed to handle continuous painting and repainting, and you're going to see some overhead from redrawing the window at every possible opportunity.

Application.DoEvents

The second method often employed to pump messages is the Application.DoEvents method. This method, when called, handles all pending window messages and then immediately returns. Here is what such a method would look like:

while (running)
{
    RenderFrame();
    Application.DoEvents();    // handle any pending window messages
}

This code has the advantage of avoiding the painting process for windows, is simple, and lets you render continuously while still handling any incoming window messages. Unfortunately, as shown by Tom Miller, DoEvents still ends up allocating each call, which can increase the frequency of garbage collections. While gen0 collections of short lived temporaries are quite fast, having them often can promote your own short-lived objects into gen1, which will be detrimental to performance.

The Solution: P/Invoke

The solution to this dilemma is to use interop to directly call into Win32 methods to bypass any allocations on the managed side. Once again Tom Miller provides an example and an overview of this solution in his blog. While this method isn't as simple as the others, it is certainly the most optimal in terms of speed and memory usage. This method was the preferred method for all of the now-deprecated MDX samples, and is the preferred method for SlimDX.

[StructLayout(LayoutKind.Sequential)]
public struct Message
{
    public IntPtr hWnd;
    public uint msg;
    public IntPtr wParam;
    public IntPtr lParam;
    public uint time;
    public Point p;
}
        
[SuppressUnmanagedCodeSecurity]
[DllImport("user32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);

static bool AppStillIdle
{
    get
    {
        Message msg;
        return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
    }
}

public void MainLoop()
{
    // hook the application's idle event
    Application.Idle += new EventHandler(OnApplicationIdle);
    Application.Run(form);
}
        
void OnApplicationIdle(object sender, EventArgs e)
{
    while (AppStillIdle)
    {
        // Render a frame during idle time (no messages are waiting)
        RenderFrame();
    }
}

As can be seen, this method takes a lot more code and is more complex than the other, simpler methods. SlimDX presents a solution to this issue, to cut down on tedious boilerplate code while still taking advantage of the performance and memory benefits provided by this option:

MessagePump.Run(form, RenderFrame);

The MessagePump class provides three overloads of the Run method, allowing you to completely hide the use of Application.Run. If you wish to still hook the Application.Idle event yourself but don’t want to P/Invoke PeekMessage, you can use the provided MessagePump.IsApplicationIdle property to do so.

Conclusion

This tutorial served as a gentle introduction to the SlimDX API and development kit. Necessary prerequisites were listed and explained, and then the process for setting up a bare bones project was presented in step-by-step detail. Finally, different methods for performing window initialization and message pumping were presented and dissected.

Future tutorials will build off of this foundational code, so ensure that it is all well understood. Next up: an introduction to Direct3D and an overview of initializing it and presenting to the window.



Download the source code for this tutorial: BasicWindow.zip