Initializing Direct3D

Introduction

Creating the Direct3D Device

Success and Failure

Finding a Usable Back-Buffer Format

Rendering Loop

Rendering

Making a Scene

Presenting the Results and Rendering Review

Lost Devices

Switching Modes

Lesson Downloads

Introduction

By the end of this lesson you will be able to create your first Direct3D application. It won't do much, but it will create a window and initialize Direct3D. We'll also cover clearing the display and presenting the results to the user. We won't do any real rendering in this lesson.

Creating the Direct3D Device

A Direct3D Device is an interface used to communicate with a video card. If you wanted to render to multiple video cards at the same time, you would need 1 device for each card. The creation of a device is fairly involved, but much simpler than the C++ version.

There is an intimidating amount of data that can be provided when creating a device. To avoid having to provide 80 different parameters (many optional) to a single function, we instead fill out a data structure to specify how we want our device created. This structure is called PresentParameters and it has the following Properties:

AutoDepthStencilFormat
This sets the format of the Depth/Stencil buffer created along with the device. This is ignored unless EnableAutoDepthStencil is set to true. This property will be covered in a later tutorial.
BackBufferCount
Legal values are from 0 to 3, with 0 being treated as 1. Typically only a single back buffer is used. This is called double-buffering since you draw to the back buffer and then the scene is displayed as a whole to the user. More buffers can smooth out your frame rate, but they can also cause input lag (delay between when the user hits a key and when he sees the results). They also consume extra memory.
BackBufferFormat
This describes the bit depth of the back buffer (16 and 32-bit are typical). 8-bit and 24-bit back buffers are not supported in DX9. The format also describes the how the format of the bits in each pixel. The 2 most common 16-bit formats are R5G6B5 and X1R5G5B5. Both are 16-bit formats, but the second has 1 bit unused, while the first assigns the extra bit to the green component. You can query the device to find a usable format using CheckDeviceType.
BackBufferWidth and BackBufferHeight
Requested dimensions for the backbuffers. In full-screen mode these must much a supported display mode, but in Windowed mode can be any size (and if set to 0 the size of the window will be used). For this tutorial we use a fairly safe 800x600.
DeviceWindow
This is the window to be associated with the device and is the default target for Present (which displays the results of rendering).
DeviceWindowHandle
This is only used for Win32 interop. Ignore this unless you're mixing Win32 and .Net code.
EnableAutoDepthStencil
Enables the creation of a Depth/Stencil buffer. For now, set it to false.
ForceNoMultiThreadedFlag
Set this to true if you're using multi-threading, otherwise false.
FullScreenRefreshRateInHz
Refresh rate to be used. Must be 0 in windowed mode. Setting this to unsupported values can yield odd or even possibly dangerous results. Left unset, Direct3D will choose a reasonable default.
MultiSample and MultiSampleQuality
These allow full-screen antialiasing. Not covered in this tutorial.
PresentationInterval
This controls the delay in presenting the back buffer to the user. In windowed mode, this value must be PresentInterval.Default (0). For a full-screen swap chain, it can be PresentInterval.Default or the value that corresponds exactly to one of the flags enumerated in PresentInterval.
PresentFlag
Flags to set advanced behaviour. See the documentation for more details.
SwapEffect
This sets the method used to Present the rendered image to the user. Discard tells the device to use the most efficient method so that's what we'll use here. The other methods are beyond the scope of this tutorial.
Windowed
If false, Direct3D will take over the display and change the display mode to match the dimensions given in BackBufferWidth and BackBufferHeight. If true, the display mode will not be changed and you will render to the client area of the Device Window.

A device is created in the same way as any other object, through the "new" keyword. There are 3 different constructors available. Two of these are designed for Win32 interop and will not be covered here. The third constructor for the device is as follows:

   Device(
      System.Int32 adapter,
      Microsoft.DirectX.Direct3D.DeviceType deviceType,
      System.Windows.Forms.Control renderWindow ,
      Microsoft.DirectX.Direct3D.CreateFlags behaviorFlags ,
      params Microsoft.DirectX.Direct3D.PresentParameters[] presentationParameters
   );
   Public Sub New(
      ByVal adapter As Integer,
      ByVal deviceType As Microsoft.DirectX.Direct3D.DeviceType,
      ByVal renderWindow As System.Windows.Forms.Control,
      ByVal behaviorFlags As Microsoft.DirectX.Direct3D.CreateFlags,
      ByVal ParamArray presentationParameters() As Microsoft.DirectX.Direct3D.PresentParameters
   )
