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 SlimDX binaries, one each for x86 and x64 in both .NET 2.0 and .NET 4.0
- Compiled documentation and XML comments for Intellisense
- ClickOnce support
- The minimum required installation of DirectX
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:
- First, select either assembly listed for your chosen architecture and add it to the project. It doesn’t matter which you pick, because we will be modifying it in a moment.
- Right click the project in the solution explorer and select “Unload Project”.
- Right click the project again and select “Edit csproj”.
- The project will open in the text editor. Scroll down to where you can see the SlimDX reference element:
<ItemGroup> <Reference Include="SlimDX, Version=2.0.9.42, Culture=neutral, PublicKeyToken=b1b0c32fd1ffe4f9, processorArchitecture=AMD64" /> <Reference Include="System" /> <Reference Include="System.Drawing" /> <Reference Include="System.Windows.Forms" /> </ItemGroup>
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
