Contents
This article presents the CDSSD3DView8
class, a CView
-derived class that provides Direct3D support for use in MFC Single Document Interface (SDI) applications. The CDSSD3DView8
class is designed to be used instead of CView
as the base class for a developer's primary SDI View class.
The goal of this class is to provide functionality similar to the familiar Direct3D SDK CD3DApplication
class, but still allow the user the ease-of-use provided by a Visual C++ AppWizard-generated SDI application. The class is also designed so that users don't have to "tack on" Direct3D support in their SDI applications. This support is built right into the View class itself.
Searching the internet, I've found a few examples describing how to use OpenGL with SDI or how to mingle Direct3D with MFC, but nothing providing a generic CView
-derived class that can serve as a launching point for developing Direct3D applications using Visual Studio's Single Document Interface functionality. So I thought I'd present a class I've been using for my own development.
The pros and cons are a direct result of using SDI to build a Direct3D application. 3D rendering will be slightly slower than a "normal" Direct3D SDK application. There is no full-screen support, since SDI applications don't run full-screen. There's also no enumeration of adapters and display modes. But since an SDI application built with the CDSSD3DView8
class will always use the desktop as its target, there's no need for such support.
On the positive side, by developing an SDI application, you have available all of the AppWizard and ClassWizard support for ease of development. Although MFC/SDI may not be desired for game development, it makes development of tools for those games much easier. For example, I use the CDSSD3DView8
class as the base class for my main View for a DirectX Mesh editor that I'm diddling around with. It's far easier to use Visual C++ to create the supporting dialogs, menus, toolbars, message handlers, etc. etc. than it would be if using the DirectX SDK interface. I shudder to think of coding these resources by hand...
The CDSSD3DView8
class is defined in files DSSD3DView8.h and DSSD3DView8.cpp.
Also included is a sample program that renders a simple cube (the "Hello, world" of 3D development) under a flickering light to show how the CDSS3DView8
class can be used as a base class (instead of CView
) for an SDI application. All source code for this sample program has been included.
This class and the included sample program use DirectX 8.0. The program was written using Visual C++ 6.0, and tested on Windows XP platform with an NVIDIA GeForce4 MX440-SE video card. The class isn't very complex, so it can easily be adapted for later versions of DirectX. In addition, the MX440-SE is a bit dated, so there should be no trouble running the sample on a more advanced card.
This article assumes that the reader has a basic understanding of Direct3D and how it is accessed using the MSSDK DirectX toolkit. This isn't meant as a Direct3D primer, since there are mountains of articles and books available on the subject.
As mentioned, the CDSSD3DView8
class uses DirectX 8.0. So, you'll obviously need to have the DirectX 8.0 SDK toolkit and a compatible 3D video card installed. In addition, since the application was built using Visual C++ 6.0, I'd recommend that as the development platform. I haven't tested this in any other Visual Studio environment.
To create your own SDI application using the CDSSD3DView8
class, just run through the standard AppWizard pages to generate your SDI application's source code. When selecting the base View class, you'll have to select CView
. All other SDI options are entirely up to the user.
Once the application code has been created, the user will have to copy the CDSSD3DView8
header and source files to the application directory and make a few simple manual changes to the generated View Header and Source files to support CDSSD3DView8
as the base class. In addition, a few changes to the Project Settings is required.
At the start of the AppWizard-generated View header file, you'll have to add the reference to CDSSD3DView8
's header file:
#include "DSSD3DView8.h"
Next, change the View class' declaration so that it derives from CDSSD3DView8
rather than CView
. CDSSD3DView8
derives directly from CView
, so all CView
methods and data will still be available to the user. The example program's view class is shown here:
class CD3D8SDIView : public CDSSD3DView8
Finally, you'll want to delete the declaration for OnDraw
that was created by AppWizard. A full implementation of OnDraw
is contained in the CDSSD3DView8
class. Details of this implementation are discussed below. Suffice to say, however, that derived View classes won't need their own implementation of OnDraw
.
A few more manual changes are required in the generated .cpp file. The bulk of these are simple search-and-replace operations.
The first two changes are to the ClassWizard macros. Note that once these two changes are made, any handlers added via ClassWizard will automatically include a call to the corresponding member function in CDSSD3DView8
.
IMPLEMENT_DYNCREATE(CD3D8SDIView, CDSSD3DView8)
BEGIN_MESSAGE_MAP(CD3D8SDIView, CDSSD3DView8)
Next, you'll want to delete the implementation of OnDraw
. As mentioned above, View classes derived from CDSSD3DView8
will not need to provide their own OnDraw
implementation.
Finally, you'll want to search for all instances of "CView::
" base class calls and replace them with "CDSSD3DView8::
". This redirects the AppWizard-generated handlers to call the CDSSD3DView8
base class instead of CView
.
Finally, you may have to make a few changes to your Project->Settings. Under the C++ tab, check the Preprocessor category. If you haven't already specified the paths to the DirectX 8.0 SDK under Tool->Options, then you'll want to add those paths here.
Next, under Project->Settings, select the Link tab and add "d3dx8.lib d3dxof.lib d3d8.lib dxerr8.lib winmm.lib dxguid.lib" to the list of Object/Library modules.
That should take care of setting up your SDI project to include the CDSSD3DView8
class. You're ready to start coding! If this is a new application, build and run it. You'll be presented with the usual SDI frame, menus, toolbar, and status bar. The client area, however, should be pitch black. That's the underlying CDSS3DView8
class clearing the backbuffer and presenting the scene to the client area.
These next few sections go into more details on how to use the CDSSD3DView8
class' simple API for your own application development.
This section discusses various aspects of the CDSSD3DView8
interface that is exposed to the application programmer as well as some of the internal private
data and methods that provide the underlying Direct3D support. First, we list the API that is exposed for use by derived classes. This API is pretty simple, and many parts will be familiar to users of the Direct3D SDK's CD3DApplication
class. After that, the "guts" of the class are touched upon... those data members and methods that provide the underlying 3D support.
Here we discuss the simpler aspects of the CDSSD3DView8
interface. The various prototypes defined in the header file are covered here, and any additional notes of clarification are included.
The core developer API methods follow the familiar Direct3D SDK prototypes. Like their counterparts in the SDK's CD3DApplication
class, these base class implementations do very little.
virtual HRESULT InitDeviceObjects() { return S_OK; }
virtual HRESULT InvalidateDeviceObjects() { return S_OK; }
virtual HRESULT RestoreDeviceObjects() { return S_OK; }
virtual HRESULT DeleteDeviceObjects() { return S_OK; }
virtual HRESULT Render() { return S_OK; }
virtual HRESULT FrameMove();
InitDeviceObjects
is called once, just after the Direct3D Device is obtained. Its counterpart, DeleteDeviceObjects
is called once, just before the Direct3D Device is released when the view is destroyed.
InvalidateDeviceObjects
is called when the device is lost or when the view is resized. RestoreDeviceObject
is called once the device has been restored, and after internal data has been recalculated due to the View being resized.
Render
is called from the CDSSD3DView8
class' OnDraw
method once OnDraw
has determined that the 3D device is stable. Render
differs slightly from the SDK version. OnDraw
brackets the call to the derived class' Render
method with Direct 3D's BeginScene
/EndScene
pair. This is followed immediately by the Present
call to present the backbuffer to the View. This relieves the derived class' Render
from worrying about these details. Rendering will be discussed in more detail later.
Finally, there's FrameMove
. This is the only method of the bunch that doesn't have a one-line implementation. FrameMove
is presented as an inline
at the end of the Header file. FrameMove
operates a bit differently than the CD3DApplication
implementation, and is also covered in more detail later.
For the most part, the application won't have to call these base class versions. The only possible exception is FrameMove
, but its implementation is so simple that it can be included in the derived class' FrameMove
methods.
According to the documentation, it's unsafe to call Windows GDI functions between the BeginScene
/EndScene
pair. To allow for GDI support, two helper virtuals have been included, both of which receive the DeviceContext passed to OnDraw
:
virtual void PreRender(CDC *) { }
virtual void PostRender(CDC *) { }
PreRender
is called by the OnDraw
method prior to clearing the 3D backbuffer and prior to the call to BeginScene
. This allows the user to perform any necessary pre-Render
tasks. An example may include establishing one or more transform matrices. Actually, although this method is listed under "GDI Support", it wouldn't be wise to perform any GDI rendering here since the client area will be occupied by the backbuffer very soon.
PostRender
is called by OnDraw
after it has called the Direct3D interface's EndScene
and Present
functions. It is here that any GDI function can be called to add any additional sprites and goodies to the scene. When PostRender
is called, the backbuffer has already been moved to the View and Direct3D no longer "owns" the client area.
All but one data member are kept private
. This ensures that a sloppy developer won't accidentally stomp on the master Direct3D Device interface or any other internal data. Access to the device and related data are through "getter" methods. The comments for these methods are included as well for basic explanation. Further details are discussed below.
public:
LPDIRECT3DDEVICE8 Get3DDevice() const { return m_p3DDevice; }
HRESULT GetDeviceState() const { return m_hDeviceState; }
const D3DXVECTOR2 & GetBackBuffer() const { return m_ptBackBuffer; }
const D3DXVECTOR2 & GetHalfBackBuffer() const { return m_ptHalfBackBuffer; }
const D3DVIEWPORT8 & GetViewport() const { return m_Viewport; }
const D3DCAPS8 & GetHALCaps() const { return m_capsHAL; }
const D3DCAPS8 & GetREFCaps() const { return m_capsREF; }
const D3DCAPS8 * GetCurrentCaps() const { return m_pCaps; }
protected:
D3DCOLOR m_cClearColor;
Get3DDevice
is how derived classes gain access to the Direct3D interface. Unlike the CD3DApplication
class, the Direct3D interface is not exposed. As mentioned above, the device pointer is kept private for protection.
Note that GetDeviceState
is included primarily for reference. The user doesn't have to take any action if the 3D Device isn't in the S_OK
state. This is handled automatically by the CDSSD3DView8
class as it periodically attempts to regain a lost 3D Device.
The GetHalfBackBuffer
function may seem odd. But I use it when converting a mouse point to a normalized (e.g., -1.0f to 1.0f) screen coordinate as the first stage of a 3D hit test.
m_cClearColor
is the only 3D-related data member that is accessible to derived classes. This establishes the background color when OnDraw
clears the backbuffer. The sample application makes use of this at the start of the program to set a dim gray background. The default m_cClearColor
is black.
Two more methods are included to assist in application debugging:
protected:
void LogDXDebug( const char * szSpec, ... );
void LogDXError( const char * szFn,
const char * szAction,
HRESULT hResult );
These functions simply log strings to an internal buffer that is then dumped to TRACE0. As a result, these messages will be disabled in Release builds. LogDXDebug
works just like a printf()
statement, allowing variable argument lists. LogDXError
logs a message with a specific format. Both of these methods are used in the base CDSSD3DView8
class as it starts up, and the output they generate are shown here:
For my own use, I have a separate debugging thread that logs debugging and error information to a set of files. But that debugger is too complex for this simple application, and I didn't want to clog up this article with too much fluff. So I opted for this simple TRACE0 approach.
To allow for proper operation, CDSSD3DView8
handles a small set of Windows messages. To ensure proper operation, it is vital that these base class versions be called if any of these methods are overridden in derived classes. The command handlers are defined here and discussed briefly below.
public:
public:
virtual void OnInitialUpdate();
protected:
virtual void OnDraw(CDC* pDC);
protected:
afx_msg void OnDestroy();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnTimer(UINT nIDEvent);
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
DECLARE_MESSAGE_MAP()
OnInitialUpdate
is where the Direct3D interface is established. This implementation calls the internal method Open3D()
to initialize the 3D windowed environment. Open3D
will be discussed in more detail later.
OnDraw
is the primary client paint entry point. This will be discussed in more detail below.
OnDestroy
is called in response to the WM_DESTROY
command received as the View is being destroyed. This method calls the internal Close3D
method, which cleans up the 3D environment. Close3D
will make a final call to InvalidateDeviceObjects
followed immediately by a call to DeleteDeviceObjects
. After that, all timers are shut down and the Direct3D interfaces are released.
OnSize
is called in response to a user resize of the view. If a Direct3D device has been established (and if the client area's size is actually changing), OnSize
will call the internal Reset3D
method to reestablish the 3D environment using the new view size.
OnTimer
is called in response to WM_TIMER
messages. The CDSSD3DView8
class manages two timers: The FrameMove
timer, which is discussed below, and a 3D Device state timer. This second timer is started when the 3D Device has been established but has been determined to be in an invalid state. This timer will fire every half-second, and call the internal TestDeviceState
method to see if the Device can be reset. This timer runs until the Device is reestablished or the View is destroyed.
OnEraseBkgnd
is a do-nothing stub, as shown in the implementation:
BOOL CDSSD3DView8::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
}
Your program won't be able to control all paths that lead to an invalidation of portions of the client area. An example would be opening a context-sensitive popup menu with a right-click of the mouse. When the menu closes, Windows sometimes invalidates your client area, which can cause flicker if the default CView::OnEraseBkgnd
is called. All of these uncontrolled client invalidation paths, however, eventually lead to OnDraw
, which calls Direct3DDevice's Clear
function to set the backbuffer to the current m_ClearColor
. So flicker is eliminated by not letting CView
handle background erasure.
Here we discuss some of the code in a bit more detail. FrameMove
is described, as is OnDraw
's relationship with Render
. Following that, some of the internal 3D control methods are examined.
FrameMove
is a bit different than the CD3DApplication
version. Since this is just a View class, it doesn't control the Run loop in the application. So FrameMove
is handled by using an internal Windows Timer.
FrameMove
doesn't fire automatically as it does in the SDK's CD3DApplication
. It has to be manually configured in derived classes via a call to:
BOOL StartFrameTimer(DWORD dwTimeoutMS);
The function accepts a millisecond count as the timeout value. It returns TRUE
if successful, FALSE
if the call to SetTimer
fails. Once this call is made, the timer will start running. As expected, FrameMove
is called by this class' OnTimer
implementation. As such, derived classes that include their own timers should make sure to call CDSSD3DView8::OnTimer
to ensure that the Frame timer runs correctly.
FrameMove
is implemented as an inline
in the header file, as follows:
inline HRESULT CDSSD3DView8::FrameMove()
{
Invalidate(FALSE);
return S_OK;
}
Note that FrameMove
invalidates the View, which will ultimately lead to a call to OnDraw
and, hence, Render
. The Invalidate
call specifies a bRepaint
flag of FALSE
. However, since the CDSSD3DView8
class stubs OnEraseBkgnd
(c.f. above), you won't see any flicker if you accidentally pass the default TRUE
as the bRepaint
flag.
The CDSSD3DView8
class also doesn't maintain any internal time counters. A derived class' FrameMove
should call:
FLOAT GetElapsedTime() const;
to get the elapsed time since the last frame. However, GetElapsedTime
simply returns the original millisecond count specified in StartFrameTimer
divided by 1000 to convert it to a floating-point value with a unit of seconds. This can lead to small timing errors if a derived class relies on precise to-the-millisecond timing.
When the application is done with the Frame timer, a simple call will disable it:
void StopFrameTimer();
This function is called during cleanup, so the derived class doesn't have to worry about this detail.
The example application makes use of FrameMove
and its supporting functions to apply a random flickering Point light to the scene. The light is located at the viewer's Eye Point.
If you'd like a more robust implementation of FrameMove
, you can try adding timing support in the main CWinApp
-derived application class' OnIdle
method.
As mentioned, there is no need for derived classes to provide their own OnDraw
implementation. The CDSSD3DView8
class provides this, calling out virtual hooks as it progresses. For ease of explaining where the various virtual hooks are called, the code for OnDraw
is presented here:
void CDSSD3DView8::OnDraw(CDC* pDC)
{
TestDeviceState();
if( m_hDeviceState != S_OK ) return;
PreRender(pDC);
m_p3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
m_cClearColor, 1.0f, 0 );
const char * szAction = "BeginScene";
HRESULT hResult = m_p3DDevice->BeginScene();
if( hResult == S_OK )
{
szAction = "Render";
hResult = Render();
m_p3DDevice->EndScene();
m_p3DDevice->Present(NULL, NULL, NULL, NULL);
}
if( hResult != S_OK ) LogDXError("OnDraw", szAction, hResult);
PostRender(pDC);
}
At the outset, OnDraw
calls the internal TestDeviceState
method. TestDeviceState
tests the current state of the 3D device and will take appropriate actions if an unexpected condition develops. The result of this test is stored in the m_hDeviceState
member which can be accessed via GetDeviceState()
. The implementation of TestDeviceState
is pretty straightforward, and can be examined in the source file DSSD3DView8.cpp.
Following TestDeviceState
comes the call to PreRender
. This method has been mentioned above and needs no further explanation.
After that, OnDraw
moves into the 3D rendering section. As mentioned above, note that OnDraw
brackets the Render
call with the BeginScene
/EndScene
pair, and follows up with the call to Present
. This differs from the Direct3D SDK's CD3DApplication
class implementation, where the user's Render
method is expected to handle all of these details. If you're uncomfortable about the way Render
is handled in CDSSD3DView8
, then feel free to remove the calls to BeginScene
, EndScene
, and Present
, and move them to your own Render
method. This sequence is included in OnDraw
for simplicity.
OnDraw
finishes up with a call to PostRender
. This has also been described earlier.
This section defines the internal methods and data used to manage the master Direct3D interface and the Direct3DDevice obtained from there. These methods and data are all private
, and are inaccessible to the application programmer. The specific declarations are in CDSSD3DView8.h, and are displayed below. Code comments have been included to provide explanation for most of this information.
First, we have the data. The comments are descriptive enough so that no further explanation should be needed.
private:
LPDIRECT3D8 m_pDirect3D;
D3DDISPLAYMODE m_d3dDisplayMode;
D3DCAPS8 m_capsHAL;
D3DCAPS8 m_capsREF;
D3DCAPS8 * m_pCaps;
D3DPRESENT_PARAMETERS m_d3dPresentParams;
LPDIRECT3DDEVICE8 m_p3DDevice;
D3DVIEWPORT8 m_Viewport;
D3DXVECTOR2 m_ptBackBuffer;
D3DXVECTOR2 m_ptHalfBackBuffer;
HRESULT m_hDeviceState;
BOOL m_bInitDevicesNeeded;
The member functions that directly manage the 3D environment are also private
and inaccessible to the application programmer. They perform all the "behind the scenes" operations to maintain the 3D environment including startup, operations, and shutdown. Since they aren't part of the formal CDSSD3DView8
API exposed to the application developer, it is up to the reader to investigate the implementation of these methods. The header file comments have been included to provide a brief explanation of these methods.
HRESULT Open3D();
HRESULT Create3DDevice(D3DDEVTYPE d3dDevType);
void Close3D();
HRESULT Reset3D();
void TestDeviceState();
UINT m_nResetTimerId;
void StartResetTimer();
inline void StopResetTimer();
These functions are all driven by various MFC events via the six ClassWizard overrides described above (OnInitialUpdate
through OnEraseBkgnd
). Thus, it is imperative that the CDSSD3DView8
version of these event handlers be called if a user's View class provides its own override of these On...
methods.
I've tested this class in a sample MDI application and have had no trouble. Using the sample code, I created an MDI application and was able to open multiple simultaneously flickering cube views within the MDI frame. Although this wasn't a full test of the CDSSD3DView8
class in an MDI environment (I was just checking for leaks and whatnot), I'd suspect that the class is quite suitable for MDI applications as well.
The bulk of the classes and files include the "DSS" prefix. This code was developed by me for my one-man company, Donut Shop Software (hence, the DSS prefix). I've removed all copyright notices in the code to release it here for this article. There are no explicit or implicit license issues involved in using this software other than those defined by this website.
Thanks to all who took the time to read this article and diddle around with the code. This is my first article, so I'm thankful that you've been able to bear with me to this point. Have fun!
Additional thanks to Obliterator for pointing out a flaw when resizing the View. This has been corrected and the downloads updated.