Making a PS2 Emulator: From Bits to Pixels

A major milestone for an emulator is being able to display even the most basic graphical output. Even the simplest consoles, such as the Game Boy, need a fairly decent CPU implementation. As a general rule of thumb, the newer a console, the harder it is to render something correctly. This is because the hardware becomes more complex and tailored for large-scale applications/games. The PS2, unsurprisingly, requires quite a bit of work before anything interesting can be drawn to the screen. Let’s take a look at just how difficult it is for a PS2 emulator to go from bits to pixels.

demo2a.elf, part of ps2tut

demo2a.elf is a homebrew ELF file part of the ps2tut series. It sends a neat picture to the screen, then goes into an infinite loop. Pretty simple, right? But where do we begin?

The Memory Map

Some people would say to start with the CPU, or start by loading the ELF into memory. However, you can’t emulate a CPU without code to read, and you can’t load code without memory. Logically speaking, this leaves only one possibility to start with: the memory map.

As documentation on the PS2 can be pretty hard to find, I’ll spare you the trouble of looking for what you need. Here’s a heavily simplified version of the physical memory map:

Address Range Description
0x00000000-0x02000000 32 MB RDRAM
0x10000000-0x11000000 Hardware I/O registers
0x12000000-0x13000000 Privileged GS registers
0x1FC00000-0x20000000 4 MB PS2 BIOS

Knowing this, we can infer that programs will execute within RAM. Great! Let’s get our ELF loader working!

…Except there’s a couple of complications. Note how I said physical memory map. In reality, the Emotion Engine (the main CPU on the PS2) works with a virtual memory map. All addresses that the EE works with must be translated to physical addresses. The MMU on the EE is fairly sophisticated and hard to emulate without tanking performance, so we’ll just use something that works for 99.99% of programs: AND all virtual addresses with 0x1FFFFFFF. For example, the memory address 0xBFC00000 gets converted to 1FC00000, the first instruction in the BIOS. On a real PS2, virtual addresses are used to separate cached and uncached regions; you would want to execute code in a cached memory region and access I/O registers in an uncached region. We don’t have to worry about the cache, but we must still abide by these rules.

Unfortunately, it’s not all that simple. Virtual addresses 0x70000000-0x70004000 expose 16 KB of scratchpad RAM, a special data cache that is read from and written to manually. As scratchpad RAM is much faster than regular RAM, it’s useful for storing the results of temporary calculations (hence the name). This demo likely doesn’t use scratchpad RAM, but larger demos and games most certainly will. Virtual addresses 0x30010000-0x30200000 also represent an “accelerated” region of our 32 MB of RAM, but there’s nothing special here aside from the extra translation rule.

Finally, after accounting for all that, we can implement our ELF loader. This is fairly simple; just copy all the data associated with program headers to their locations in RAM and start executing from the entry point. We’ll have to start implementing the Emotion Engine now.

Emotion Synthesis

The EE chip in the PS2 contains a custom MIPS CPU, a system coprocessor (COP0), a non-standard FPU (COP1), two Vector Units (VU0 and VU1), and the DMA Controller (DMAC). For such a simple demo, we can ignore most of this. The two vital components are the CPU and the DMAC.


Referred to as the R5900, EE Core, or just the EE, the CPU implements the MIPS III standard as well as some MIPS IV extensions. Sony decided that this wasn’t good enough and also added dozens of Multimedia Instructions (MMI), SIMD instructions similar to Intel’s SSE. As a result, the EE has 128-bit internal buses as well as 128-bit general-purpose registers (GPRs). In practice, the 128-bit components of the processor are only utilized by MMI. This demo likely doesn’t use MMI, or only a very limited subset of it if it does.

Implementing the EE instruction set is probably the easiest part of making a PS2 emulator, even more so if you’re already familiar with MIPS. There’s at least one difference between the MIPS standards and the EE though: the multiplication instruction mult and friends. On MIPS III, the 64-bit result of multiplying two 32-bit registers goes into two special-purpose registers: LO and HI. The result must then be accessed by storing LO/HI into the GPRs using mflo or mfhi. On the EE, the low 32-bit result can go into a GPR directly! This can save an extra instruction, making code smaller and faster.

Also of note is how to handle the 128-bit registers; there’s a few ways to address this issue. PCSX2 uses unions:

/* Located in pcsx2/R5900.h */
union GPR_reg { u128 UQ; s128 SQ; u64 UD[2]; s64 SD[2]; u32 UL[4]; s32 SL[4]; u16 US[8]; s16 SS[8]; u8 UC[16]; s8 SC[16];

With this method, it’s easy to access every part of a register, in both signed and unsigned formats. DobieStation uses a different approach; all registers are stored in an 8-bit array and accessed through templated functions:

/* Located in src/core/emotion.hpp */
uint8_t gpr[32 * sizeof(uint64_t) * 2]; template <typename T>
inline T EmotionEngine::get_gpr(int id, int offset)
{ return *(T*)&gpr[(id * sizeof(uint64_t) * 2) + (offset * sizeof(T))];
} template <typename T>
inline void EmotionEngine::set_gpr(int id, T value, int offset)
{ if (id) *(T*)&gpr[(id * sizeof(uint64_t) * 2) + (offset * sizeof(T))] = value;

PCSX2 possibly has a better approach (it’s less wordy and maybe slightly faster), but I don’t think it matters either way. Oh, and register 0 on MIPS processors is hardwired to zero, hence the check in set_gpr.

Aside from the above oddities, it’s ultimately a tedious but easy process.


Once we’ve implemented a decent chunk of the EE instruction set, we encounter the first SYSCALL instruction. On the EE, this raises an exception that sends the PC to address 0x80000180. If you know how MIPS virtual memory is segmented, then you know this address is in KSEG0 (addresses 0x80000000-0xA0000000), or cached kernel memory. Furthermore, there’s no code that the demo has loaded here. What’s happening?

Unfortunately, we’ve hit our first roadblock towards getting the demo working. When the PS2 BIOS boots, one of its tasks is to copy a basic kernel into low KSEG0 memory. While not a true operating system as PS2 programs still run on the bare metal, it nonetheless provides many services for programs. One of these services is intercepting syscall exceptions and calling a specific function that a program requests. For instance, this demo calls functions to initialize the main thread’s stack and heap. We can’t ignore syscalls, but booting the BIOS is a complicated process that will require weeks, if not months, of added work. We want to get graphical output on the screen as fast as possible, so is there any way around this?

If you guessed “write the EE syscalls using high-level emulation*”, you’re correct. As it turns out, emulating what the demo actually needs is quite trivial. A basic implementation of InitMainThread (RFU060), for instance, only requires storing the main thread’s stack pointer in a return register. Things become more complicated when you introduce interrupts and threads, but we don’t need to worry about either.

Only a few syscalls need to be implemented for this to work, thankfully. That’s one hurdle out of the way!

* High-level emulation (HLE) in this case means re-implementing the PS2 BIOS as a series of C++ functions as opposed to low-level emulation (LLE), which would entail executing the MIPS machine code in the BIOS file. HLE of the PS2 becomes incredibly difficult once you start implementing the Input/Output Processor (IOP), as nearly every game will have patches for the many IOP modules. Play!, an HLE emulator, has made good progress but is still inferior to PCSX2, which uses LLE.


The EE is only capable of accessing certain registers in the video card of the PS2, the Graphics Synthesizer (GS). These “privileged” registers control attributes such as interrupt masks and the size of the final outputted image. However, they provide no access to any rendering functionality. VRAM is also separate from the EE memory map, and thus, inaccessible. Other hardware is required to access the rest of the GS.

This demo uses the DMA Controller (DMAC) to send an image to the GS. Before we get into the details of that, let’s talk about DMA in general. DMA, short for Direct Memory Access, is a piece of hardware found in most gaming consoles (as far as I know) that facilitates copying data from one memory location to another. Most systems will give you various ways to control the process, but the basic concept stays the same.

Predictably, the PS2 likes to be different. First of all, remember how the EE has 128-bit registers? That’s right, the DMAC is also 128-bit! Secondly, all of the ten DMAC channels are hardwired for one peripheral; you cannot perform a memcpy or memset in RAM using the DMAC, for instance. Finally, the DMAC can best be described as “intelligent”, as it does not simply copy data from point A to point B. It’s not necessary to really discuss the full capacities of the DMAC as the demo goes nowhere near using them, but it’s better to think of it as an interface for the EE to communicate with everything else. In certain games, the DMAC can perform even more work than the EE itself!

The demo writes to Channel 2, the EE->GIF interface. The GIF (Graphics Interface) processes data sent to it from three different PATHs and converts it to a format that the GS likes. DMAC Channel 2 happens to be PATH3, the lowest priority PATH. Not much more to say, other than that the demo has to send multiple transfers as there’s a hard limit of 16,384 quadwords per transfer (the image size is 640x256x32 bytes, or 327,680 quadwords).

To prevent this from becoming too long, I’ve decided to split this into two parts. In Part 2, coming up shortly, I will go in more detail about the GIF and GS as well as discuss why the PS2 was designed in such a bizarre manner.

Related posts

Leave a Reply

Be the First to Comment!

Notify of