Hosting a C++ D3D engine in C# Winforms

Published June 17, 2008 by Ryan Villamil, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
Motivation screenshot.jpg So you have invested man-years of effort in your C++ D3D graphics/visualization/game engine. Now you want to build a nice GUI that utilizes this engine, such as a scene editor or modeler, but low and behold you are inundated by potential hosting solutions: MFC, wxWidgets, QT, Winforms, WPF. All with different challenges and subtleties. Back in the day I solved this problem with MFC. Microsoft has not yet abandoned MFC (given the recent release of MFC 9.0), but they have seemed to shift their GUI development focus to Winforms (and more recently WPF). Having spent considerable time struggling with MFC in the past, I have found Winforms' ease of use, consistency of design, and community support, more than enough reason to switch. When adapting an existing engine to be hosted in Winforms, you must forgo creating your own window, and be able to attach to any given window. You must also be able to react to changes that window might undergo (loss of focus, minimization, resize, etc). Also keep in mind that your engine does not really own the window it is given, so changing properties of the window should be discouraged, as this may break Winforms' management of that window. For this article I am going to adapt a small unmanaged C++ D3D9 engine to be hosted in a C# Winforms panel via a C++/CLI glue DLL (and I will then explain how to adapt this to a WPF app). The actual rendering engine is kept to an absolute minimum, as to not obscure the actual goal of the article. Also note that the techniques described in this article are not necessarily restricted to D3D, and can be adapted to OpenGL as well This article will be broken up into:
  • Creating a simple D3D management class
  • Hooking an HWND for messages
  • Application specific derived class
  • Exposing the derived class in a Win32 DLL
  • Creating a C++/CLI wrapper DLL
  • Utilizing the wrapper in a C# application
  • Extracting an HWND from a panel control
  • Setting up an efficient render loop
  • What about WPF???
  • Conclusion You can download the source code for this article here Creating a simple D3D management class I am not going to spend too much time explaining how to write a D3D9 rendering framework, but lets write a simple wrapper class to handle this for us. This class is very similar to the DXUT demo framework, only extremely simplified, and not able to handle full screen rendering. class CD3DManager { public: CD3DManager(); virtual ~CD3DManager(); HRESULT Initialize(HWND hWnd, BOOL bRenderOnPaint, BOOL bHookWnd); HRESULT Shutdown(); HRESULT Reset(); HRESULT ProcessFrame(); HRESULT Resize(); HRESULT HandleW32Msg(HWND hWnd, UINT message,WPARAM wparam, LPARAM lparam); protected: virtual HRESULT OnInit(); virtual HRESULT OnShutdown(); virtual HRESULT OnUpdate(double dTime, double dElapsedTime); virtual HRESULT OnRender(LPDIRECT3DDEVICE9 pd3dDevice); virtual HRESULT OnMsg(HWND hWnd, UINT message,WPARAM wparam, LPARAM lparam); virtual HRESULT OnInitDeviceObjects(LPDIRECT3DDEVICE9 pd3dDevice); virtual HRESULT OnRestoreDeviceObjects(); virtual HRESULT OnInvalidateDeviceObjects(); virtual HRESULT OnDeleteDeviceObjects(); }; Given an HWND we initialize our D3D device. When the device is lost we call Reset, when we wish to update and draw a frame we call ProcessFrame, and so on. We then add some virtual functions that a derived class will override to receive essential events it must handle.
    • OnInit: Called after the device has been initialized, a good place to load resources, and create objects
    • OnShutdown: Called right before the device is about to be destroyed. Destroy or release what you created or loaded in OnInit
    • OnUpdate: Update your simulation state
    • OnRender: Render your frame
    • OnMsg: Handle windows messages
    • OnInitDeviceObjects: Handle initialization of device objects
    • OnRestoreDeviceObjects: Handle restoration of device objects
    • OnInvalidateDeviceObjects: Handle invalidation of device objects
    • OnDestroyDeviceObjects: Handle destruction of device objects Hooking an HWND for messages One of the issues with hosting your rendering display in a C# Winforms application is gaining access to the appropriate windows events that must be handled. For example the WM_RESIZE and/or WM_EXITSIZEMOVE message(s) must be handled with care, since resizing our host HWND requires a D3D device reset. There is also the potential of many more windows message we might want to handle. One approach is to add to the appropriate event delegate handler to our Winforms panel like this: panel1.Resize += new EventHandler(myForm.Panel1_Resize); One problem with this approach is that there are many events that Winforms does not expose in this manner. Another issue is that you might have existing input code that responds to raw windows messages (like I did). In light of this I have elected to use a low-level Win32 message hooking approach. Essentially we can monitor all messages a window produces, by replacing its WndProc with one of our own, then invoking its original WndProc. Like this: BOOL CD3DManager::hookWindow( HWND hWndNew) { m_bHookedWnd = TRUE; //adds HWND to a static map associating that HWND with this instance of D3DManager addHWNDPtr(m_hDevWindow); if(m_lpfnChildWndProc = (WNDPROC)SetWindowLong( m_hDevWindow, GWL_WNDPROC, (LONG)RenderWndProc ) ) return TRUE; return FALSE; } LRESULT CALLBACK CD3DManager::RenderWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRet = 0; CD3DManager* pThis = getHWNDPtr(hwnd); if(pThis==NULL) return lRet; switch(uMsg) { case WM_EXITSIZEMOVE: pThis->Resize(); break; case WM_PAINT: if(pThis->m_bRenderOnPaint) pThis->ProcessFrame(); break; case WM_CLOSE: pThis->unhookWindow(); break; } pThis->HandleW32Msg(hwnd,uMsg,wParam,lParam); lRet = CallWindowProc(pThis->m_lpfnChildWndProc, hwnd, uMsg, wParam,lParam); return lRet; } One issue with overriding a window's WndProc is that, the function itself is a static function of type CALLBACK, with no implicit this pointer sent along with it. There is no direct way of associating the system's call to our WndProc (with a specific HWND), to a specific instance of our CD3Dmanager class. One way to accomplish this is through the use of a static map member variable that links a HWND to a CD3Dmanager*. Our WndProc is called and we use the given HWND to find the corresponding object instance. We add to this map via the supplied addHWNDPtr function and retrieve from this map via the getHWNDPtr function. Application specific code Now CD3Dmanager by itself doesn't actually do anything useful. We can derive from this class and override its virtual member functions with code specific to our application. The following code snippet defines a renderer that draws some dynamically updated text using a ID3Dfont. class CD3DTestRender : public CD3Dmanager { ???? }; HRESULT CD3DTestRender::OnUpdate(double dTime, double dElapsedTime) { m_Time = dTime; m_FPS = 1.0 / dElapsedTime; return S_OK; } HRESULT CD3DTestRender::OnRender(LPDIRECT3DDEVICE9 pd3dDevice) { HRESULT hr; // Clear the render target and the zbuffer ATTEMPT(pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff000000, 1.0f, 0) ); // Render the scene if(SUCCEEDED( pd3dDevice->BeginScene())) { RECT rc; TCHAR szInfo[256]; _stprintf(szInfo,_T("Time: %f\nFPS: %f\nRes: %d %d\nMState:0x%x\nMXY:%d %d"), m_Time,m_FPS,m_uiWidth,m_uiHeight,m_dwMouseButtonStates,m_iMouseX,m_iMouseY); SetRect(&rc, 10, 10, 0, 0 ); m_pFont->DrawText( NULL, szInfo, -1, &rc, DT_NOCLIP, D3DXCOLOR( 1.0f, 0.0f, 0.0f, 1.0f )); ATTEMPT(pd3dDevice->EndScene()); ATTEMPT(pd3dDevice->Present( NULL, NULL, NULL, NULL )); } Sleep(10); return S_OK; } HRESULT CD3DTestRender::OnInitDeviceObjects(LPDIRECT3DDEVICE9 pd3dDevice) { m_pd3dDevice = pd3dDevice; return D3DXCreateFont(m_pd3dDevice, // D3D device 12, // Height 0, // Width FW_REGULAR, // Weight 1, // MipLevels, 0 = autogen mipmaps FALSE, // Italic DEFAULT_CHARSET, // CharSet OUT_DEFAULT_PRECIS, // OutputPrecision DEFAULT_QUALITY, // Quality DEFAULT_PITCH | FF_DONTCARE, // PitchAndFamily L"Courier", // pFaceName &m_pFont); // ppFont } HRESULT CD3DTestRender::OnRestoreDeviceObjects() { return m_pFont->OnResetDevice(); } HRESULT CD3DTestRender::OnInvalidateDeviceObjects() { return m_pFont->OnLostDevice(); } HRESULT CD3DTestRender::OnDeleteDeviceObjects() { SAFE_RELEASE(m_pFont); return S_OK; } Why expose the derived class in a Win32 DLL? Now comes the task of exposing our derived class in a Win32 DLL. Why can't we just stick all of this code into our C++/CLI DLL? Well we can, C++/CLI is real C++ code, just with some additions and a few not so obvious restrictions (like no inline assembly, no variable argument lists,..). When importing your derived class code into a C++/CLI project, be aware that your class will be compiled as managed code, and there might be performance penalties (as for how much, that is up for debate). At some point I attempted to import a large C++ code base into a C++/CLI project, and came across a number of problems with the acceptance of my own code and the code of third parties as valid C++/CLI code. I am sure there are ways around these problems, but for this article I elected to wrap my C++ code into an unmanaged Win32 DLL, for use by the C++/CLI DLL. This Win32 DLL will now run in an unmanaged environment and not suffer any performance penalties. Given the simplicity of the given CD3Dmanager and CTestRenderer classes, they could have been placed directly in the C++/CLI project, but for illustrative purposes I am using this Win32 DLL. To accomplish this, create a new Win32 DLL project in Visual Studio, and add the code for our CD3Dmanager and CTestRenderer classes. To expose our classes and their public member functions from the DLL, we can prepend this define to our class declarations: #ifdef _EXPORTING #define CLASS_DECLSPEC __declspec(dllexport) #else #define CLASS_DECLSPEC __declspec(dllimport) #endif Like so: class CLASS_DECLSPEC CD3Dmanager ???? class CLASS_DECLSPEC CD3DTestRender : public CD3Dmanager ???? Where _EXPORTING is defined when building the DLL, and not defined when using the DLL. Creating a C++/CLI wrapper DLL Now how do we expose our fancy C++ engine DLL to our equally fancy C# Winforms application? Well there are many ways: pInvoke, SWIG, COM, etc. But the way I prefer is a C++/CLI DLL. With C++/CLI exposing a class for usage in a C# application is as simple as declaring it as a ref class, like so: class CD3DTestRender; public ref class D3DWrap { public: D3DWrap(); ~D3DWrap(); HRESULT Initialize(IntPtr hWnd); HRESULT Shutdown(); HRESULT ProcessFrame(); HRESULT Resize(); protected: CD3DTestRender* m_pRenderer; }; As long as the public member functions of D3DWrap take valid .NET types everything is as right as rain. Your private and protected members can do anything their C++ hearts desire. Also make sure not to expose any header files that C# might take offense to. I forward declare my CD3DTestRenderer class and keep a protected pointer to an instance of it. The associated .cpp file is where I actually include D3DTestRenderer.h. Then we import the D3DEngine.dll for use by linking with the stub .lib file generated when we built it. The D3Dwrap implementation merely creates and destroys an instance of CD3DTestRenderer and forwards the function calls, but we can do much more if we like. This is also the class where we would add functions that our Winforms code would call to manipulate our internal C++ engine state. Utilizing the wrapper in a C# application Ok now that we have our C++/CLI DLL (D3DWrap.dll), using it in our C# application is trivial. Simply right click on the Reference subfolder of the C# project (under the Solution Explorer view of Visual Studio) and add a reference to our new DLL. We can then allocate an instance of D3DWrap in our form (or anywhere we like) like so: m_D3DWrap = new D3DWrap(); Extracting an HWND from a panel control To initialize our engine we have to supply it with an HWND. The panel we wish to draw to is nothing more than a high-level wrapper on a Win32 window, of which we can attain a HWND. To extract a HWND from a Winforms panel and pass it to our initialization, we do this: m_D3DWrap.Initialize(panel1.Handle); //in our C# m_pRenderer->Initialize((HWND)(hWnd.ToPointer()),TRUE,TRUE); //in our C++/CLI Setting up an efficient render loop Setting up an efficient render loop in a C# Winforms application is actually non-trivial, and has been hotly debated for some time. The technique I am using was developed by Tom Miller and presented in his blog. MFC used to have a virtual member function you could override called OnIdle that would be called continuously in a loop when there were no windows messages to process. This was the ideal place for the update and draw functions of a rendering framework to be called. Winforms, on the other hand, does not supply such a function. Instead we only have the Idle event that is pulsed when our application goes into and out of an idle state. So how do we draw on the equivalent of MFC's OnIdle? The basic gist of the technique is to loop (in the handler for the Idle event) on the condition that there are no waiting messages to be processed, by using a low-level PeekMessage to test that criteria. It's actually quite clever, and you can refer the link provided for a more in depth explanation. There are also many situations where you would not wish to draw on idle, and would rather just draw when the user manipulates the scene (such as in a modeler). Instead of placing your call to ProcessFrame in the idle loop we could just as well place it in the response to a WM_PAINT message. What about WPF??? WPF (Windows Presentation Foundation) is Microsoft's new(ish) wiz bang GUI platform, simultaneously providing a potential replacement for Winforms, GDI+, Adobe Flash (Silverlight), and much more. With the release of Visual Studio 2008, a fully integrated WPF forms editor was finally introduced, making it a viable choice for GUI development. Some might argue that it was viable before the release VS 2008, but without that beautiful forms editor, I would beg to differ. Much deserved attention has been lavished on WPF recently, but in my opinion it is still incomplete and immature in comparison to Winforms (which Microsoft plans to support and actively develop for some time). However, many people are indeed migrating to WPF, so for them has this article been in vain? Nope!! The funny thing about WPF is that unlike previous incarnations of Microsoft GUI APIs, controls are no longer actually windows! Luckily, Microsoft has left in a backdoor for those of us that need a HWND to get anything useful done. HwndHost is a WPF control that actually exposes an HWND. An HwndHost (or its derived class WindowsFormsHost) can be placed into a form, from which we can attain our much needed HWND, and everything should just work as it did before. Conclusion With a little effort, drawing your D3D scene into a control of a .NET application can be quite straightforward. Adding the advanced GUI capabilities of Winforms or WPF to your rendering application can be enormously beneficial, be it a game engine, a scientific visualization, or even a simple graphics technique demo (with all their wonderful adjustable parameters).