adapter
This specifies which display adapter the device should be associated with. In most cases there is a one-to-one relationship between adapters and video cards. On cards that support multi-head (they can drive multiple monitors from a single card) each "head" may be its own adapter. To get the primary display, use 0.
deviceType
There are 3 possible values:
  • Hardware - This is the one that will be used most often. It is fully accelerated by the graphics card and will give you the best performance.
  • Reference - The Reference device is a software device that (almost) fully implements the DirectX9 specification. It will support many features that your graphics card will not. This makes it useful for testing features that you wouldn't have access to when using a Hardware device. Also, the Reference device is useful for testing for driver errors. When unexpected behaviour is observed when using a Hardware device, the same code can be run on the Reference device and if it works on Reference then it is possible that it is a bug in the graphics card driver rather than the application. This device is implemented in software and is coded for correctness not performance, as a result even simple scenes will render very slowly (single digit frames per second). This device is only available on computers that have the DirectX SDK installed.
  • Software - Choosing the Software device would allow the application to use an optimized software rasterizer rather than depend on hardware. Currently none are available.
renderWindow
This is the default target for the results of rendering. It can be overridden by passing a different window to the Present call. In full-screen mode this window must be a top-level window, it can not be a child window or control. In windowed mode this may be null, but then a window is required to be set in the Present call. Typically this will be set to the same window set in the DeviceWindow member of the PresentParameters.
behaviorFlags
These flags can be OR'd together to define the behaviour of the device. One (and only one) of the following flags must be set:HardwareVertexProcessing, SoftwareVertexProcessing, MixedVertexProcessing.
  • AdapterGroupDevice - Used with cards that have multiple "heads" (they can display to multiple monitors from a single card). This is an advanced flag and will not be covered here.
  • DisableDriverManagement - Requests that resources be managed by Direct3D rather than the device. This would only be used to work around bugs in a driver.
  • HardwareVertexProcessing - All vertex processing will be done by the graphics card. This requires support from the graphics card.
  • SoftwareVertexProcessing - All vertex processing will be done by the CPU. This will work on any card. This is what most of these tutorials will use for the sake of simplicity.
  • MixedVertexProcessing - Vertex processing can be changed between Software and Hardware at will. This is often used to gain performance from Hardware processing while still being able to access features through Software that the graphics card may not support.
  • PureDevice - Only used with HardwareVertexProcessing. The device will not support most Get*() methods. This sacrifices convenience for a performance boost.
  • MultiThreaded - Indicates that the device will be used by a multi-threaded application. This can lower performance slightly. In Managed DirectX this defaults to true.
  • FpuPreserve - Causes DirectX to save the FPU state before each call and reload it after each call. This will degrade performance but allows the use of floating point exceptions and doubles.
presentationParameters
The present parameters set the behaviour related to displaying the results of rendering to the user. After creating the device a number of members of this class may be changed.

