With that out of the way, we will begin with Chapter 4, Direct3D Initialization. This chapter covers basic initialization of the Direct3D device, and lays out the application framework used for the rest of the book. If you’ve read any of Mr. Luna’s other DirectX books, this framework will look very familiar, and even if you have not, it is basically a canonical example of a Windows game loop implementation. The framework provides a base class that handles creating a window, initializing DirectX, and provides some virtual methods that can be overridden in derived classes for application-specific logic. Additionally, this chapter introduces a timer class, which allows for updating the game world based on the elapsed time per frame, and for determining the frame rate, using the high-performance system timer.
If you are not familiar with the standard game loop, the control flow is as follows, in pseudo-code:
void main(){ Init(); while( running){ if ( there are windows events){ Handle system events } else { Determine elapsed time since last frame Update(deltaTime); Render(); } } Cleanup(); }
Now, the original code for the book is written in C++, and we are going to use C# instead. Fortunately, it is relatively straightforward to convert C++ windows code to use C# and WinForms in .NET, and since SlimDX is a thin wrapper around the DirectX interface, there will not be many necessary changes there, either. However, there are some foundations that we will need to lay before we begin converting the C++ library code to C#. I invite you to view my github repository, at https://github.com/ericrrichards/dx11.git, for the full code of the examples, as I won't be including every line of code ( this post is lengthy enough as is...)
First, we have a utility class that contains some functions that would be exposed via macros in C++:
1: public static class Util {
2: public static void ReleaseCom(ComObject x) {
3: if (x != null) {
4: x.Dispose();
5: x = null;
6: }
7: }
8: public static int LowWord(this int i) {
9: return i & 0xFFFF;
10: }
11: public static int HighWord(this int i) {
12: return (i >> 16) & 0xFFFF;
13: }
14:
15: }
The first static method is a helper to free SlimDX COM objects, such as the Device, Textures, buffers, etc. It is important to free these objects when they are no longer in use, as they are not automatically garbage-collected, and depending on your settings in the DirectX control panel, unfreed COM objects can crash your programs if they are still alive when the program exits.
The latter two methods are extension methods that return the high and low-order bits of an integer. The implementations are taken from the windows.h system header macros LOWORD and HIWORD. We’ll use these functions when we get to implementing a custom Windows event loop for our demo application.
Next, we are going to subclass the default WinForms Form class. Derived forms are able to substitute their own implementations of the Windows event loop, which is not possible if one merely creates an instance of the Form class. We are going to want to capture some messages in our application which are not exposed particularly well by the existing C# events in the Form class, which is why we are jumping through this particular hoop.
1: public delegate bool MyWndProc(ref Message m);
2: public class D3DForm : Form {
3: public MyWndProc MyWndProc;
4: protected override void WndProc(ref Message m) {
5: if (MyWndProc != null) {
6: if (MyWndProc(ref m)) return;
7: }
8: base.WndProc(ref m);
9: }
10: }
We define a delegate for a custom injectable message dispatching function. In the overridden WndProc message handler, we check if the custom handler is defined. If it is, and the message is handled by the user-defined function, we skip the default processing, otherwise, we run the default message handler of the Form base class.
Next, we’ll convert the GameTimer class from C++ to C#. This class uses the system high-precision timer to measure time intervals. In C#, we use the Stopwatch class, from System.Diagnostics, to access the performance counter. The full code of the C# GameTimer class follows:
1: public class GameTimer {
2: private double _secondsPerCount;
3: private double _deltaTime;
4:
5: private long _baseTime;
6: private long _pausedTime;
7: private long _stopTime;
8: private long _prevTime;
9: private long _currTime;
10:
11: private bool _stopped;
12:
13: public GameTimer() {
14: _secondsPerCount = 0.0;
15: _deltaTime = -1.0;
16: _baseTime = 0;
17: _pausedTime = 0;
18: _prevTime = 0;
19: _currTime = 0;
20: _stopped = false;
21:
22: var countsPerSec = Stopwatch.Frequency;
23: _secondsPerCount = 1.0 / countsPerSec;
24:
25: }
26:
27: public float TotalTime {
28: get {
29: if (_stopped) {
30: return (float)(((_stopTime - _pausedTime) - _baseTime) * _secondsPerCount);
31: } else {
32: return (float)(((_currTime - _pausedTime) - _baseTime) * _secondsPerCount);
33: }
34: }
35: }
36: public float DeltaTime {
37: get { return (float)_deltaTime; }
38: }
39:
40: public void Reset() {
41: var curTime = Stopwatch.GetTimestamp();
42: _baseTime = curTime;
43: _prevTime = curTime;
44: _stopTime = 0;
45: _stopped = false;
46: }
47:
48: public void Start() {
49: var startTime = Stopwatch.GetTimestamp();
50: if (_stopped) {
51: _pausedTime += (startTime - _stopTime);
52: _prevTime = startTime;
53: _stopTime = 0;
54: _stopped = false;
55: }
56: }
57:
58: public void Stop() {
59: if (!_stopped) {
60: var curTime = Stopwatch.GetTimestamp();
61: _stopTime = curTime;
62: _stopped = true;
63: }
64: }
65:
66: public void Tick() {
67: if (_stopped) {
68: _deltaTime = 0.0;
69: return;
70: }
71: var curTime = Stopwatch.GetTimestamp();
72: _currTime = curTime;
73: _deltaTime = (_currTime - _prevTime) * _secondsPerCount;
74:
75: _prevTime = _currTime;
76: if (_deltaTime < 0.0) {
77: _deltaTime = 0.0;
78: }
79: }
80: }
In our application, we will call Reset() before entering our game loop, to clear the timer. Every frame, we will call Tick(), to update the timer. When we call our overridden Update method, we will pass in the DeltaTime property, to inform the game world how much time has passed since the last frame. Finally, when the application is paused, minimized, or otherwise loses focus, we will want to pause, so that the world is not updated. To do this, we will call the Timer’s Stop method, and then when focus is regained, call the Start method to resume timing. The one odd wrinkle in this code is the check at the end of the Tick method to ensure that _deltaTime is greater than 0. I have not experienced this, but the original code mentions that in the case that the process is shuffled between processors or the machine enters a power-save state, the time delta can become negative.
The Demo Framework
With this groundwork out of the way, we can begin to create the demo application framework class. Here is the class declaration, omitting instance fields:
public class D3DApp : DisposableClass { public static D3DApp GD3DApp; public Form Window { get; protected set; } public IntPtr AppInst { get; protected set; } public float AspectRatio { get { return (float)ClientWidth / ClientHeight; } } protected D3DApp(IntPtr hInstance) protected override void Dispose(bool disposing) protected bool InitMainWindow() protected bool InitDirect3D() protected void CalculateFrameRateStats() private bool WndProc(ref Message m) // framework methods, to be overridden in derived classes public virtual bool Init() public virtual void OnResize() public virtual void UpdateScene(float dt) {} public virtual void DrawScene(){} // Main entry point public void Run() }
We’ll go through each of these members in turn.
- GD3DApp – This is something of a poor-man’s Singleton. Only a single instance of a D3DApp-derived class should be created in a user application; this field is set in the D3DApp constructor. I will probably change this to be a more proper implementation of the Singleton pattern, as described here, with a public get/private set property exposed, and a private volatile backing field.
- Window – A handle to the main application window, created by InitMainWindow().
- AppInst – This is a handle to the application instance. I am still in a more-or-less direct porting process presently, so I have included this field, although I am not sure that it is necessary. This field should be supplied from Process.GetCurrentProcess().Handle.
- AspectRatio – Returns the ratio of window width/height. We’ll use this later in setting up the projection matrix.
- D3DApp(IntPtr hInstance) – The class constructor. As mentioned previously, hInstance is a handle to the application process, received from Process.GetCurrentProcess().Handle. This parameter may not be necessary, using WinForms – honestly, I am not sure what purpose having the application handle around serves, as it is not necessary for window creation with WinForms, and that appears to be the only usage of the handle in the original code. The code of this method is rather trivial – all it does is set the class’ instance fields to default values. The meat of the initialization is performed by the virtual Init() method.
- Dispose(bool disposing) – This is an override of the base class DisposableClass method. Here, we release all the SlimDX COM objects that the D3DApp class manages. The DisposableClass implementation ensures that this method will be called when the application is cleaned up by the garbage collector, so, as long as we release all of our COM resources here, we don’t need to worry about any additional cleanup.
- InitMainWindow() – This function create the application window. I’m going to post the full listing for this function, as there are a couple of wrinkles contained therein that I want to go over.
protected bool InitMainWindow() { try { Window = new D3DForm { Text = MainWindowCaption, Name = "D3DWndClassName", FormBorderStyle = FormBorderStyle.Sizable, ClientSize = new Size(ClientWidth, ClientHeight), StartPosition = FormStartPosition.CenterScreen, MyWndProc = WndProc, MinimumSize = new Size(200, 200), }; Window.MouseDown += OnMouseDown; Window.MouseUp += OnMouseUp; Window.MouseMove += OnMouseMove; Window.ResizeBegin += (sender, args) => { AppPaused = true; Resizing = true; Timer.Stop(); }; Window.ResizeEnd += (sender, args) => { AppPaused = false; Resizing = false; Timer.Start(); OnResize(); }; Window.Show(); Window.Update(); return true; } catch (Exception ex) { MessageBox.Show(ex.Message + "\n" + ex.StackTrace, "Error"); return false; } }
First, we create the form object, which is our custom D3DForm type, as discussed previously. We set the caption of the window title bar to the instance field MainWindowCaption. We specify that the form should have a resizable border, with an initial height and width, and that the window should be initially positioned center-screen. We also specify a minimum size, so that the user cannot resize the form such that it is too small. Additionally, we specify that the form should use the custom Windows message handler, WndProc, which we will discuss next.
We bind the form’s mouse events to our virtual event handlers, which have been ommitted, as they are at this point empty – if you require mouse events, you can override the handlers in a derived class.
We also bind the ResizeBegin/End events. While the user is resizing the form, we will want to pause updating and rendering the world. Once the resizing is complete, we will want to resume, and we will also need to recreate some of our DirectX resources, as the dimensions of the application window will have changed.
Finally, we pop the window using the Show/Update combo. If anything went wrong during this process, we pop and messagebox with the error information.
- InitDirect3D() – Here is where we will initialize the Direct3D device, and create our render target and depth/stencil buffer. I’m going to break this function down into chunks, as it is quite meaty, and deserves some explanation.
protected bool InitDirect3D() { var creationFlags = DeviceCreationFlags.None; #if DEBUG creationFlags |= DeviceCreationFlags.Debug; #endif
First, we determine if we are building in Debug mode. If we are, then we can create a debug device, which will provide us some additional logging to the Visual Studio output window, which can be helpful when things don’t go quite right
try { Device = new Device(DriverType, creationFlags); } catch (Exception ex) { MessageBox.Show("D3D11Device creation failed\n" + ex.Message + "\n" + ex.StackTrace, "Error"); return false; } ImmediateContext = Device.ImmediateContext; if (Device.FeatureLevel != FeatureLevel.Level_11_0) { MessageBox.Show("Direct3D Feature Level 11 unsupported"); return false; }
Next, we create the device. We pass in the DriverType which we would like to use, which is an instance variable of the class. In almost all cases, we will be using the hardware driver, DriverType.Hardware, although you could potentially use DriverType.Reference, if you did not have a DX11 compatible graphics card, aand could tolerate it being slow… We also grab the new Device’s ImmediateContext, which will be used for rendering commands, and verify that the device does indeed support the DX11 feature level.
Debug.Assert((Msaa4XQuality = Device.CheckMultisampleQualityLevels(Format.R8G8B8A8_UNorm, 4)) > 0); try { var sd = new SwapChainDescription() { ModeDescription = new ModeDescription(ClientWidth, ClientHeight, new Rational(60, 1), Format.R8G8B8A8_UNorm) { ScanlineOrdering = DisplayModeScanlineOrdering.Unspecified, Scaling = DisplayModeScaling.Unspecified }, SampleDescription = Enable4xMsaa ? new SampleDescription(4, Msaa4XQuality - 1) : new SampleDescription(1, 0), Usage = Usage.RenderTargetOutput, BufferCount = 1, OutputHandle = Window.Handle, IsWindowed = true, SwapEffect = SwapEffect.Discard, Flags = SwapChainFlags.None }; SwapChain = new SwapChain(Device.Factory, Device, sd); } catch (Exception ex) { MessageBox.Show("SwapChain creation failed\n" + ex.Message + "\n" + ex.StackTrace, "Error"); return false; } OnResize(); return true;
Next, we create the Direct3D swapchain. Coming from DirectX 9, the SwapChainDescription structure seems very similar to the old PresentationParameters struct that was used in device creation there. Creating the SwapChain is relatively straightforward. Note that, depending on whether we use multi-sampling anti-aliasing, we need to create different SampleDescription structures.
Finally, we call the virtual OnResize method (which we will discuss shortly), to actually create our back and depth/stencil buffers and bind them to the device.
- CalculateFrameRateStats() – Here, we calculate the frame rate and the time per frame, and append this information to the main window caption.
protected void CalculateFrameRateStats() { frameCount++; if ((Timer.TotalTime - timeElapsed) >= 1.0f) { var fps = (float)frameCount; var mspf = 1000.0f / fps; var s = string.Format("{0}\tFPS: {1}\tFrame Time: {2} (ms)", MainWindowCaption, fps, mspf); Window.Text = s; frameCount = 0; timeElapsed += 1.0f; } }
WndProc(ref Message m) – This is our custom windows event handler. This is actually not as hairy as it looks… We are primarily concerned with two messages, WM_ACTIVATE and WM_SIZE.
WM_ACTIVATE is raised when the main window form gains or loses focus. Technically, we could handle this with the WinForms Activated event, but I have pulled my hair out previously trying to do all the book-keeping necessary to handle the state involved there(why there couldn’t be a simple flag to indicate whether the form was gaining or losing focus in the event’s arguments, I don’t know), and have finally settled on falling back to the raw Windows message as the easier way to deal with gaining/losing focus.
WM_SIZE is raised whenever the size of the application form is changed. We handle user-initiated resizing with the ResizeBegin/End events, so we ignore that case. Otherwise, we determine which state the window is in, and whether we should pause or not, then kick over to the virtual OnResize function.
Lastly, when the application is closed, a WM_DESTROY message will be raised. We catch this message to toggle the state flag which controls our game loop.
private bool WndProc(ref Message m) { switch (m.Msg) { case WM_ACTIVATE: if (m.WParam.ToInt32().LowWord() == 0) { AppPaused = true; Timer.Stop(); } else { AppPaused = false; Timer.Start(); } return true; case WM_SIZE: ClientWidth = m.LParam.ToInt32().LowWord(); ClientHeight = m.LParam.ToInt32().HighWord(); if (Device != null) { if (m.WParam.ToInt32() == 1) { // SIZE_MINIMIZED AppPaused = true; Minimized = true; Maximized = false; } else if (m.WParam.ToInt32() == 2) { // SIZE_MAXIMIZED AppPaused = false; Minimized = false; Maximized = true; OnResize(); } else if (m.WParam.ToInt32() == 0) { // SIZE_RESTORED if (Minimized) { AppPaused = false; Minimized = false; OnResize(); } else if (Maximized) { AppPaused = false; Maximized = false; OnResize(); } else if (Resizing) { } else { OnResize(); } } } return true; case WM_DESTROY: _running = false; return true; } return false; }
- On to our virtual framework methods!
- Init() – Derived classes will use this method to instantiate any application-specific resources. The base class implementation, which should be called prior to any application-specific loading, calls our InitMainWindow and InitDirect3D functions to create the window and get DirectX up and running.
- OnResize() – This function recreates our dept/stencil and render target buffers when the dimensions of the application window change. It also sets the primary viewport to cover the entirety of the application window. This function can be overridden in derived classes if different functionality is desired.
public virtual void OnResize() { Debug.Assert(ImmediateContext != null); Debug.Assert(Device != null); Debug.Assert(SwapChain != null); Util.ReleaseCom(RenderTargetView); Util.ReleaseCom(DepthStencilView); Util.ReleaseCom(DepthStencilBuffer); SwapChain.ResizeBuffers(1, ClientWidth, ClientHeight, Format.R8G8B8A8_UNorm, SwapChainFlags.None); using (var resource = SlimDX.Direct3D11.Resource.FromSwapChain<Texture2D>(SwapChain, 0)) { RenderTargetView = new RenderTargetView(Device, resource); } var depthStencilDesc = new Texture2DDescription() { Width = ClientWidth, Height = ClientHeight, MipLevels = 1, ArraySize = 1, Format = Format.D24_UNorm_S8_UInt, SampleDescription = (Enable4xMsaa) ? new SampleDescription(4, Msaa4XQuality - 1) : new SampleDescription(1, 0), Usage = ResourceUsage.Default, BindFlags = BindFlags.DepthStencil, CpuAccessFlags = CpuAccessFlags.None, OptionFlags = ResourceOptionFlags.None }; DepthStencilBuffer = new Texture2D(Device, depthStencilDesc); DepthStencilView = new DepthStencilView(Device, DepthStencilBuffer); ImmediateContext.OutputMerger.SetTargets(DepthStencilView, RenderTargetView); Viewport = new Viewport(0, 0, ClientWidth, ClientHeight, 0.0f, 1.0f); ImmediateContext.Rasterizer.SetViewports(Viewport); }
- UpdateScene(float dt) & DrawScene() – These functions need to be overridden in derived classes to actually update the world and render it to the screen.
- Run() – This is our game loop. It is pretty self-explanatory, if you’ve had any experience with the standard game loop pattern before.
public void Run() { Timer.Reset(); while (_running) { Application.DoEvents(); Timer.Tick(); if (!AppPaused) { CalculateFrameRateStats(); UpdateScene(Timer.DeltaTime); DrawScene(); } else { Thread.Sleep(100); } } Dispose(); }
Let’s See Something!
With the demo framework code in place, we can now create a simple driver program to get something up onto the screen. We’ll need to:
- Subclass D3DApp
- Implement the DrawScene method.
- Provide a Main function
- That’s it!
This is an extremely simple example, with no world to update, so we can omit the UpdateScene method, and no additional objects to create, so we can rely on the base D3DApp’s Init method. The full code of the example follows:
1: class InitDirect3D : D3DApp {
2: private bool _disposed;
3: public InitDirect3D(IntPtr hInstance) : base(hInstance) {
4: }
5: protected override void Dispose(bool disposing) {
6: if (!_disposed) {
7: if (disposing) {
8:
9: }
10: _disposed = true;
11: }
12: base.Dispose(disposing);
13: }
14:
15: public override void DrawScene() {
16: Debug.Assert(ImmediateContext!= null);
17: Debug.Assert(SwapChain != null);
18: ImmediateContext.ClearRenderTargetView(RenderTargetView, Color.Blue);
19: ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Depth|DepthStencilClearFlags.Stencil, 1.0f, 0);
20:
21: SwapChain.Present(0, PresentFlags.None);
22: }
23: }
24:
25: class Program {
26: static void Main(string[] args) {
27: SlimDX.Configuration.EnableObjectTracking = true;
28: var app = new InitDirect3D(Process.GetCurrentProcess().Handle);
29: if (!app.Init()) {
30: return;
31: }
32: app.Run();
33: }
34: }
NOTE: The line SlimDX.Configuration.EnableObjectTracking=true; turns on SlimDX’s internal object tracking mechanism (duh…) This will report any unfreed COM objects when the program closes, which can be very helpful.
Voila!
No comments :
Post a Comment