DERAILED — Platform independent dynamic linking
Posted in Uncategorized on August 9th, 2009 by Chaos EngineerOoops, I definitely got sidetracked more mucking around with my platform independent rendering engine. Instead of just holding out on the NDS wifi article in silence for any longer, I thought I would chime in and let you know I haven't forgot about you and show you what I've been up to.
My platform independent rendering engine is based on a pure virtual renderer base class that is filled with an instance of a derived class by a dynamically linked code module. Along with the pure virtual renderer class, there are a few pure virtual resource classes as well, the actual instantiation of which are performed by the renderer class. Trying to maintain platform independence, the singleton (factory / manager) class that returns this renderer instance has some interesting code I thought I would share.
It is possible to have a project directory (even though a bit messy) that will compile in both Microsoft Visual Studio and in GCC without having to change a lick of code. Thats child's play for a simple project, but its still possible even for a project that contains a main executable and several other dynamic libraries.
So first off, since we are dynamically linking, we are definitely going to have function pointers returned by dlsym(...) or GetProcAddress(...). The actual DLL/SO modules export only three functions--one to create the instance, one to destroy it, and one to return the version. So we have the typedefs for these as so:
typedef int (*fpCreateRendererInterface)(CRenderer **pInterface); typedef int (*fpDestroyRendererInterface)(CRenderer **pInterface); typedef int (*fpQueryRendererVersion)(void);
Second off, this is platform independent, so obviously we will need to be using some fancy pre-processor conditionals (PPCs) to detect the platform / compiler. Now in certain instances we only want to know what the compiler is, because the actual platform doesn't matter, but in other cases we want to know the actual platform as well because the same compiler is used on multiple platforms (GCC is used in Linux and also Mac). First for the compiler, we can detect Visual C++ by checking to see if _MSC_VER is defined, and we can detect GCC by checking to see if __GNUC__ is defined. For the platform, we can detect Windows by checking if WIN32 is defined (yes, even 64-bit Windows defines this), we can detect Linux by checking if __linux__ is defined, and we can detect OSX by checking if __MACH__ and __APPLE__ are defined.
We can then use these PPCs to create code that is savagely flexible. In this singleton class, I have a static void pointer to the library (Windows' HMODULE is just a void*, don't let them trick you) called m_hLibrary. Lets see how we could dynamically link either a .so file in Linux or a .dll file in Windows and place the returned handle in the same variable... here is the code for the "SGL" (SDL GL) renderer type which can be used in either Linux or Windows, taking into account if the code is in release or debug mode:
case rstSGL: Log::Entry(-1,__FILE__,__LINE__,"Creating SGL rendering interface..."); #if defined(__GNUC__) #if defined(NDEBUG) Log::Entry(-1,__FILE__,__LINE__,"__GNUC__ defined... opening release library"); m_hLibrary = dlopen("./libRendererSGL.so",RTLD_NOW); #else Log::Entry(-1,__FILE__,__LINE__,"__GNUC__ defined... opening debug library"); m_hLibrary = dlopen("../RendererSGL/bin/Debug/libRendererSGL.so",RTLD_NOW); #endif if(!m_hLibrary) Log::Entry(2,__FILE__,__LINE__,"Error loading shared library. dlerror() = %s",dlerror()); #elif defined(_MSC_VER) #if defined(NDEBUG) m_hLibrary = LoadLibraryExA("RendererSGL.dll",NULL,NULL); #else m_hLibrary = LoadLibraryExA("../RendererSGL/bin/Debug/RendererSGL.dll",NULL,NULL); #endif #endif break;
Please note that NDEBUG is not defined by GCC even when using -O2, so you need to set your release target to define this manually. I find it strange because it is supposedly a standard...
Also note that when linking using dlopen(...), we specify RTLD_NOW to force resolution of all symbols in the dynamic library. If we used RTLD_LAZY, we could exclude a lot of code from our library, and let it resolve symbols later (i.e. from the main executable). Unfortunately this works great on Linux but seemingly has no analogy in Windows, and in order to keep the projects symmetrical across platforms, we use RTLD_NOW to behave appropriately.
So lets look at whats going on here... its a bit over complicated, but I thought I would provide some extra good ideas for you guys. If using GCC, we use dlopen(...) to get the handle to the .so file, and if using VC++, we use LoadLibraryExA(...) to get the handle to the .dll file. If NDEBUG is defined, we link to the release version of the dynamic library (actually look in the current directory or system path as if it were a proper deployment), otherwise we link to the debug version of the library to simplify development.
Oh snap! So we now have linked to a dynamic library regardless of what platform we are running on! This is kickass, so where to now? We need to get pointers to the functions in the library we are going to use. The function that does this in Linux is dlsym(...), and in windows is GetProcAddress(...). Here is what this would look like in a platform independent form:
#if defined(__GNUC__) fpCreateRendererInterface IntCreate=(fpCreateRendererInterface)dlsym(m_hLibrary,"CreateInterface"); #elif defined(_MSC_VER) fpCreateRendererInterface IntCreate=(fpCreateRendererInterface)GetProcAddress((HMODULE)m_hLibrary,"CreateInterface"); #endif
Damn, its that easy? Indeed it is. We now have a function pointer to a procedure in a dynamically linked library regardless of if it came from a .so file in Linux or a .dll file in Windows. Obviously once armed with the function pointer, the remaining code is the same regardless of platform. We just use IntCreate(...) like it were a normal function. In this case we pass a pointer to a pointer to the pure virtual renderer base class, and inside the dynamic library, we assign the pointer to an instance of a derived class:
extern "C" { int CreateInterface(CRenderer **pInterface) { if(*pInterface) return -1; *pInterface = new CRendererSGL(); return 0; } ...
This is really quite powerful, and since the only actual call to an exported function is when creating the derived class instance, the performance hit while using the derived class is only from the vftable. More importantly this lets us do something ultra simple in our main code to get a reference to an abstract renderer interface that doesn't require knowledge of the nitty-gritty of either D3D or OpenGL:
Log::Entry(0,__FILE__,__LINE__,"Creating rendering device..."); Graphics::CreateInterface(); CRenderer *renderer = Graphics::GetInterface(); renderer->Initialize(0); if(g_bSafeDevice) renderer->CreateDeviceSafe(); else renderer->CreateDevice(800, 600, 32, g_bWindowed);
Yeah, I know. That code is delicious. Its even more tasty when you have a CMesh that contains instances of pure virtual CResourceVtxBuff and CResourceIdxBuff created by the derived CRenderer class in the dynamic library, and CModel that contains a number of CMesh instances as well as a number of CMaterial instances that contain instances of pure virtual CResourceTexture also created by the derived CRenderer class in the dynamic library. So to create a model and render it, you would have to do something like the following:
// example of loading a mesh mshOut = new CMesh(); iStride=mshOut->GetVtxStride(); mshOut->SetVtxCount(iVertexCount); mshOut->SetFaceCount(iFaceCount); m_pRenderer->CreateMeshBuffers(iStride*iVertexCount,sizeof(int)*3*iFaceCount,mshOut); // if pRenderer is D3D, this lock would be like IDirect3DVertexBuffer9->Lock(...), //while in OpenGL it would be like glMapBufferARB(...). Transparent at this point. VtxData=(unsigned char*)mshOut->GetVtxPtr()->LockWrite(0,0); // Write vertex buffer to VtxData here... mshOut->GetVtxPtr()->Unlock(); IdxData=(unsigned char*)mshOut->GetIdxPtr()->LockWrite(0,0); // write index buffer to IdxData here.. mshOut->GetIdxPtr()->Unlock(); // example of loading a material mtlOut=new CMaterial(); mtlOut->SetDiffuse(FloatToLongColor(v3fDiffuse.x, v3fDiffuse.y, v3fDiffuse.z)); mtlOut->SetAmbient(FloatToLongColor(v3fAmbient.x, v3fAmbient.y, v3fAmbient.z)); mtlOut->SetSpecular(FloatToLongColor(v3fSpecular.x, v3fSpecular.y, v3fSpecular.z)); mtlOut->SetMaterialName(sMaterialName); mtlOut->SetTexture(m_pRenderer->CreateTexture(&imgTexture)); // and you add them to a model which contains std::vectors of meshes and materials... iMaterialIdxList[j] = mdlOut->AddMaterial(mtlOut); //... mdlOut->AddMesh(mshOut,iMaterialIdxList[iMaterialRef]); // and eventually render! iMeshCount = mdlCurr->GetMeshCount(); for(j=0;j<iMeshCount;j++) { pRenderer->SetWorld(&matWorld); k = mdlCurr->GetMeshMaterialIdx(j); pRenderer->SetTexture(mdlCurr->GetMaterial(k)->GetTexture()); pRenderer->Render(mdlCurr->GetMesh(j)); }
Anyway, I guess this isn't all that useful, and maybe I'm just showing off at this point. It does exhibit some good ideas, and show how glorious abstraction can be though. Regardless, I got distracted again. The NDS Wifi example continues. I will post in the next few days to describe the protocol used and give a little primer on sockets. Keep your eyes peeled.
We want to rotate the cube along an axis perpendicular to the mouse motion vector. This will give the impression that the cube is spinning as a direct result of the motion, as if you had quickly ran your hand along it. So we first need to construct a vector that is perpendicular to the motion, and lies in the plane of the screen. Lets assume we have a default OpenGL view setup, where we are looking along the negative z-axis, and the screen lies in the xy-plane. Thus, we want a vector that is perpendicular to the mouse motion and lies in the xy-plane.









