Back To The Classics: Perfecting The Emulation For Digital Eclipse's Atari Anthology
January 13, 2005 Page 1 of 2
Emulation has long been an interest for me. In fact, it was that interest that led me to become a part of Digital Eclipse in its infancy as a game development studio. Growing up as an avid player of an Atari 2600, Atari has long had a special place in my heart. Even in 1993, a year before joining Digital Eclipse, I was dreaming of the idea of getting an Atari 2600 emulator going on my PC. I had a prototype going, but it would be ten years before the dream would fully become a reality. In 1993, the challenge of trying to get an Atari 2600 emulator going on a 486 33MHz was immense. In 2003, our ultra-modest system requirement of a 200MHz Pentium was much more agreeable, and so Atari: The 80 Classic Games in One for Windows was born. In 2004, Atari invited us to follow up and expand on the concept with Atari Anthology for the Xbox and PlayStation 2 - an endeavor that presented its own challenges.
|Asteroids on Atari Anthology|
Why would a modest console like the Atari 2600, with its 128 bytes of RAM and 1.2MHz CPU, pose even the slightest threat to a modern console? The answer is in the nature of emulation, and why we'd even contemplate emulation instead of rewriting the games for the new console.
How Emulation Works
In general terms, emulation is the painstaking process of recreating the experience of playing a game on the original machine for which it was created. We consider the characteristics of every piece of hardware from the controller buttons to the CPU and figure how each adds to the feel of the game.
Technically, here's how it works:
Every computer ultimately runs machine code binaries. Programs are a series of instructions encoded numerically (e.g., in 8- or 16-bit values). The process performs a specific simple mathematical or logical step based on each instruction. People don't typically write programs that way. They write them symbolically in a programming language. Early on in arcade-style games, it might've been assembly language. Subsequent to that, it might've been a higher language like C.
When someone is asked to convert a game from one platform to another, there are a few approaches: rewrite, port, or emulate.
In a rewrite, you just write a new program that has the look-and-feel of the original. That effort is going to be as good as the new programmer's attention to detail might be, and there's a lot of quirky detail in an action game that can be difficult to reproduce precisely given how a game can act so differently in a different player's hands.
In a port, it's easiest to consider a game written in a high-level language like C (though that wasn't at all common in the first half of the '80s or earlier). As the person porting the game, you'd separate the program into two parts. There's the C code that represents the game logic itself, which you try to leave intact, and there's the platform-specific code (for example, a video driver might be considered part of the platform-specific code). Early computers, arcade games and home consoles had video chipsets that bore no resemblance at all to what we have now. So, you'd have to rip out that code and replace it with something that hopefully works the same way on the new platform.
Inaccuracies come up in two areas here. First, the obvious thing is if the game timing, visuals, etc. derived in some way from the platform specific part of the code you ripped out, you've destroyed that part of the game. Likewise, the performance specs of the new hardware itself can alter game play.
For example, in the Williams game Joust, the player gets to update position every 1/60th of a second, but the enemies update only as many as they can in the CPU bandwidth that's left. If the processing time runs out for a frame of the game, the rest of the enemies are unable to make a new AI decision until the next frame. Thus, the game's AI slows down as the screen gets more crowded. The game has been balanced to compensate for this. If the exact same code were brought to hardware that was identical in every way, excepting only that the CPU was faster, the AI would be able to "think" faster on a crowded screen. The player would see more aggressive and skilled enemies compared to the original.
It really pains me when I read reviews that talk about how appalling it is that our emulation appeared to slow down somewhere, as, for example, one review commented of the smart bomb effect in the N64 version of Defender on Midway's Greatest Arcade Hits, released a few years back. The emulation slowed down because the original game slowed down, and emulation strives to reflect every nuance of the original game. There are often timing nuances and sometimes even original code bugs, which become part of a player's strategy in playing the arcade game. For a truly authentic experience, every one of these quirks needs to be reproduced.
A second source of inaccuracy is the C compiler itself and the fact that the new platform is likely to have a different set of instructions in its machine code vocabulary than the old one did. This means the same line of C is not going to be translated into precisely the same set of mathematical and logical steps. Maybe the new platform had different mathematical precision. You could get a butterfly effect where a small mathematical imprecision (either existing and compensated for in the original game, or introduced by the compiler in the new port) gets amplified over the course of a game until it has some significant consequence, perhaps to player firepower or the like.
So, the question is - how do you make sure all these potential inaccuracies are kept in check? Well, the basic problem is that you're not running the game on the same hardware as the original game. That means you can't use the original machine code, and that means you can't rely on the original graphics or processor timings being the same. Emulation is the answer to this.
In its most basic approach, emulation is an on-the-fly translator. The analogy I'm fond of is this: In porting, it's like you took a foreign movie, gave the script to someone fluently bilingual, and got that person to rewrite the script in English. You'd rely on the translator's appreciation for the nuances of the original language, appreciation for the subtext, the message of the movie, etc. The quality of the product would be entirely a property of the translation effort, and regardless, what is important to one person is not what's important to another. Some double-entendres and the like just don't come across, and need to be replaced with something of equal value, or else ditched.
In emulation, you're watching the original foreign movie, but someone has given you a translating dictionary and all the books on the language's grammar and nuances. Looking up each word on the fly as it's spoken, and appreciating all the impact it has, and still being able to follow the movie in real time sounds impossible. It would be, unless you could think about 10 times to 100 times faster than the movie's pace.
This is what emulation does. It reads each byte from the original game's binary machine code (not the game's source code), looks up precisely how the original game's processor would've responded to it, how long it would've taken to respond, and what aspects of the hardware would've been affected (video, sound, coprocessors, etc.). It also updates a vast array of data, everything there is to know about the original system's state (what was in its memory, what state its hardware such as its timers were in, etc.), in response to these instructions. Just like the movie analogy, all this maintenance takes a lot of effort on the new platform's CPU, since it takes a lot of code in its native language to explain every nuance of the foreign language it's interpreting. So, likewise, a new platform's CPU needs to be 10 times to 30 times faster than the original hardware to process the original hardware's machine code.
The factor varies depending on just how alien the original hardware's capabilities were, and how many coprocessors it might've had. The "modest" Atari 2600 platform, in fact, is so alien to just about every console and computer platform that came after it, that the code necessary to explain it is extremely convoluted. So, emulating an Atari 2600 is much more challenging than, say, emulating Bally/Midway's Rampage, which was released nine years later. But more on that later.
When it comes to recreating the original game's sound, it's a similar process. Sound in an arcade machine or home console is usually built in one of two ways. There will either be some sort of custom sound hardware that's manipulated by changing values that the CPU can access: pitch, volume, tonal quality, etc.; or, the sound samples are built by a CPU and just played out the same way a .WAV file might be on your PC. The first method is typically called FM synthesis; the second is called DAC (digital-to-analog converter) or PCM (pulse code modulated) sound. (There are minor variations on this. For example, instead of FM synthesis, early arcade games may have a trigger that makes custom hardware produce a small selection of very specific sounds, such as explosions or laser shots in different volumes or pitches. Or, in later games, they might be able to handle compressed audio, which might, for example, take the form of ADPCM instead of PCM.)
In any event, the sound controls accessible to the original game's CPU that are going to drive the sound hardware are either going to be manipulated by the main game's CPU, or one of its coprocessors. As our first step, we simulate the original game's CPU and all of its coprocessors. From there, we're going to see the control values it's trying to send to the sound hardware. Like processor emulation, we must then respond to these values and rebuild sounds according to the same rules the original hardware did. In most of our emulation efforts from the PSone on, we rebuilt the sounds as straight audio samples, like .WAV files. If the platform doing the emulating had the CPU bandwidth to spare, we'd build the sounds in real time, (hopefully) just an imperceptible fraction of a second ahead of when they were played back. (Building sounds in this fashion requires careful timing, and you need a CPU fast enough to make sure you have enough power to spare to maintain that timing. It's akin to trying to stay just far enough ahead of an avalanche so that you can get a nice shower of snowflakes down the back of your neck.)
In the case of FM synthesis (or other custom hardware), building the sound in this fashion also requires a precise rule set for how that sound hardware behaved and what its samples would've looked like. You don't just need to know how it responds to the control values the CPU is giving it, but also how it might be influenced by what it was doing before hand. In its own way, it is just as complicated (perhaps more so) than simulating a microprocessor.
In the case of the Atari 2600 and many of the Atari arcade games, the sounds were created using FM synthesis. So, we'd create special ROMs that would just write every possible unique command to the sound hardware, and capture the results that came out of the amplifier. We'd store these as looped samples (after carefully examining the original sound to determine where its loop point was). Then, in our game's emulator, we'd look for the commands that triggered these sounds and run the appropriate sample (with pitch, volume, etc. adjusted) into our console's sound mixer.
|Bowling on Atari Anthology|
Actually, mixing these samples on the fly is something we've only recently been able to accomplish. For older platforms that didn't have the CPU power to do this in real-time and still have the spare bandwidth to emulate the game's main CPUs, we have to pre-process the sound as much as we can. As anyone viewing a streaming video on the Internet knows, you don't need to be able to stream the video in as fast as it plays back if you're willing to wait for it. So, in our "test bed" emulators, even if the computer is not fast enough, the emulator can take as long as it needs to build the sound, and we write the .WAV-like data to the hard drive, instead of playing it out to the speaker. We can isolate certain sounds in the game by patching the ROM in our test bed, so that other sounds can be shut down. This usually requires us delving into the original ROM somewhat. That requires reverse engineering skills and that's where things can get complicated. Fortunately, since we've already created a full simulator of the main CPU running the original machine code, we can see when it writes values to its sound controls, trace back to see what code did that, and then patch that code as we see fit. In that same vein, we find out where the triggers in the code are that trigger these sounds, and patch our emulators to play the pre-fabbed .WAV files when those triggers are seen.
Generally speaking, this is a fairly safe bending of the rules of emulation, because the code looking for triggers is "looking over the shoulder" of the emulator as it runs the original machine code. It does not affect how the emulator responds to the original code. Beyond that, the CPU tells the sound hardware what sounds to make, but it never checks to see what's coming out of the speakers.
The main hazard of this approach is if a game has a large variety of sounds it can make that are "dynamic", influenced in pitch, tempo, or volume, etc. by game events. Mercifully, most early arcade games do not seem to do this, for whatever reason. In the rare case where this does happen, we have to delve deeper into the original machine code, the triggers that change these properties, and how the sound responded to it. We usually have to do a lot of trial and error and hacking (typical of reverse engineering) to pull this off.
Thankfully, since about the N64 and Dreamcast days, home consoles have been fast enough that we don't need to use any of this pre-fab sound technique anymore and can just build the sounds on the fly, at least when it comes to the early '80s games. When it comes to the '90s games (games much newer than what appeared in Atari Anthology), another reason we might not build sound in real-time is if the sound hardware was so complex, it is difficult to create a rule set that mimics it with sufficient accuracy. For that, we can't even pre-process the sounds in our "test bed" and we instead resort to extracting them out of the original arcade games by plugging a computer into the audio out of the machine and sampling it. We might use the same techniques of hacking the ROM as we would above to trigger sounds in isolation, if necessary, except instead of dumping emulated sound output to the hard drive, we record it from the hacked genuine arcade machine using our computer's "line in" jack.