Cancel Save
0 Likes 1 Comments

Comments

sunny1390

192168010 192168010019216801254 19216801adminwireless 19216801netgear 19216801setup 1921680254 192168101 192168o100 192168o11admin192168o11tp-link 16819211 1921681100 19216812-t 19216813 192168-21 192168l1 192168l2 192168o1admin192168o1adminpassword 192168o1login 192168o1tp-link 19816801 19816811 router settings best convert 129168o1 16819211 192-168-0-1 192168-01 19216802 19216810 192-168-1-1 192168-11 19216812 19216813 19216821 19816801 19816811 192168010192168011 192168101 192168110 192168111 192168-11-1 1921680100 1921680254 1921681100 19216801254 19216801admin19216801adminnetgear 19216801adminwireLesssettings 19216801setup 19216801-t 19216811admin19216811adminwireLesssecurity 19216811adminwireLesssettings 19216811setup 19216811-t 19216812-t 192168L1 192168L2192168LL 192168LLadmin 192168LLLogin 192168LLstc 192168LLtp-Link 192168o1 192168o11 192168o11admin 192168o11tp-Link192168o1admin 192168o1adminpassword 192168o1Login 192168o1tp-Link 192186o1 http19216811 www19216801 www19216811www-19216811 www192168LL 192168-21 ip-19216811 129168o1 16819211 192-168-0-1 192168-01 19216802 19216810 192-168-1-1 192168-11 19216812 19216813 19216821 19816801 19816811 192168010 192168011 192168101 192168110 192168111 192168-11-1 1921680100 1921680254 1921681100 19216801254 19216801admin 19216801adminnetgear 19216801adminwireLesssettings19216801setup 19216801-t 19216811admin 19216811adminwireLesssecurity 19216811adminwireLesssettings 19216811setup19216811-t 19216812-t 192168L1 192168L2 192168LL 192168LLadmin 192168LLLogin 192168LLstc 192168LLtp-Link 192168o1192168o11 192168o11admin 192168o11tp-Link 192168o1admin 192168o1adminpassword 192168o1Login 192168o1tp-Link192186o1 http19216811 www19216801 www19216811 www-19216811 www192168LL 192168-21 ip19216811 1921680101921680100 19216801254 19216801adminwireless 19216801netgear 19216801setup 1921680254 192168101 192168o100192168o11admin 192168o11tp-link 16819211 1921681100 19216812-t 19216813 192168-21 192168l1 192168l2 192168o1admin192168o1adminpassword 192168o1login 192168o1tp-link 19816801 19816811 router settings best convert 129168o1 16819211 192-168-0-1 192168-01 19216802 19216810 192-168-1-1 192168-11 19216812 19216813 19216821 19816801 19816811 192168010192168011 192168101 192168110 192168111 192168-11-1 1921680100 1921680254 1921681100 19216801254 19216801admin19216801adminnetgear 19216801adminwireLesssettings 19216801setup 19216801-t 19216811admin19216811adminwireLesssecurity 19216811adminwireLesssettings 19216811setup 19216811-t 19216812-t 192168L1 192168L2192168LL 192168LLadmin 192168LLLogin 192168LLstc 192168LLtp-Link 192168o1 192168o11 192168o11admin 192168o11tp-Link192168o1admin 192168o1adminpassword 192168o1Login 192168o1tp-Link 192186o1 http19216811 www19216801 www19216811www-19216811 www192168LL 192168-21 ip-19216811 129168o1 16819211 192-168-0-1 192168-01 19216802 19216810 192-168-1-1 192168-11 19216812 19216813 19216821 19816801 19816811 192168010 192168011 192168101 192168110 192168111 192168-11-1 1921680100 1921680254 1921681100 19216801254 19216801admin 19216801adminnetgear 19216801adminwireLesssettings19216801setup 19216801-t 19216811admin 19216811adminwireLesssecurity 19216811adminwireLesssettings 19216811setup19216811-t 19216812-t 192168L1 192168L2 192168LL 192168LLadmin 192168LLLogin 192168LLstc 192168LLtp-Link 192168o1192168o11 192168o11admin 192168o11tp-Link 192168o1admin 192168o1adminpassword 192168o1Login 192168o1tp-Link192186o1 http19216811 www19216801 www19216811 www-19216811 www192168LL 192168-21 ip19216811

July 30, 2016 03:04 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement