4.5 The Demo Application Framework
The demos in this book use code from the d3dUtil.h, d3dApp.h, and d3dApp.cpp files, which can be downloaded from the book’s website. These common files, which are used in every demo application, reside in a Common directory for Parts II and III of the book, so that the files are not duplicated in each project. The d3dUtil.h file contains useful utility code, and the d3dApp.h and d3dApp.cpp files contain the core Direct3D application class code that is used to encapsulate a Direct3D application. The reader is encouraged to study these files after reading this chapter, as we do not cover every line of code in these files (e.g., we do not show how to create a window, as basic Win32 programming is a prerequisite of this book). The goal of this framework was to hide the window creation code and Direct3D initialization code; by hiding this code, we feel it makes the demos less distracting, as you can focus only on the specific details the sample code is trying to illustrate.
4.5.1 D3DApp
The D3DApp class is the base Direct3D application class, which provides functions for creating the main application window, running the application message loop, handling window messages, and initializing Direct3D. Moreover, the class defines the framework functions for the demo applications. Clients are to derive from D3DApp, override the virtual framework functions, and instantiate only a single instance of the derived D3DApp class. The D3DApp class is defined as follows:
1 class D3DApp
2 {
3 public:
4 D3DApp(HINSTANCE hInstance);
5 virtual ~D3DApp();
6
7 HINSTANCE getAppInst();
8 HWND getMainWnd();
9
10 int run();
11
12 // Framework methods. Derived client class overrides these methods
13 // to implement specific application requirements.
14
15 virtual void initApp();
16 virtual void onResize(); // reset projection/etc.
17 virtual void updateScene(float dt);
18 virtual void drawScene();
19 virtual LRESULT msgProc(UINT msg, WPARAM wParam, LPARAM lParam);
20
21 protected:
22 void initMainWindow();
23 void initDirect3D();
24
25 protected:
26 HINSTANCE mhAppInst; // application instance handle
27 HWND mhMainWnd; // main window handle
28 bool mAppPaused; // is the application paused?
29 bool mMinimized; // is the application minimized?
30 bool mMaximized; // is the application maximized?
31 bool mResizing; // are the resize bars being dragged?
32
33 // Used to keep track of the "delta-time" and game time (§4.3).
34 GameTimer mTimer;
35
36 // A string to store the frame statistics for output. We display
37 // the average frames per second and the average time it takes
38 // to render one frame.
39 std::wstring mFrameStats;
40
41 // The D3D10 device (§4.2.2), the swap chain for page flipping
42 // (§4.1.4), the 2D texture for the depth/stencil buffer (§4.1.5),
43 // and the render target and depth/stencil views (§4.1.6). We
44 // also store a font pointer (§4.4) so that we can render the
45 // frame statistics to the screen.
46 ID3D10Device* md3dDevice;
47 IDXGISwapChain* mSwapChain;
48 ID3D10Texture2D* mDepthStencilBuffer;
49 ID3D10RenderTargetView* mRenderTargetView;
50 ID3D10DepthStencilView* mDepthStencilView;
51 ID3DX10Font* mFont;
52
53 // The following variables are initialized in the D3DApp constructor
54 // to default values. However, you can override the values in the
55 // derived class to pick different defaults.
56
57 // Hardware device or reference device? D3DApp defaults to
58 // D3D10_DRIVER_TYPE_HARDWARE.
59 D3D10_DRIVER_TYPE md3dDriverType;
60
61 // Window title/caption. D3DApp defaults to "D3D10 Application".
62 std::wstring mMainWndCaption;
63
64 // Color to clear the background. D3DApp defaults to blue.
65 D3DXCOLOR mClearColor;
66
67 // Initial size of the window's client area. D3DApp defaults to
68 // 800x600. Note, however, that these values change at run time
69 // to reflect the current client area size as the window is resized.
70 int mClientWidth;
71 int mClientHeight;
72 };
73
74
We have used comments in the above code to describe some of the data members; the methods are discussed in the subsequent sections.
4.5.2 Non-Framework Methods
-
D3DApp: The constructor simply initializes the data members to default values.
-
~D3DApp: The destructor releases the COM interfaces the D3DApp acquires.
-
getAppInst: Trivial access function returns a copy of the application instance handle.
-
getMainWnd: Trivial access function returns a copy of the main window handle.
-
run: This method wraps the application message loop. It uses the Win32 PeekMessage function so that it can process our game logic when no messages are present. The implementation of this function is shown in Appendix A.
-
initMainWindow: Initializes the main application window; we assume the reader is familiar with basic Win32 window initialization.
-
initDirect3D: Initializes Direct3D by implementing the steps discussed in §4.2.
4.5.3 Framework Methods
For each sample application in this book, we consistently override five virtual functions of D3DApp. These five functions are used to implement the code specific to the particular sample. The benefit of this setup is that all the initialization code, message handling, etc., is implemented in the D3DApp class, so that the derived class only needs to focus on the specific code of the demo application. Here is a description of the framework methods:
-
initApp: Use this method for initialization code for the application, such as allocating resources, initializing objects, and setting up lights. The D3DApp implementation of this method calls initMainWindow and initDirect3D; therefore, you should call the D3DApp version of this method in your derived implementation first, like this:
1 void AlphaTestApp::initApp()
2 {
3 D3DApp::initApp();
4
5 /* Rest of initialization code goes here */
6 }so that the ID3D10Device is available for the rest of your initialization code. (Direct3D resource acquisition generally requires a valid ID3D10Device.) In addition, the D3DApp implementation also creates an ID3DX10Font object, which should be used to output the frame statistics; the font creation code was shown in §4.4.
-
onResize: This method is called by D3DApp::msgProc when a WM_SIZE message is received. When the window is resized, some Direct3D properties need to be changed, as they depend on the client area dimensions. In particular, the back buffer and depth/stencil buffers need to be recreated to match the new client area of the window. The back buffer can be resized by calling the IDXGISwapChain::ResizeBuffers method. The depth/stencil buffer needs to be destroyed and then remade based on the new dimension. In addition, the render target and depth/stencil views need to be recreated. The D3DApp implementation of onResize handles the code necessary to resize the back and depth/stencil buffers; see the source code for the straightforward details. In addition to the buffers, other properties depend on the size of the client area (e.g., the projection matrix), so this method is part of the framework because the client code may need to execute some of its own code when the window is resized.
-
updateScene: This method is called every frame and should be used to update the 3D application over time (e.g., per form animation and collision detection, check for user input, calculate the frames per second, etc.). The D3DApp implementation of this method computes the average frames per second and time elapsed per frame, and stores the result in the mFrameStats string. In this way, by calling the D3DApp implementation of this method, you get the frame statistics ready for output. See §4.5.4 for details on how the frame statistics are computed.
-
drawScene: This method is invoked every frame and is used to draw the current frame of our 3D scene. The D3DApp implementation of this method simple clears the back buffer and depth/stencil buffer to prepare for drawing (it is common to always reset these buffers before drawing a new frame so that the values from the previous frame are not “left over”):
1 void D3DApp::drawScene()
2 {
3 // Clear the render target to the color specified by mClearColor.
4 md3dDevice->ClearRenderTargetView(mRenderTargetView, mClearColor);
5
6 // Clear the depth buffer to 1.0, and clear the stencil buffer
7 // to 0.
8 md3dDevice->ClearDepthStencilView(mDepthStencilView,
9 D3D10_CLEAR_DEPTH|D3D10_CLEAR_STENCIL, 1.0f, 0);
10 } -
msgProc: This method implements the window procedure function for the main application window. Generally, you only need to override this method if there is a message you need to handle that D3DApp::msgProc does not handle (or does not handle to your liking). The D3DApp implementation of this method is explored in §4.5.5.
Note When you override these methods, you typically always want to call the base implementation from the
derived implementation. For example, if you override msgProc, but do not call D3DApp::msgProc from
the derived implementation, then you won’t get the message handling that D3DApp::msgProc provides.
Likewise, if you override updateScene, but do not call D3DApp::updateScene from the derived impleme-
ntation, then you won’t get the frame statistics calculated for you.
4.5.4 Frame Statistics
It is common for games and graphics applications to measure the number of frames being rendered per second (FPS). To do this, we simply count the number of frames processed (and store it in a variable n) over some specified time period t. Then, the average FPS over the time period t is fpsavg = n/t. If we set t = 1, then fpsavg = n/1 = n. In our code, we use t = 1 since it avoids a division, and moreover, one second gives a pretty good average — it is not too long and not too short. The code to compute the FPS is provided by the D3DApp implementation of updateScene:
1 void D3DApp::updateScene(float dt)
2 {
3 // Code computes the average frames per second, and also the
4 // average time it takes to render one frame.
5
6 static int frameCnt = 0;
7 static float t_base = 0.0f;
8
9 frameCnt++;
10
11 // Compute averages over one second period.
12 if( (mTimer.getGameTime() - t_base) >= 1.0f )
13 {
14 float fps = (float)frameCnt; // fps = frameCnt / 1
15 float mspf = 1000.0f / fps;
16
17 std::wostringstream outs;
18 outs.precision(6);
19 outs << L"FPS: " << fps << L"\n"
20 << "Milliseconds: Per Frame: " << mspf;
21
22 // Save the stats in a string for output.
23 mFrameStats = outs.str();
24
25 // Reset for next average.
26 frameCnt = 0;
27 t_base += 1.0f;
28 }
29 }
30
This method would be called every frame in order to count the frames.
In addition to computing the FPS, the above code also computes the number of milliseconds it takes, on average, to process a frame:
float mspf = 1000.0f / fps;
|
Note |
The seconds per frame is just the reciprocal of the FPS, but we multiply by 1000 ms / 1 s to convert from seconds to milliseconds (recall there are 1000 ms per second). |
The idea behind this line is to compute the time, in milliseconds, it takes to render a frame; this is a different quantity than FPS (but observe this value can be derived from the FPS). In actuality, the time it takes to render a frame is more useful than the FPS, as we may directly see the increase/ decrease in time it takes to render a frame as we modify our scene. On the other hand, the FPS does not immediately tell us the increase/decrease in time as we modify our scene. Moreover, as [Dunlop03] points out in his article FPS versus Frame Time, due to the non-linearity of the FPS curve, using the FPS can give misleading results. For example, consider situation (1): Suppose our application is running at 1000 FPS, taking 1 ms (millisecond) to render a frame. If the frame rate drops to 250 FPS, then it takes 4 ms to render a frame. Now consider situation (2): Suppose that our application is running at 100 FPS, taking 10 ms to render a frame. If the frame rate drops to about 76.9 FPS, then it takes about 13 ms to render a frame. In both situations, the rendering per frame increased by 3 ms, and thus both represent the same increase in time it takes to render a frame. Reading the FPS is not as straightforward. The drop from 1000 FPS to 250 FPS seems much more drastic than the drop from 100 FPS to 76.9 FPS; however, as we have just shown, they actually represent the same increase in time it takes to render a frame.
4.5.5 The Message Handler
The window procedure we implement for our application framework does the bare minimum. In general, we won’t be working very much with Win32 messages anyway. In fact, the core of our application code gets executed during idle processing (i.e., when no window message is present). Still, there are some important messages we do need to process. Because of the length of the window procedure, we do not embed all the code here; rather, we just explain the motivation behind each message we handle. We encourage the reader to download the source code files and spend some time becoming familiar with the application framework code, as it is the foundation of every sample for this book.
The first message we handle is the WM_ACTIVATE message. This message is sent when an application becomes activated or deactivated. We implement it like so:
1 case WM_ACTIVATE:
2 if( LOWORD(wParam) == WA_INACTIVE )
3 {
4 mAppPaused = true;
5 mTimer.stop();
6 }
7 else
8 {
9 mAppPaused = false;
10 mTimer.start();
11 }
12 return 0;
13
As you can see, when our application becomes deactivated, we set the data member mAppPaused to true, and when our application becomes active, we set the data member mAppPaused to false. In addition, when the application is paused, we stop the timer, and then resume the timer once the application becomes active again. If we look back at the implementation to D3DApp::run (§4.3.3), we find that if our application is paused, then we do not update our application code, but instead free some CPU cycles back to the OS; in this way, our application does not hog CPU cycles when it is inactive.
The next message we handle is the WM_SIZE message. Recall that this message is called when the window is resized. The main reason for handling this message is that we want the back and depth/stencil buffer dimensions to match the dimensions of the client area rectangle (so no stretching occurs). Thus, every time the window is resized, we want to resize the buffer dimensions. The code to resize the buffers is implemented in D3DApp::onResize. As already stated, the back buffer can be resized by calling the IDXGISwapChain::ResizeBuffers method. The depth/stencil buffer needs to be destroyed and then remade based on the new dimensions. In addition, the render target and depth/stencil views need to be recreated. If the user is dragging the resize bars, we must be careful because dragging the resize bars sends continuous WM_SIZE messages, and we do not want to continuously resize the buffers. Therefore, if we determine that the user is resizing by dragging, we actually do nothing (except pause the application) until the user is done dragging the resize bars. We can do this by handling the WM_EXITSIZEMOVE message. This message is sent when the user releases the resize bars.
1 // WM_ENTERSIZEMOVE is sent when the user grabs the resize bars.
2 case WM_ENTERSIZEMOVE:
3 mAppPaused = true;
4 mResizing = true;
5 mTimer.stop();
6 return 0;
7
8 // WM_EXITSIZEMOVE is sent when the user releases the resize bars.
9 // Here we reset everything based on the new window dimensions.
10 case WM_EXITSIZEMOVE:
11 mAppPaused = false;
12 mResizing = false;
13 mTimer.start();
14 onResize();
15 return 0;
16
Finally, the last three messages we handle are trivially implemented and so we just show the code:
1 // WM_DESTROY is sent when the window is being destroyed.
2 case WM_DESTROY:
3 PostQuitMessage(0);
4 return 0;
5
6 // The WM_MENUCHAR message is sent when a menu is active and the user
7 // presses a key that does not correspond to any mnemonic or accelerator
8 // key.
9 case WM_MENUCHAR:
10 // Don't beep when we alt-enter.
11 return MAKELRESULT(0, MNC_CLOSE);
12
13 // Catch this message to prevent the window from becoming too small.
14 case WM_GETMINMAXINFO:
15 ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
16 ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
17 return 0;
18
4.5.6 Going Full Screen
The IDXGISwapChain interface created with D3D10CreateDeviceAndSwapChain automatically catches the Alt+Enter key combination and will switch the application to full-screen mode. Pressing Alt+Enter while in full-screen mode will switch back to windowed mode. During the mode switch, the application window will be resized, which sends a WM_SIZE message to the application; this gives the application a chance to resize the back and depth/stencil buffers to match the new screen dimensions. Also, if switching to full-screen mode, the window style will change to one that works for the full screen if necessary. You can use the Visual Studio Spy++ tool to see the Windows messages that are generated by pressing Alt+Enter for the demo applications that use the sample framework.
One of the exercises at the end of this chapter explores how you can disable the default Alt+Enter functionality should you need to.
Note
|
The reader may wish to review the DXGI_SWAP_CHAIN_DESC::Flags description in §4.1.4. |
4.5.7 The Init Direct3D Demo
Now that we have discussed the application framework, let’s make a small application using it. The program requires almost no real work on our part since the parent class D3DApp does most of the work required for this demo. The main thing to note is how we derive a class from D3DApp and implement the framework functions, where we will write our sample-specific code. All of the programs in this book will follow the same template.
1 #include "d3dApp.h"
2
3 class InitDirect3DApp : public D3DApp
4 {
5 public:
6 InitDirect3DApp(HINSTANCE hInstance);
7 ~InitDirect3DApp();
8
9 void initApp();
10 void onResize();
11 void updateScene(float dt);
12 void drawScene();
13 };
14
15 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
16 PSTR cmdLine, int showCmd)
17 {
18 // Enable run-time memory check for debug builds.
19 #if defined(DEBUG) | defined(_DEBUG)
20 _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
21 #endif
22
23
24 InitDirect3DApp theApp(hInstance);
25
26 theApp.initApp();
27
28 return theApp.run();
29 }
30
31 InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance)
32 : D3DApp(hInstance)
33 {
34 }
35
36 InitDirect3DApp::~InitDirect3DApp()
37 {
38 if( md3dDevice )
39 md3dDevice->ClearState();
40 }
41
42 void InitDirect3DApp::initApp()
43 {
44 D3DApp::initApp();
45 }
46
47 void InitDirect3DApp::onResize()
48 {
49 D3DApp::onResize();
50 }
51
52 void InitDirect3DApp::updateScene(float dt)
53 {
54 D3DApp::updateScene(dt);
55 }
56
57 void InitDirect3DApp::drawScene()
58 {
59 D3DApp::drawScene();
60
61 // We specify DT_NOCLIP, so we do not care about width/height
62 // of the rect.
63 RECT R = {5, 5, 0, 0};
64 mFont->DrawText(0, mFrameStats.c_str(), -1, &R, DT_NOCLIP, BLACK);
65
66 mSwapChain->Present(0, 0);
67 }
68


浙公网安备 33010602011771号