This code sample illustrates how to create a device in both full-screen and windowed mode.

   //Assume this is pre-initialized to your choice of full-screen or windowed mode.

   bool full_screen; 

   //Hard-coded to a common format.  A better method will be shown later in this lesson.

   Format format = Format.R5G6B5;

   //Allocate our class

   PresentParameters present_params=new PresentParameters();

   //No Z (Depth) buffer or Stencil buffer

   present_params.EnableAutoDepthStencil = false;

   //1 Back buffer for double-buffering

   present_params.BackBufferCount = 1;

   //Set our Window as the Device Window

   present_params.DeviceWindow = this;

   //Do not wait for VSync

   present_params.PresentationInterval = D3D.PresentInterval.Immediate;

   //Discard old frames for better performance

   present_params.SwapEffect = D3D.SwapEffect.Discard;

   //Set Windowed vs. Full-screen

   present_params.Windowed = !full_screen;

   //We only need to set the Width/Height in full-screen mode

   if(full_screen) {
      present_params.BackBufferHeight = m_size.Height;
      present_params.BackBufferWidth = m_size.Width;

      //Choose a compatible 16-bit mode.

      present_params.BackBufferFormat = format;
   }else{
      present_params.BackBufferHeight = 0;
      present_params.BackBufferWidth = 0;
      present_params.BackBufferFormat = D3D.Format.Unknown;
   }

   Device device=new D3D.Device(0,                       //Adapter

                               D3D.DeviceType.Hardware,  //Device Type

                               this,                     //Render Window

                               D3D.CreateFlags.SoftwareVertexProcessing, //behaviour flags

                               present_params);          //PresentParamters

   'Assume this is pre-initialized to your choice of full-screen or windowed mode.

   Dim full_screen As Boolean

   'Hard-coded to a common format.  A better method will be shown later in this lesson.

   Dim format As Format = Format.R5G6B5

   'Allocate our class

   Dim _presentParameters As D3D.PresentParameters = _
          New D3D.PresentParameters

   With _presentParameters
      'No Z (Depth) buffer or Stencil buffer

      .EnableAutoDepthStencil = False

      '1 Back buffer for double-buffering

      .BackBufferCount = 1

      'Set our Window as the Device Window

      .DeviceWindow = Me

      'Do not wait for VSync

      .PresentationInterval = D3D.PresentInterval.Immediate

      'Discard old frames for better performance

      .SwapEffect = D3D.SwapEffect.Discard

      'Set Windowed vs. Full-screen

      .Windowed = Not full_screen

      'We only need to set the Width/Height in full-screen mode

      If Me.FullScreen Then
         .BackBufferHeight = _size.Height
         .BackBufferWidth = _size.Width

         'Choose a compatible 16-bit mode.

         .BackBufferFormat = format
      Else
         .BackBufferHeight = 0
         .BackBufferWidth = 0
         .BackBufferFormat = D3D.Format.Unknown
      End If
   End With

Success and Failure

When a failure occurs an exception is thrown. All DirectX exceptions are inherited from the DirectXException class. Standard exception handling (try/catch) is used in DirectX. Following is a code sample showing how to bracket a Direct3D call with try/catch and interrogate a resulting exception.

   try{

      //Direct3D calls go here


   }catch(GraphicsException exception){

      //Trace is in System.Diagnostics

      //WriteLine outputs to the IDE's Output window

      Trace.WriteLine("Error Code:" + exception.ErrorCode);
      Trace.WriteLine("Error String:" + exception.ErrorString);
      Trace.WriteLine("Message:" + exception.Message);
      Trace.WriteLine("StackTrace:" + exception.StackTrace);
   }

   Try

      'Direct3D calls go here


   Catch exception As GraphicsException

      'Trace is in System.Diagnostics

      'WriteLine outputs to the IDE's Output window

      Trace.WriteLine("Error Code:" + exception.ErrorCode);
      Trace.WriteLine("Error String:" + exception.ErrorString);
      Trace.WriteLine("Message:" + exception.Message);
      Trace.WriteLine("StackTrace:" + exception.StackTrace);
   End Try

Logging this information could make debugging much easier. Hopefully as Managed DirectX matures the quality of the information provided by the exception will improve.

Finding a Usable Back-Buffer Format

In the code sample presented above the format for the back-buffer was hard-coded. This was done to keep the sample focussed on the device creation. A real application should never do this. DirectX9 compatible graphics cards can have a dizzying array of capabilities. An application that runs well on a developer's computer may crash on a user's computer if these differences aren't taken into account.

In the Direct3D namespace there is a static class called Manager. This class can be queried to test various capabilities of the video card(s) present in the user's computer. The CheckDeviceType is used to query a device for compatibility with a back-buffer format.

bool CheckDeviceType(
   System.Int32 adapter,
   Microsoft.DirectX.Direct3D.DeviceType checkType,
   Microsoft.DirectX.Direct3D.Format displayFormat,
   Microsoft.DirectX.Direct3D.Format backBufferFormat,
   System.Boolean windowed
)
Public Shared Function CheckDeviceType(
   ByVal adapter As Integer,
   ByVal checkType As Microsoft.DirectX.Direct3D.DeviceType,
   ByVal displayFormat As Microsoft.DirectX.Direct3D.Format,
   ByVal backBufferFormat As Microsoft.DirectX.Direct3D.Format,
   ByVal windowed As Boolean
) As Boolean
adapter
Determines which graphics adapter is queried. 0 is the default adapter.
checkType
The type of device to create. This is one of Hardware/Software/Reference as mentioned above in Device creation.
displayFormat
The format of the adapter display mode for which the device type is being checked. For example, some devices operate only in modes of 16 bits per pixel.
backBufferFormat
The format of the buffer that is the default target for all rendering operations.
windowed
Indicates whether the device will be running in windowed or full-screen mode.

For most applications displayFormat and backBufferFormat will be set to the same value. In windowed mode applications will often clone the user's desktop (done automatically by using Format.Unknown) which removes the necessity of testing for a usable format.

Following is a short function that cycles through a list testing for a supported Format. More desirable formats should be listed first.

public Format Find16BitMode(){
   Format[] poss_formats = {D3D.Format.R5G6B5, D3D.Format.X1R5G5B5};
   DeviceType dev = DeviceType.Hardware;

   foreach(Format format in poss_formats){

      if (Manager.CheckDeviceType(0,dev,format,format,false)){
         return format;
      }
   }

   return Format.Unknown;
   
}
   Public Shared Function Find16BitMode() As D3D.Format
      Dim possFormats() As D3D.Format = {D3D.Format.R5G6B5, D3D.Format.X1R5G5B5}
      Dim dev As D3D.DeviceType = D3D.DeviceType.Hardware

      For Each format As D3D.Format In possFormats
         If D3D.Manager.CheckDeviceType(0, dev, format, format, False) Then
            Return format
         End If
      Next format

      Return D3D.Format.Unknown
   End Function

Rendering Loop

Though nothing is rendered in this tutorial, the basic structure of the render loop will be explored.

static void Main(){
D3DApp app = new D3DApp();

   app.Show();

   while ( app.Created ) { 

      app.Heartbeat(); 

      Application.DoEvents (); 

   }
}
   Public Shared Sub Main()
      Dim app As D3DApp = New D3DApp

      app.Show()

      Do While app.Created
         app._Heartbeat()
         Application.DoEvents()
      Loop
   End Sub

Simply, this will cause the application to loop as long as the window is valid. When the window is closed, the application exits the loop and terminates. Each iteration through the loop will render one frame of graphics. This is done through the application's Heartbeat function. Additionally, Windows will be sending messages to the application and these need to be handled promptly. A call to the system's static function DoEvents handles those messages.

The Heartbeat function is where any updating of the game state will occur and also where the Render function will be called. The additional code in this function is related to handling Lost Devices and will be covered later in this tutorial.

Rendering

The first thing that needs to be done before any rendering can occur is to clear the back buffer. There are a few different overloads for the Clear method. Some allow an array of rectangles to be passed in which define what areas of the back-buffer are cleared, but in this application the entire back-buffer will be cleared. This is the most common use of Clear. In addition to the back buffer, the Z Buffer and Stencil Buffer may be cleared as well. Z Buffers and Stencil buffers will be covered in a later lesson.

void Clear(
   Microsoft.DirectX.Direct3D.ClearFlags flags,
   System.Drawing.Color colour,
   System.Single zdepth,
   System.Int32 stencil
);
Public Sub Clear(
   ByVal flags As Microsoft.DirectX.Direct3D.ClearFlags,
   ByVal colour As System.Drawing.Color,
   ByVal zdepth As Single,
   ByVal stencil As Integer
)
flags
These flags specify which components get cleared. A buffer may have a Z (Depth) buffer and a Stencil buffer associated with it. If you specify that a component should be cleared when that component does not exist, the call will fail. The flags are:
  • Target - The current render target (which is the back-buffer by default) should be cleared.
  • ZBuffer - The depth buffer will be cleared.
  • Stencil - The stencil buffer will be cleared.
colour
This is a standard System.Drawing.Color. The entire backbuffer will be set to this colour. Other versions of this method may take the colour as a 32-bit number which encoded in Hex looks like 0xAARRGGBB. R, G, and B are the Red. Green, Blue components of the colour while the A is the alpha component. If you choose to use that version set the Alpha to 255 (0xFF), using other values is a more advanced topic.
zdepth
This value can range from 0.0 (nearest) to 1.0 (farthest). This value is ignored if the ZBuffer flag isn't set.
stencil
This parameter can be in the range of 0 through 2n-1, where n is the bit depth of the stencil buffer. This is ignored if the Stencil flag isn't set.
   device.Clear(D3D.ClearFlags.Target, //Flags, only clear the backbuffer

             Color.CadetBlue,       //Clear it to CadetBlue, a rather nice blue I think

             1.0f,                  //ZBuffer, ignored

             0                      //Stencil, ignored

   );
   _device.Clear(D3D.ClearFlags.Target, Color.CadetBlue, 1.0, 0)

Making a Scene

To indicate that rendering is about to begin, BeginScene must be called. Similarly, when rendering of the current scene is complete, EndScene must be called. Neither of these functions take any parameters.


   device.BeginScene();

   //Spiffy rendering goes here


   device.EndScene();

   With _device
      .BeginScene()

      'Spiffy rendering goes here


      .EndScene()

   End With

Presenting the Results and Rendering Review

Finally, the results of the rendering must be shown to the user. This is done with a call to Present. The simple version of this function takes no parameters and is the one most commonly used. The more advanced version isn't covered here.

This is the complete Render function used in the sample code.

protected void Render(){
Color colour=Color.CadetBlue;

   m_device.Clear(D3D.ClearFlags.Target,colour,1.0f,0);

   m_device.BeginScene();

   //Spiffy rendering goes here


   m_device.EndScene();

   m_device.Present();

}
Protected Sub _Render()
   Dim colour As Color = Color.Thistle

   With _device
      .Clear(D3D.ClearFlags.Target, colour, 1.0, 0)

      .BeginScene()

      'Spiffy rendering goes here


      .EndScene()

      .Present()
   End With
End Sub

Lost Devices

Lost Devices are an important topic. If you have a full-screen application and a user hits ALT+TAB to get back to the desktop, your device loses it's access to the display. There are a few other events that can cause it to happen, but users ALT+TABing away is the most common.

When you've lost your device you are unable to render anything. Attempts to call Present will simply raise an exception. One drastic way to sort things out is to free your device and all your resources. But there is an easier way.

When the device is lost a DeviceLostException will be raised. When this occurs rendering will not be possible. Also the device cannot be Reset yet. Many resources will survive a Reset, but some do not. The ones that won't survive it need to be freed before the device can be Reset.

When a lost device is ready to be Reset, a DeviceNotResetException will be raised. This indicates that the device can be Reset and any resources that need to be, can be recreated.

You can rely on your normal Render call to raise these exception when there is a problem, but it means running through your whole Render function only to fail on Present. It would also be nice to detect when this happens and put your application to sleep so it doesn't waste CPU time. Before each call to Render, a call to CheckCooperativeLevel will check if the device is lost. If the function returns true, then the device is fine. If it returns false, it also passes back a result code which can be checked to see what state the device is in (Lost/NotReset).

System.Boolean CheckCooperativeLevel ( out System.Int32 result )
Public Function CheckCooperativeLevel(ByRef result As Integer) As Boolean

The complete Heartbeat function illustrates one method of handling Lost Devices.

