[Edit 20051110: We've been hosted on BerliOS! The new wiki info page is on the home page at http://taspring-linux.berlios.de and the project page is at http://developer.berlios.de/projects/taspring-linux/]

TA: Spring is a rewrite of Total Annihilation, the old RTS game made by Cavedog back in 1997. It’s designed to use the old units, but it essentially moves them into an up-to-date and advanced engine – it’s fully 3D with a rotatable camera (like Ground Control), supports shadows, positional sound, terraforming, etc.
I’ve joined the team of several people that’s working on rewriting all the windows-specific code to be cross platform. (https://opensvn.csie.org/traccgi/taspring_linux/trac.cgi/wiki)

Here I’ll be collecting notes on porting (for people who might be doing the same thing), code segments, experiences, etc. It’s all about the work I’ve done for it, and there’s no real organization; it’s pretty much just a list of items in the order that I did them.

- I first got started by submitting a small patch as a compile fix.

- One of the first things I did was rewrite the Bitmap loading code to correctly load up bitmaps for use in the game. The bitmap header that comes before the actual image data is something like this:


struct bitmapfileheader_s {
uint16_t bfType;
uint32_t bfSize;
uint16_t bfReserved1;
uint16_t bfReserved2;
uint32_t bfOffBits;
};
struct bitmapinfoheader_s {
uint32_t biSize;
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
int32_t biXPelsPerMeter;
int32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
};

Those two follow each other in order. So, for example, the first 2 bytes of the file is the file type (bfType), the next 4 bytes is the file size (bfSize), etc.
A lot of people like to load the data directly into the struct. With something like:


fread(&bmpfileheader, sizeof(struct bitmapfileheader_s), 1, fileptr);

Which kind of make sense, right? It just fills in the fields from top to bottom. However, there are two major flaws with this. One is that you cannot assume every compiler automatically pads structs the same way. Depending on the architecture and the compiler implementation, it could be putting padding in places to optimize the way the struct fits into memory. So there’s no telling whether there’s, maybe, two bytes of unused space in between bfType and bfSize. So if it reads directly in, the first two bytes of bfSize are going to end up in those two padding spaces, and then the last two bytes of bfSize get loaded into bfSize along with two bytes of the _next_ field, etc. The second problem is that that will only work if the endian-ness of the machine is exactly the same as the one that created the file. A little endian bitmap will have the bytes of bfType swapped. If you read it in directly on a little endian machine it’s fine, since it’s already swapped, but if you read that bitmap on a big-endian machine, it’ll appear backwards and the data will be incorrect.
Therefore as a solution to this, you need to read each variable in order and byteswap if necessary. So for example, you would fread two bytes, swap if necessary, then put it into bfType. Then you would fread four bytes, swap if necessary, and put it into bfSize. And so on. It’s more work, but it guarantees that it’ll work on all architectures and compilers.

- Computers have a floating point flag register that controls what exceptions get thrown on floating point errors. Windows uses the functions _control87() and _clearfp() to set and clear flags. Posix uses fegetenv() and fesetenv() to get and set, respectively. Windows also accepts weird parameters that will set and clear the flags in a weird way. The replacement mapping code that would be needed is something like:


#define _EM_INVALID FE_INVALID
#define _EM_DENORMAL __FE_DENORM
#define _EM_ZERODIVIDE FE_DIVBYZERO
#define _EM_OVERFLOW FE_OVERFLOW
#define _EM_UNDERFLOW FE_UNDERFLOW
#define _EM_INEXACT FE_INEXACT
#define _MCW_EM FE_ALL_EXCEPT
static inline unsigned int _control87(unsigned int newflags, unsigned int mask)
{
fenv_t cur;
fegetenv(&cur);
if (mask) {
cur.__control_word = ((cur.__control_word & ~mask)|(newflags & mask));
fesetenv(&cur);
}
return (unsigned int)(cur.__control_word);
}
static inline unsigned int _clearfp(void)
{
fenv_t cur;
fegetenv(&cur);
#if 0 /* Windows control word default */
cur.__control_word &= ~FE_ALL_EXCEPT;
fesetenv(cur);
#else /* Posix control word default */
fesetenv(FE_DFL_ENV);
#endif
return (unsigned int)(cur.__status_word);
}

- MessageBox(), for showing dialog boxes on windows, of course doesn’t work on linux. This is easy to wrap into a function that will print out an error message to stderr or something.

- QueryPerformanceCounter() and QueryPerformanceFrequency() are used for timing purposes. I originally thought these were equivalent to getting the computer’s RTC time and getting the computer’s processor speed respectively, but I was actually wrong. More on that later.

- For some reason, windows likes to remap all of the standard variable types. So for example, instead of just using the standard ansi C uint32_t and int32_t, they use UINT32 and INT32 respectively. There are a whole bunch of other variables like that, so they all needed to be fixed and remapped to standard variables.

- Naturally, there’s no registry on Linux. I had to write an alternative way to store configuration settings, in the standard way of putting a dotfile in the user’s home directory (like /home/xiphux/.springrc).

- The original version of the program had a really terrible method of handling fonts for text display. What it would pretty much do is load up a font from the core windows system (with CreateFont), render all the glyphs into various GL textures for a number of different font sizes, and then save that all into a windows *.FNT file. On the next run if it found that file it would just load and use it, and if not, it would just try to regenerate the file again.
Not only is it a big waste of space to store all those textures on disk, you’re essentially doing twice the work since you’re having the windows font system read whatever font from C:\windows\fonts, convert all those glyphs into textures, store it into a file, and then reload them from the file again. Since the program was already using the FreeType font library in another place, I just used that and had it load up and render the glyphs from Truetype fonts on the fly. It’s faster and it allowed me to implement other more advanced features like using the font kerning data in Truetype fonts to adjust the glyph positions to make them look better.

- Windows is a 32-bit system, so all of its types are geared for 32-bit programming. So while windows does actually support 64-bit types, most people tend to use the LARGE_INTEGER struct. It has a member, QuadPart, for systems that support 64-bit types, and HiPart and LoPart members to access the higher and lower 32-bits of the number. This is a waste because we would have to implement the structure on linux; it’s a lot easier to change all LARGE_INTEGERs to actual 64-bit types.

- Linux doesn’t have a built in find function, the way windows does. It has file globbing functions, but they’re not recursive. Not only that, but since Linux is case sensitive and windows is not, it would require a lot of work to convert case-sensitive linux globs into case-insensitive results. For convenience, we decided to use Boost for cross-platform pathname handling. Once that was decided, it wasn’t too hard to implement a generic find function.

- Total Annihilation stores its data in HPI resource files. Spring originally used a DLL version of HPIUtil to read the resource files. And of course, DLLs don’t work on linux. There is source code available for HPIUtil, but it’s completely windows specific – there are numerous cases of load-to-struct that I mentioned before. It casts everything back and forth between void pointers and integers. Integers are 32 bits, but pointers are the word size of the system. On 32 bit systems (like windows systems) it’s fine, but on 64-bit systems it’s a great way to cause crashes and security holes. Fixing that would require a complete rewrite, so I wrote up a new version that’s C++ class-based. For lack of a better name, I just called it hpiutil2.

- Boost was also used for cross platform threading. It’s very easy to program in that it uses scoped locks. Scoped locks only exist for a certain scope, like maybe for one function. It’s a lot like how temporary variables inside functions don’t exist outside of the function. It makes it a lot easier to lock and unlock, since you don’t have to manually unlock mutexes all the time – you just declare one to lock it, and at the end of its scope it’s just destroyed and the lock is automatically released.

- sleeping in windows is given in milliseconds, but sleeping in linux is given in microseconds.

- I ported the window creation, basic GL initialization, and input handling to SDL. GLUT (what was originally being used before I joined) is a good system, but the problem is that it’s not as fully featured as an actual game library. It’s really suited for small 3D tech demos and stuff. More specifically, the problem is that using the modifier keys (Ctrl, Alt, Shift, etc) doesn’t send any keyboard events; it has to be tested for manually with glutGetModifiers(). So some of the features we needed (like holding down shift to show the unit’s current orders and queue up new ones) weren’t supported in plain glut, so I moved it to SDL.

- Sound was originally implemented with DirectSound. I rewrote it to use OpenAL for 3D positional sound.

- In the windows version, shadows are implemented using a small vertex shader and fragment shader program, and are rendered in the windows pbuffer before being displayed. I’ve extended it to use EXT_framebuffer_object (a new GL extension, released in Feb. 2005), which is pretty much a generic platform-independent pbuffer. If it can’t find that (since it’s a relatively new extension so not all drivers support it), it’ll fall back on the windows pbuffer on win32.