June 30th, 2010
This is a guest blog post by Andrew Church. He is a talented software engineer and OSS aficionado based in Japan. Since the Aquaria source was released, Andrew has been busy hacking away on an unofficial PSP version. Read on for his story:
After discovering (and being completely enthralled by) Aquaria via the Humble Indie Bundle, I was delighted to see that the source code was going to be released as open source. I've lately done a fair amount of development for the PSP—its restrictive environment is a nice challenge to work with, and of course it's fun to get game programs running on a gaming platform—so I thought I'd see if Aquaria could be ported to the PSP.
As it happens, I had already developed a multiplatform engine for another game, supporting the PSP among other platforms. That engine unfortunately never saw the light of day, but it did provide a convenient place from which to start. In fact, I got underway a full week before the Aquaria source was released, sifting through my code to figure out what would most likely be useful and how best to interface it to an as-yet-unknown code base.
The PSP environment
While Sony has gone to a fair amount of trouble to prevent people from running their own software (so-called "homebrew" software) on their PSPs, it's nonetheless possible to do so thanks to the efforts of a number of independent developers who opened up the platform. There's an unofficial SDK (see ps2dev.org, though the site goes down occasionally) which is more than adequate for game development, and with older PSP models, it's even possible to install custom firmware to extend the OS services provided by Sony's official system software (and avoid the necessity of running a "homebrew enabler" every time you restart the system).
Thanks to the unofficial SDK, as well as a USB shell (psplink, also from ps2dev.org) which allows programs to be run directly from a PC, the process of developing for the PSP is not much different from developing for the PC environment: write code in your favorite editor or IDE, build it using a cross-compiler and cross-linker, and run it through the USB shell. However, if you approach programming for the PSP as you would a typical PC environment, you'll quickly run into difficulties.
For those unfamiliar with the PSP's internals, it has:
- a MIPS-based CPU running at a user-selectable speed of up to 333 MHz;
- 24MB of system RAM available to user programs, increased to 52MB on the PSP-2000 "Slim" and PSP-3000 "Brite" models;
- 2MB of graphics RAM;
- a simple (shaderless) graphics processor;
- 8 channels of stereo audio;
- a secondary CPU used for video and audio decoding;
- and a Memory Stick for data storage.
And that's it, more or less. No virtual memory, no I/O caching, no many things taken for granted on a PC.
My own engine was in fact originally Linux-specific, only ported to the PSP after I had completed the first iteration of the program. As a result, I already had a fairly good idea of the issues I'd need to deal with; floating point calculations—the PSP can only handle single-precision values in hardware, so every use of "double" requires a software emulation call—and memory allocation were close to the top of the list.
The hidden horrors of STL
It turned out that memory allocation was the biggest hurdle to creating a PSP port. Aquaria is written in C++, and makes heavy use of the Standard Template Library. From a PC development standpoint, I'm sure that's a great idea, as it saves the time of manually implementing all sorts of data structures; if you want a mapping from strings to FooStruct pointers, you can just say "std::map<std::string, FooStruct*>" and presto, you have your mapping. Unfortunately, every time you do that, STL kills a kitten.
Well, maybe not. But practically every operation involving STL objects—be it storing to a map, concatenating strings, what have you—triggers a memory allocation, sometimes several. On a PC, where you have gigabytes of virtual memory space even on 32-bit systems and you don't have to worry about fragmenting physical memory because the OS will remap it for you, that's just fine. On the PSP? Not so much.
I had already written a custom memory allocator for my own engine, which had served me very well. It allowed the caller to choose where the memory was to be allocated (top or bottom of memory, main or temporary pool), so I could minimize fragmentation. When built in debug mode, the allocator would even record the file and line where each block was allocated, an invaluable aid in minimizing memory usage and tracking down the occasional leak.
This allocator used a block size of 64 bytes, partly to accommodate hardware alignment requirements and partly to reduce the potential for memory fragmentation. For my engine, which was judicious about allocations, this was no problem. For STL, which likes to allocate 5-byte and 12-byte and 27-byte blocks all over the place, the overhead got pretty ridiculous.
I ended up writing a secondary allocator that sat under the primary one and functioned more like a traditional malloc(), parcelling out memory in 8-byte blocks with only a minimal header. I changed my custom malloc() routines to target this secondary allocator, and was able to get the overhead down to manageable levels. (I subsequently had to add a third allocator specifically for Lua, the scripting language used by Aquaria, which spams tiny requests even more often than STL.) I still haven't ironed out all of the fragmentation issues, but that was at least enough to get the game running.
As mentioned earlier, the PSP has only 2MB of graphics RAM. The PSP's display size is 480x272 pixels, with a line stride of 512 pixels, so a standard double-buffered setup at 32 bits per pixel already takes up over half of the available memory; add a 16-bit depth buffer and an extra framebuffer for offscreen rendering, and you've pretty much exhausted it entirely. Fortunately, the PSP's rendering hardware (the GE, presumably "Graphics Engine") can load texture data from system RAM, though it's terribly slow unless you "swizzle" the data into the 16-byte-by-8-line blocks which the GE uses for pixel processing.
Aquaria's textures were designed for an 800x600 display, which is of course more resolution than is needed on the PSP. Moreover, there are a number of 1024-wide or 1024-high textures, which exceed the GE's limits of 512x512 pixels. Since shrinking every texture at runtime either reduces quality (if you use the simple pixel-dropping algorithm needed to maintain reasonable execution speed) or is unbearably slow (if you try to scale down more smoothly), I opted to pre-scale the textures down by half in each direction and save them in custom-format files which the PSP port would look for in preference to the original PNGs. I also converted each texture to 8-bit indexed format, reducing the total data size by a further factor of 4 while causing minimal image degradation—I haven't yet noticed any textures where the color quantization stands out.
I subsequently took advantage of my custom texture format to record the amount of blank space at the edge of each texture. Since the GE takes time to process each pixel of a texture even if it's transparent, the wide transparent borders used in some of Aquaria's textures to ensure power-of-two texture sizes slowed the frame rate down noticeably. I added code to my texture converter to detect such blank borders and record their sizes in the texture files, then modified the core renderer to make use of those values when assigning vertex coordinates.
Packaging the data files
The commercial distribution of Aquaria includes close to 5,000 data files. As mentioned earlier, the PSP's OS does not do any sort of I/O caching, so every time you open a file, the OS has to read through the entire directory to find the file you need. When running the code over a USB connection, this delay is hidden by the host OS's disk cache, but when you move to the Memory Stick, the program can slow down by an order of magnitude or more. In fact, when I first ran Aquaria from a Memory Stick, it took nearly eight minutes just to load the initial data set.
In the original engine from which I took most of my code, I had already developed a package file system to deal with these delays. Rather than opening individual files from the physical filesystem, I stored them all in a single package file, whose file lookup table I could cache in memory when the program started up. Loading a data file then becomes as simple as seeking to the start of the data in the package file and reading the requisite number of bytes, which the PSP can handle much more quickly. For Aquaria, the use of a package file reduced the initial loading time from 460 seconds to a much more reasonable 14 seconds.
To improve speed further, the PSP port uses a separate I/O thread for reading from files. (In fact, the PSP-specific code makes fairly extensive use of threads: an audio mixing thread, one thread per audio stream decode, a vertical sync thread, and probably a couple others I'm forgetting.) My original engine took advantage of this to issue asynchronous reads for data files, allowing gameplay to continue while new data was being loaded into memory. Aquaria's core code does not seem to be designed for asynchronous I/O, but the use of an I/O thread still provides a moderate performance improvement by reducing the number of system calls required to load a file.
A minor issue with the use of a package file was that the Aquaria source uses standard library calls such as stdio's fopen() and C++ file streams (which in turn call stdio) to access data files. Rather than trying to search out and change all of these calls to PSP-specific ones, which would then of course require #ifdef bracketing, I wrote a replacement stdio library which first looked up each requested file in the package, falling back to system calls if the file was not found there. This coincidentally fixed an unnecessary dependency on the PSP networking libraries, which were pulled in by a setsockopt() reference in Newlib's implementation of fcntl(). (Newlib is the standard C library used with the unofficial PSP SDK; there's also a minimal C library included with the SDK, though I haven't yet tested whether it has enough functionality to support Aquaria and its dependencies.)
Pushing the code
Once I had gotten the game to a minimally playable state, I then wanted to push my code to a public repository for others to use. I didn't want to push the entire 57,000-line diff as a single changeset, since that would make it all but impossible for anyone else to see what I had actually changed in the original Aquaria code and why, and equally difficult to track down the cause of any regressions. I also didn't want to individually push the 500-odd changesets from my private repository, for the same reason (I often make multiple commits while working on a single feature, using the repository as a sort of undo buffer while I experiment) and also because some of my commit comments weren't exactly . . . fit for public consumption, let us say. Instead, I went through my changesets one by one, grouping the changes into related categories, and ended up with a total of 40 changesets which I pushed to a separate Mercurial repository I had created for publishing my patches to Aquaria.
I've been through a few source code management systems over the years (RCS, CVS, Subversion), but Mercurial is my current preference. As a distributed SCM tool, Mercurial has the significant advantage of being able to commit changes without access to a primary repository, a feature of which I make extensive use. Perhaps because of my experience with CVS, I've found Mercurial straightforward and easy to use, as opposed to Git, which feels just a bit too dense for me to take interest in. It also turns out to be easy to publish Mercurial repositories on the web, so I put my public Aquaria repository online for others to peruse.
But . . . does it work?
At the moment, I would have to say "sort of". It works in the sense of being playable; you can explore the world of Aquaria as freely as on a PC. On the other hand, there's still a lot of optimization needed, as the frame rate drops sharply in heavily-inhabited areas. (Naturally, I don't mean any criticism of the original code, which runs just fine on the PC. The PSP in particular is arguably outdated in terms of technology, and I can hardly fault Alec for not considering it when he created the program.) Nonetheless, with sufficient patience, it may already be possible to complete the game on the PSP. And I don't intend to stop here, of course—a masterpiece like Aquaria deserves better!
Aquaria on the PSP in action: watch on YouTube