It's Just UI... Right?
At RAD, we don't think game UI should be a big deal.
When we started developing our UI product Iggy in 2008, we wanted to make a product that could be used to create smooth, functional and sizzling UI for game players. But more importantly, we wanted it to be simple, quick, and resource-efficient for game developers, because, after all, the UI isn't actually the game - it's just UI.
Now, after more than two years of development, we've finished Iggy, and we can say that hitting all those goals was a huge job!
To make UIs easy to create, we chose to use Adobe Flash as our platform for authoring. That makes it easy for artists to create UIs, since they can create in a professional product with a lot of power and flexibility.
But this choice means we have to provide good SWF compatibility (which turns out to be pretty complicated), and we have to do it across multiple platforms, and we have to control resource usage necessitated by Flash, and we have to make it easy to use from C++.
That's where the value of Iggy lies: all the optimizations, hard decisions, and testing to make this whole crazy plan come together.
We needed to make graphics fast, but we wanted to port Iggy to many platforms. To solve this, we defined our own 2D custom graphics rendering API, GDraw. We split Iggy into two pieces, one portable Flash-content core library that renders using the GDraw API, and a non-portable implementation of GDraw on each platform. We tuned the GDraw API so it expresses the things that matter for Iggy in a way that we can best optimize on each of the platforms.
For example, UIs tend to use very large numbers of batches, since many UI elements are made up of multiple objects in Flash. So we iterated heavily on batch performance on each platform as well as tweaking the GDraw API so that we could minimize setting redundant state as much as possible. Even though drawing Flash content uses a lot of draw calls, we've made the cost of making those draw calls amazingly low.
Another example: some platforms may store vertex buffers or textures in memory that's slower to access from the CPU, so you have to be careful to write it a cache line at a time. Rather than burden Iggy with this, we designed GDraw with a flexible interface where Iggy writes to memory specified by GDraw in blocks specified by Iggy. On platforms where this isn't needed, GDraw tells the Iggy to write all the data to one continuous block and Iggy proceeds at full speed. On platforms that need more careful access, GDraw can have Iggy write to a small buffer then transfers the data itself a cache-line at a time; the buffer is small enough to always stay in L1 cache.
Tons of little tricks like this make all the difference - in some cases, doubling or quadrupling performance over a naive port.
The biggest complexity in Iggy comes from supporting ActionScript.
Since artists can script anything in ActionScript 3, we wanted to do a good job in terms of performance and memory. Doing this was a lot of work. We carefully defined the object layouts for the virtual machine so they're compact but so that the interpreter and the garbage collector can extract information they need efficiently. The garbage collector provides a good example that's easy to describe.
To avoid spiky performance from garbage collection, we chose to use a fairly sophisticated incremental garbage collector. Specifically, Iggy uses a generational collector with two generations -- with a copy collector for the young-to-old collection and an incremental collector for the old generation. The copy collector must relocate objects, but to keep the interpreter fast, we use direct pointers rather than indirection through an object table; this has the downside of making it more complicated to support the object relocation required by the copy collector. But that's ok; it's a complicated implementation, but we wanted to support relocation for another reason: it's necessary to be able to defragment the AS3 heap so that we can run more effectively in a fixed memory budget on a console.
We knew right from the start we'd have to run in such contexts, and that's why we committed to such a sophisticated design. And by committing to it right from the start, it meant that every data structure we designed, and every line of code we wrote, was written with awareness of, and optimized, for this garbage collector design.
But speed and efficient memory use only help if artists can actually use ActionScript in Flash and have it behave the same in Iggy. That means we have to support executing ActionScript bytecodes, carry out their effect on the ActionScript memory model, and make the ActionScript interact properly with objects defined in Flash and with animations and event flow defined by Flash.
Now, although Adobe have published several Flash specifications, these docs are mostly just file format descriptions: they don't describe anything about how things behave, especially over time. There is a lot of ambiguity, for example, about exactly how subsystems work and how various pieces interrelate. And that's not a knock - it's an incredibly hard job describing the behavior of complicated source code, even when the product isn't being updated and new features constantly being added!
As a specific example, the SWF document describes the idea of what things should do, but not the details. And the ActionScript 3 bytecode specification describes what all the opcodes do, but they don't really describe the memory model or execution environment in which the things are happening. And then there is no document explaining the interaction between the two; the AS3 bytecode specification mentions nothing Flash-specific, and the SWF specification just describes where the AS3 bytecode is located in the file. So we had to fill in all these gaps ourselves.
We did this in many different ways. In some cases, we could just write ActionScript code in Flash which would print out easily visible state; in others we wrote raw bytecodes and loaded those into Flash Player. For analyzing existing pieces of code for which we didn't have source, we wrote a system which rewrote a SWF file so that new bytecode was interleaved with the existing bytecode. The new bytecode printed out the current executation address and the top element of the interpreter stack. By logging the output of executing this new SWF in Iggy and in Flash, a simple diff would show us the exact instruction where Iggy went wrong, so we could quickly discover and fix circumstances where we hadn't correctly determined what ActionScript's behavior was.
On top of the ActionScript execution itself, the ActionScript libraries are fairly complex and require a lot of detail to implement in a compatible way. This is time-consuming because the only documentation for these libraries is user-facing. That is, the documentation just describes what the function does, not always how it interacts, or how edge cases or out-of-range values are handled, or how Adobe themselves implemented the libraries.
Since we want Iggy to work even if artists cut-and-paste sample code off the web, we have to match Flash Player's behavior even if the code that is executing isn't officially well-defined by Adobe. This meant we had to spend a lot of time studying the behaviors of each of the functions and methods exposed by the library to make sure we were implementing them correctly.
The libraries are also fairly broad. They include a lot of functionality that is useful for Flash on the web, but maybe not so useful for game UI. But we tried to implement everything that we thought made sense from a UI standpoint.
For example, we implemented everything from MovieClip to MouseEvent, but what about Date? The Date class defines 44 methods, some of them trivial and some of them pretty complicated. Most games won't even need it -- but it's in there, just in case.
Iggy includes many other subsystems, as well!
- A triangulator to convert vector shapes to triangles.
- A bitmap font rasterizer for small characters (large characters go through the triangulator).
- A separate system for converting wide lines to triangles.
- A system to generate edge anti-aliasing for the polygons.
- Linear and circular gradient support.
- The GDraw implementations on platforms with pixel shaders include a pixel shader for a special circular gradient variant and pixel shaders for a full GPU-accelerated implementation of blurring and the other filter effects defined by Flash.
- A full audio mixer which can mix from 8-bit and 16-bit mono and stereo elements.
- The super-optimized MP3 decoder from Miles (including patent rights), plus an ADPCM decoder just in case.
- A custom memory allocator that lets each Iggy file segregate it's allocations from other Iggy files to avoid fragmentation if you're running more than one Iggy at a time.
- ActionScript 3 supports a Dictionary type in which the dictionary keys are weak references, so you can cache data associated with objects without preventing the objects from garbage collecting - and Iggy supports it too.
- Debugging tools: Iggy Explorer (to view the heirarchies that your artists create), Telemetry bindings (so you can drop Iggy timings right along side your game's), and full memory allocation tracking.
We're very happy with how Iggy turned out and are excited for everyone to try it. It's been a long development and we can't wait to see what you guys make with it - after all... it's just UI!