
| Initializing Direct3D |
|
Finding a Usable Back-Buffer Format |
|
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:
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
)
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
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 )
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
|
|
|