Welcome back to the second part of the Windows to Linux porting blog series. Today, we’re going to look at how to port the graphics part which we omitted last time, and we’re also going to actually use Linux this time!
Here comes the ugly part of the porting story: There is no Direct3D on Linux. Ideally, if your application can work just as well using OpenGL, you can ditch Direct3D completely, otherwise — and that’s what I use in my framework — you’ll have to port your graphics system to OpenGL while keeping the Direct3D part intact. The way I solved this is by providing an abstract interface and two implementations for it, one using OpenGL, the other using Direct3D. The APIs are actually close enough that this is feasible. Clients then choose at run-time which one to use, and the implementations are fully self-contained in a DLL/shared object. Unfortunately, OpenGL and Direct3D don’t use the same shading language, so I had to rewrite all shaders in GLSL again. This is not that hard (the languages are quite similar), but it’s still quite a bit of typing. Before you ask, no, I’m not using the Direct3D effects framework, so I just had to rewrite the actual shader source.
For compute on graphics, I’m using OpenCL, so there’s nothing to do, porting wise. It works exactly the same with Direct3D as with OpenGL. The only minor nuisance is that you need to implement the OpenGL interop twice, once for platforms supporting OpenCL 1.1 and once for 1.2, as they use a different function for texture sharing, but that’s it.
For the actual work, I would suggest to start with a cross-platform library to create the OpenGL window first, and then start to (re-)implementing your existing Direct3D code in OpenGL. This is going to be rather straightforward for most of the code (see my porting guide for hints.) On caveat about window handling: In my framework, I did the mistake of not using a cross-platform library for window creation. Instead, I manually created a window on Windows using the WinAPI and on Linux, I did the same using by using the libX11 directly. This led to differences between the Windows OpenGL path and the Linux OpenGL path which I’m still ironing out — learn from my mistakes and use either GLFW or SDL2.
There isn’t much more to say about the actual porting from Direct3D to OpenGL. It is a lot of work, and there is little to speed this process up. One thing that helped me was to use a right-handed coordinate system in both DirectX and OpenGL, which was one less source of error during porting. Other than that, the only help you’ll get here are the various visual debugger tools for OpenGL — apitrace is currently probably the best tool currently, as you can trace on Windows and Linux — and of course the OpenGL wiki, which provides lots of good documentation.
Before we start running the code on Linux, you’ll have to pick a distribution. This is mostly a religious choice, really. My current favourite is Kubuntu, which is the Ubuntu flavour using KDE as its window manager. The nice thing about (K)ubuntu is that it’s really popular, so you can find help easily if needed. If you are less adventurous, you’ll also might enjoy one of the enterprise Linux distributions like RHEL or CentOS. Keep in mind though that those distributions tend to ship pretty old compilers. While compiling your own GCC on them is certainly possible, I wouldn’t recommend this if you are just getting started with Linux.
On Linux, you’ll also want to use an IDE. Qt Creator, KDevelop and Eclipse are all fine — I’m using Qt Creator right now, as is Valve. Just make sure to spend a day or two to get used to your IDE. If possible, try to debug a small application first to get a grip on how GDB works. There was a great presentation on getting started on Linux at the 2014 Steam Dev Days, make sure to check out the videos before proceeding.
So, everything compiles cleanly on Windows, you’re only using portable dependencies, you’ve replaced Direct3D with OpenGL and you’re eager to start working on Linux? Let’s not loose time then. Check out your code on your Linux machine and run CMake. As the target generator, use either Makefiles or Ninja files — Ninja is typically way faster and more likely to choke on incorrectly specified dependencies.
Once the build files are generated, run your compilation from the command line and prepare yourself for extremely quick compile times, as your application will most likely fail immediately with a compile error. That’s fine, and this is the part where you’re going to have a lot of fun trying to figure out what exactly went wrong. The reason why you should be doing this from the command line is that you get the full error messages and you can quickly abort the build. The fastest and most effective way here is to try to fix each compile error individually on Linux and quickly check the fix on Windows. This will save you from long cycles where you fix lots of bugs on Linux, go back to Windows, fix problems there again, go back, just to see you have broken the Linux build, etc. Avoid this and try to get both platforms into a stable state. If you hit a function which you haven’t implemented yet, take a note, write a stub implementation which just exits and go on. This shouldn’t happen too often though if you have identified the platform dependencies correctly. If it happens all the time, I would rather go back, fix the Windows version first and continue porting after that.
Even though it might seem at first that Visual C++ and GCC speak two different languages, changes required to get the code work with both should make the code cleaner and better. One minor source of frustration you might hit are templates, as GCC supports two-phase lookup, while Visual C++ doesn’t (see this question of Stackoverflow for details.) One note on GCC: If you use Visual Studio 2010, 12 or 13, you’ll want to compile with
-std=c++11. Unfortunately (well, depends on how you put it), GCC supports way more C++11 than Visual Studio, so you have to apply special care before you use any new C++ 11 feature.
There’s also one problem with GCC’s (or more precisely, the default C++ standard library on Linux, libstdc++) regular expression library, which is simply non-existent. I do a configure-time check using CMake to see if the standard library provides a working regular expression implementation, and if it doesn’t, I transparently fall back to boost’s regular expression library. With GCC 4.9, this will be fixed and you’ll be able to just use the standard library on all platforms.
I know you were all hoping for some magic trick which allows you to skip the “lots of work” part of actually porting source code, but I haven’t found one. That said, it’s usually pretty obvious which code is calling platform APIs and which isn’t. It’s also really helpful that GCC has better and more extensive support for C++ (11) than Visual C++, as you typically won’t have to work around bugs or missing features. So much for today — next week, we’ll take a look at how to keep it working, and a few other porting tips & tricks.