protected void Heartbeat(){
int result;

   if (m_device.CheckCooperativeLevel(out result)){  //Okay to render


      try{
         Render();
      }catch(D3D.DeviceLostException){
         m_device.CheckCooperativeLevel(out result);
      }catch(D3D.DeviceNotResetException){
         m_device.CheckCooperativeLevel(out result);
      }

   }
   if (result == (int)D3D.ResultCode.DeviceLost){
      Thread.Sleep(500);    //Can't Reset yet, wait for a bit

   }else if (result == (int)D3D.ResultCode.DeviceNotReset){
      m_device.Reset(m_present_parameters);
   }


}
Protected Sub _Heartbeat()
   Dim result As Integer

   If _device.CheckCooperativeLevel(result) Then  'Okay to render

      Try
         _Render()
      Catch ex As D3D.DeviceLostException
         _device.CheckCooperativeLevel(result)
      Catch ex As D3D.DeviceNotResetException
         _device.CheckCooperativeLevel(result)
      End Try
   End If

   If result = CType(D3D.ResultCode.DeviceLost, Integer) Then
      Thread.Sleep(500)     'Can't Reset yet, wait for a bit

   ElseIf result = CType(D3D.ResultCode.DeviceNotReset, Integer) Then
      _device.Reset(_presentParameters)
   End If
End Sub

Thread.Sleep is a static function which causes the application to wait for a given number of milliseconds. While in a sleep state the application will not use any CPU cycles.

To Reset the device, the present parameters are required. Reset can also be used to change the device from full-screen to windowed mode. More on that later.

void Reset ( params Microsoft.DirectX.Direct3D.PresentParameters[] presentationParameters )
Public Sub Reset(ByVal ParamArray presentationParameters() As Microsoft.DirectX.Direct3D.PresentParameters)

In the Heartbeat function there is no code to handle freeing resources or re-initializing them. That code is register as an event handler on the device. In the constructor, after the device was created 2 event handlers were registered.

   device.DeviceLost += new EventHandler(OnDeviceLost);
   device.DeviceReset += new EventHandler(OnDeviceReset);
   AddHandler _device.DeviceLost, AddressOf OnDeviceLost
   AddHandler _device.DeviceReset, AddressOf OnDeviceReset

OnDeviceLost and OnDeviceReset are 2 methods in the class that will be automatically called by the device at the appropriate time. Since this lesson has no resources, those event handlers are empty.

Managed DirectX is designed to be easier and more user-friendly than unmanaged DirectX. The handlers for DeviceLost and DeviceReset are 2 examples of how well this can work. Some features are either badly implemented or badly documented (or both) so that they end up being more annoying than helpful. Managed DirectX will by default try to handle window resizing in a transparent manner. However this same code fires when simply resetting a device after returning from an ALT+TAB and it will throw exceptions. The easiest way to handle this is to simply disable this handler.

device.DeviceResizing += new System.ComponentModel.CancelEventHandler(this.CancelResize);

protected void CancelResize(object sender, System.ComponentModel.CancelEventArgs e) {
   e.Cancel = true;
}
AddHandler _device.DeviceResizing, AddressOf CancelResize

Protected Sub CancelResize(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs)
   e.Cancel = True
End Sub

Switching Modes

As mentioned above, Reset can also be used to change the device after it's been created. This will allow you to switch between full-screen and windowed mode easily. Since the original window code supports toggling between these modes, it only makes sense to extend that support to handle the necessary device handling.

This is accomplished by overriding the GoWindowed and GoFullscreen methods. The code used by both functions is nearly the same so only GoWindowed will be shown here. The full source is available in the project source files.

protected override void GoWindowed(){

   base.GoWindowed();

   init_present_params();
   
   //We don't want to call this the first time through since the device

   //isn't set up yet.

   if (m_device != null){
         m_device.Reset(m_present_parameters);
   }
}
Protected Overrides Sub _GoWindowed()
   MyBase._GoWindowed()

   _InitPresentParams()

   'We don't want to call this the first time through since the device

   'isn't set up yet.

   If Not _device Is Nothing Then _device.Reset(_presentParameters)
End Sub

First, the base (D3DWindow) GoWindowed method is called. This sets up all of the proper window states and sets the internal full-screen flag to false. Then init_present_params is called to set up our PresentParameters. Since the application was running in full-screen mode, the PresentParameters need to be modified for windowed mode. Finally, calling Reset sets the device into Windowed mode.

The window is created before the device and as part of its initialization it will call GoWindowed or GoFullscreen. At this point the device has not been initialized so it isn't safe to call Reset. This is why the device is checked for a null status before calling Reset.

Lesson Downloads

Back