CONCEPT
Quite some years ago I was reading a very good book on game engine programming. It’s called “3D Game Engine Programming” by Stefan Zerbst. A rather large book and very thorough in its descriptions of concepts and code. I didn’t follow everything Stefan wrote at the time, but I certainly absorbed some of his ideas and methods. One thing which I feel I did grasp was the need to abstract anything specific to rendering away from the code that actually ran the game or demo. For the most part (if not entirely) the game code will be keeping track of objects, their positions and orientations, also NPC’s and their reactions, inventory, and so on. This is rather different from the code that’s performing the rendering. Given that game code is always the same regardless of which graphics API we’re using there is no good reason for it to be mixed up with the rendering code.
That’s where the interface concept comes in. I’m unsure if this particular method is still in common use today, it does however work well enough and that’s what we’ll be using. It uses a class that has pure virtual functions declared within it. If I remember correctly a ‘pure’ virtual function requires that the class must be inherited from by another class and the functions over-written. The ‘base’ class is the one we will be using for the interface. The class that derives from it, is what we will be using to actually access the DirectX functions we use.
This chapter simply takes the code from the previous one and applies the concept of interfaces to it.
Note that we will also be containing the derived class inside a DLL file. This comes from Stefan Zerbst’s idea I read in his book I mentioned earlier. Putting the rendering code inside a DLL file means that once the rendering codes become complex, all one has to do to make changes to it, is to re-compile the DLL. Likewise for the game engine code. Otherwise, any even small changes in either, would mean we would have to recompile both in their entirety.
A DLL file is also handy as it sort of plugs in and plugs out of a .exe file. So if we wanted to try something different in the rendering code we could just make a new DLL with the same name, and provided we replace the original in the project folder the code will run the same (without noticing they’ve been swapped).
THE INTERFACE CLASS
This is the interface class we’ll be using:
class D3DInterface
{
public:
// Release COM stuff (if needed - smart pointers may negate this)
virtual void ReleaseCOM() = 0;
virtual WNDPROC GetWndProcAddress() = 0;
// Direct X specific functions
virtual bool InitDirectX() = 0;
virtual WCHAR* LogAdapter() = 0;
virtual void LogResolution() = 0;
};
If you followed the last chapter some of the functions in this class will be familiar. Note the “= 0;” part at the end of function declarations. This makes the functions ‘pure’ virtual functions rather than just virtual functions. This makes a demand they must be overridden in a derived class, and essentially forces this class to never really exist itself, only as a blueprint for another.
Some considerations to point out before going any further are that I wanted the window message handling function and dialog box message handling function to be within the DLL file (as part of the derived/inherited class). Had I not chosen to do this then it means for example in the dialog box message handling function, there would have been calls over and over to the functions that set the data in the combo boxes and text fields of the dialog box, within the main program. That wouldn’t really have looked very good.
I didn’t want this. I wanted as much as possible of DirectX or anything related to it hidden away from the main program. Thus I decided to write the window and dialog box message handling functions inside the derived class and thus, inside the DLL file. I’ll discuss these functions later, but I wanted to point it out now in case anyone downloads the code and then finds it strange they can’t locate message handling functions in the main project file.
Some other things to consider in the base class header file:
#pragma once
#include "framework.h"
#if defined( BUILD_DLL )
#define IMPORT_EXPORT __declspec(dllexport)
#else
#define IMPORT_EXPORT __declspec(dllimport)
#endif
typedef LRESULT (*WNDPROC) (HWND, UINT, WPARAM, LPARAM);
class IMPORT_EXPORT D3DInterface
{
public:
// Release COM stuff (if needed - smart pointers may negate this)
virtual void ReleaseCOM() = 0;
virtual WNDPROC GetWndProcAddress() = 0;
// Direct X specific functions
virtual bool InitDirectX() = 0;
virtual WCHAR* LogAdapter() = 0;
virtual void LogResolution() = 0;
};
extern "C" /*Important for avoiding Name decoration*/
{
IMPORT_EXPORT D3DInterface* _cdecl CreateRenderDevice();
};
typedef D3DInterface* (*CREATE_RENDER_DEVICE) ();
That is actually the complete header file for the base class. Note the #define macro. If BUILD_DLL is defined it sets the syntax for an DLL export, otherwise for a DLL import. In the main program, it’s not defined, but in the DLL project file, it is. This simply means when compiling the DLL it’s all set to export and when compiling the main program it’s all set to import. I’m not actually sure how much difference the macro makes to the class definition as I tried compiling without it, and it still seemed to work to me.
It does make a difference for the function CreateRenderDevice() however, as otherwise we cannot export this function out of the DLL file. Bear in mind this header file is shared between both the main program and the DLL file. When compiling the DLL file the block:
extern "C" /*Important for avoiding Name decoration*/
{
IMPORT_EXPORT D3DInterface* _cdecl CreateRenderDevice();
};
Uses the extern “C” instruction to make sure the name of the function is exported plainly as is. Without the use of extern “C” you can get name mangling. You can learn more about this online. In plain terms, it means by the time the code has been compiled and made into a DLL file the function exported would not actually be named CreateRenderDevice(). It would be some bizarre mixture of this name interspersed with all sorts of symbols. Ridiculous as that might look like to the human eye that would however now be its real name and attempting to access the function outside of the DLL with the name CreateRenderDevice() would fail because in the export its name was no longer CreateRenderDevice() anymore. The extern “C” part stops this happening, and means we can get access to this function outside the DLL using its original name: CreateRenderDevice().
We’ll cover more about what this function does later but for now all you need to know is this function lives inside the DLL file and is exported out of it, which means we can call it outside the DLL file. When we do call it, it creates a class that is derived from the base class shown above and returns a pointer to it which is now accessible in the main program. That’s how we can acquire a pointer to a derived class defined in the DLL file from inside the main program.
The last two points of interest are the typedefs we see in the header file. There are two of them.
typedef LRESULT (*WNDPROC) (HWND, UINT, WPARAM, LPARAM);
....and
typedef D3DInterface* (*CREATE_RENDER_DEVICE) ();
They are both essentially aliases for functions. The type WNDPROC is a function type that takes the four arguments shown and returns an LRESULT. This could well feel rather familiar to some readers. It’s essentially an alias for a window message handling function. The reason it’s there is that I decided to leave window creation and display stuff inside the main program. There was just no good reason to go through the complexities of putting window stuff inside the DLL as well. What that does mean however is when the window class is registered inside the main program it will want a function name for the function that handles the window messages (the one it wants is inside the derived class in the DLL).
In the DLL there’s a class member function that returns the address of the window message handling function contained inside the derived class.
WNDPROC D3DRenderer::GetWndProcAddress()
{
return D3DRenderer::WndProc;
}
Whilst we haven’t come to the discussion of the derived class yet, a brief mention of this function is helpful here. The typedef WNDPROC (which is shared by both the main program and the DLL file) allows this function to return the class member function D3DRenderer::WndProc() as a return that can be used by something else. In our case it’s required by our main program when wanting the name of a function that handles the window messages. Using a typedef like this and writing a function that returns another function as a return type, makes retrieving the window message handling function defined inside the DLL easy. You can see a sneak peek below from the main program acquiring this function when creating the window class:
wcex.lpfnWndProc = pD3DInterface->GetWndProcAddress();
Notice it uses the pointer to the base class to ultimately call the over-ridden function GetWndProcAddress() which returns the function buried inside the DLL.
I honestly do not know of any other way to do this other than using a typedef to make a variable of type equivalent to a specific function call (what it returns and the arguments it receives). Given that this works however, I don’t need to be aware of any other way. This is what the function typedef WNDPROC is for, it makes all this possible and easy.
THE DLL FILE
We’ll now take a quick look inside the DLL file and see what’s going on in there. Essentially it’s just the previous chapter’s code arranged within a DLL file. Whereas in the previous chapter, the functions that loaded data into the text fields and combo box were in the main program .cpp file and the DirectX related functions were in the class, now everything is in the one class in the DLL file. I wanted it this way so all things concerning the dialog box were contained in the DLL file class.
class D3DRenderer : public D3DInterface
{
public:
D3DRenderer();
~D3DRenderer();
static LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
static INT_PTR CALLBACK ChooseRender(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
void ReleaseCOM();
WNDPROC GetWndProcAddress();
bool InitDirectX();
WCHAR* LogAdapter();
void LogResolution();
void SetAdapterBufferField(WCHAR* text, HWND hDlg);
void SetResolution(HWND hDlg);
void SetRefresh(HWND hDlg);
void TestMessage();
D3DRenderer* mD3DRenderer;
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
std::vector<UINT> mResolutionPair;
struct resData { UINT w = 0; UINT h = 0; double Hz = 0; };
std::vector<D3DRenderer::resData> mComboResData;
WCHAR mAdapterName[128];
bool mSetRefresh = false;
double mRefreshRate = 0;
};
As you can see it’s very similar to the class in the previous chapter. There are only a few differences. You can see I made the two message-handling functions static. I did this to make it easier to acquire them when creating the main window and when calling the dialog box. I didn’t want to make every function static though. As far as I can tell making every function static is rather similar to over-using global variables. You may be wondering then, how do we get access to a specific object or instance’s member function for say the function SetAdapterBufferField()?
Well, this is where some trickery is required. Briefly harking back to the main program when we created the window we made it using a WNDCLASSEXW type. This allows us to add one of our own variables to the call that creates the window. It may even work with a standard WNDCLASS type but the tutorial I read from Microsoft suggests using the WNDCLASSEXW type so I won’t be arguing with them.
Before we can discuss this further we need to briefly mention how we get a pointer to our derived (or inherited) class in the DLL from the main program. In the DLL file there’s the function to be exported mentioned earlier:
D3DInterface* CreateRenderDevice()
{
gpD3DRenderer = new D3DRenderer();
gpD3DRenderer->mD3DRenderer = gpD3DRenderer;
return gpD3DRenderer;
}
And yes it does make use of one global variable. I was happy to allow one global variable to exist in the DLL and store a copy of a pointer to itself inside the class. I think one global variable is probably ok! Note that it creates an instance of the derived class D3DRenderer, but returns a pointer of type D3DInterface. In C++ you can return a pointer of a derived class as a type of its base class. That’s a fundamental part of how C++ allows for abstraction using this method. The main program will have a pointer to a class it already knows about (D3DInterface) but it actually points to a class of the derived type (D3DRenderer). The main program doesn’t know any different and is happy to proceed.
So returning to the part where we create the window in the main program. We have this:
HWND hWnd = CreateWindowW(szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInst,
pD3DInterface);
Note the last argument to the function. It’s a type of our own making. Windows allows for this with the WNDCLASSEXW function (I assume there’s probably a WNDCLASSEXA function as well if not using wide character strings). In our main program, I made a global (oh no!) pointer variable of type D3DInterface*. When the DLL file is loaded and the exported function creates a derived class and returns a pointer to it, it gets loaded into this variable in the main program. We can then pass this into the function shown above as the last argument.
You may be wondering what the point of any of this is. I frequently wondered this myself many times when trying to get the program to work. What this does is make a pointer of this user-defined type available to the window message handling function specified when creating the window class. In our case, that’s contained inside the DLL file. Note below a small part at the start of the function D3DRenderer::WndProc().
static HINSTANCE hInstance;
static D3DRenderer* pD3DRenderer;
switch (message)
{
case WM_CREATE:
{
CREATESTRUCT* pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
pD3DRenderer = reinterpret_cast<D3DRenderer*>(pCreate->lpCreateParams);
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pD3DRenderer);
hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
return 0;
}
....continues
Note the two static variables at the top. One is the HINSTANCE which we need to launch the dialog box, and the other is a pointer to our user-defined class D3DRenderer. Following Microsoft’s instructions the 3 lines just underneath the curly bracket after the WM_CREATE message set some sort of intrinsic pointer in Windows which can be recovered later.
The hInstance line does the same thing just with less code, which I learned from a book on Windows (Charles Petzold’s book mentioned in the previous chapter). I wonder if the first 3 lines could be condensed in the same way, but for now I’m going to stick with what Microsoft tell me to do!
What this means is that when we launch the dialog box and thus its associated message-handling function, we can retrieve this pointer. We could have just used the global pointer in the DLL file for the same purposes to be honest, but I felt I wanted to do a little bit more than that and see if I could find another way. Anyway, have a look at a small part of the dialog box message handling function:
LONG_PTR ptr = GetWindowLongPtr(GetParent(hDlg), GWLP_USERDATA);
static D3DRenderer* pD3DRenderer = reinterpret_cast<D3DRenderer*>(ptr);
This is where the pointer to our class is retrieved. The GetParent() call presumably references the window message handler and finds the pointer we made there. Note this function also takes a GWLP_USERDATA flag too, for it to work properly. What this all means is that we now have successfully passed a pointer to our derived class from the main program, down into the window message handler in the DLL, and then further retrieved the same pointer in the dialog box message handler. Neat huh?
And from there inside the dialog box message handler, we can call all our derived class member functions without having to declare them as static, and without using global variables (although we did need a couple of global variables to start things off). Here’s a short example:
case WM_INITDIALOG:
{
// fills out text fields for adapter
pD3DRenderer->SetAdapterBufferField(text, hDlg);
// fills class vector with display data
pD3DRenderer->LogResolution();
// clear combo box
SendMessageW(GetDlgItem(hDlg, IDC_COMBO1), CB_RESETCONTENT, 0, 0);
// loads class data into combo box fields and strings for selection
pD3DRenderer->SetResolution(hDlg);
// sets starting selection in combo box
SendMessageW(GetDlgItem(hDlg, IDC_COMBO1), CB_SETCURSEL, (WPARAM)0, (LPARAM)0);
// shows Refresh Rate
pD3DRenderer->SetRefresh(hDlg);
return TRUE;
}
And there is our retrieved pointer being used to call class member functions. The pointer that began life all the way up in the main program when we first loaded the DLL:
HMODULE hDll = LoadLibrary(L"Render Device.dll");
CREATE_RENDER_DEVICE _createRenderDevice = (CREATE_RENDER_DEVICE)GetProcAddress(hDll,
CreateRenderDevice");
pD3DInterface = _createRenderDevice();
And for the most part that concludes this chapter. The program runs exactly the same as it did in the previous chapter, only this time all the DirectX-specific stuff is contained nicely inside a DLL file loaded at run time. There is no API-specific stuff at all in the main program.
CONCLUSION
To conclude this chapter I’ll give a quick run down as to what we did. We moved all our API (DirectX) specific stuff into a class inside a DLL file. We created a base class with pure virtual functions in it that we created the derived class in the DLL file from. We loaded the DLL and used a function exported from it to create an instance of our derived class and obtained a pointer to it. We then added this pointer as an argument to the function that creates the window in the main program. This pointer was then acquired in the window message handler inside the DLL file. Finally, the dialog box message handler retrieved this pointer and used to it call the member functions of a specific object of our derived class type. The program ran as it did before but with the addition of abstraction principles made possible by the interface system. And that’s the end of this chapter, thank you for reading.
As always if you’d like to get in touch (for whatever reason) email me at:
chris.h.kp.2022@gmail.com
The link to the code:
