An interactive course · Dolphin emulator

How the GameCube thought.

We start from the very beginning — what a processor even is — assuming you know nothing about computer architecture. Once the vocabulary is solid, we meet Gekko, the modified IBM PowerPC at the console's heart, and finish inside Dolphin's just-in-time compiler. Fifteen modules, each with a live, clickable machine so you can watch every idea run, not just read about it. No game code ships with this page; every simulation is built in your browser.

Part I

How CPUs work

This first part is pure fundamentals — nothing about any specific console or chip yet. It's the toolkit every processor is built from, whether that's your phone, your laptop, or the chip we'll meet in Part II. We build the vocabulary one word at a time — instruction, register, pipeline, cache — and you can step and run each idea in a tiny simulated machine. If a term is ever used before it's explained, that's a bug; tell us. Everything in Parts II and III is just these six ideas wired together.

Modules 01–06Difficulty Absolute beginnerLabs 4 interactive
Module 01 · Part I

The fetch–decode–execute loop

GoalUnderstand, from scratch, what a processor actually does: fetch a number from memory, work out what it means, do it — forever.

Let's start with what a processor physically is. Strip away the marketing and a CPU is a machine that repeats one tiny ritual, billions of times per second, without ever getting bored. The ritual has three steps: fetch a number from memory, decode it — work out what command that number stands for — and execute it. Then do the next one. That's the whole job. Everything a computer has ever done — every game, every physics engine, every pixel — is that loop, running very fast.

Two words in that ritual need unpacking. Memory is a long row of numbered pigeonholes, each holding one small number (a byte). The pigeonhole's position in the row is its address — cell 0, cell 1, cell 2, and so on. And an instruction is just one of those stored numbers that the CPU has agreed to treat as a command. There is nothing special about the number itself: 0x1E sitting in memory might mean “load from cell 14” to the processor, or it might just be the number 30 in someone's data. Position and convention decide, nothing else. Programs and data live in the same memory, side by side — this is the single most important idea in all of computing, and (in Module 14) it's also what makes emulators sweat.

So how does the CPU know which cell holds its next command? It keeps a bookmark: a small counter called the program counter (PC). Each trip round the loop it fetches the instruction the PC points at, then normally bumps the PC forward by one so the next trip fetches the next cell. The exception is a branch (a “jump”): an instruction whose entire job is to overwrite the PC with a new address, so execution leaps somewhere else. Branches are how programs make decisions and loops — and in Module 04 they'll turn out to be the pipeline's biggest headache.

Analogy · A cook with recipe cards

Picture a cook working from a stack of numbered recipe cards, one short command per card: “fetch the flour”, “add two eggs”, “go back to card 3”. The cook keeps a finger on the current card — that finger is the program counter. Read a card, do what it says, slide the finger to the next card. The cook never sees the “whole recipe”; only ever one card at a time. A CPU is exactly this cook — just one who reads half a billion cards a second and never misreads one.

Memory 0 · 0x1Eload 1 · 0x2Dadd 2 · 0x3Estore 3 · 0x40jump data PC = 1 (the bookmark) 1 · Fetch read the cell PC points at 2 · Decode 0x2D → “add cell 13” 3 · Execute do it, then PC = PC + 1 repeat, forever instruction = a number
The loop that runs the world. Instructions are ordinary numbers stored in memory. The program counter points at the next one; the CPU fetches it, decodes what the number means, executes it, bumps the PC, and repeats — billions of times a second.

A worked example, by hand

Let's invent the smallest possible machine and run it on paper. Our toy CPU has 16 memory cells (addresses 0–15), one working register called A (a register is a scratch value inside the CPU — Module 02 is all about them), and a PC. Each instruction is one byte: the first hex digit says what to do — the opcode — and the second says which cell to do it to. Our whole instruction set is four opcodes: 1n = load cell n into A, 2n = add cell n to A, 3n = store A into cell n, 4n = jump to cell n.

Now put this five-instruction program in cells 0–4, with data in cells 12–14: 1E 2D 2C 3E 40. Read it with the table above: load cell 14, add cell 13, add cell 12, store cell 14, jump to 0. Cell 13 holds 2 and cell 12 holds 1, so each trip round the loop the number in cell 14 grows by 3. That's it — a counter, the “hello world” of hardware. The lab below is this exact machine. Press Step and watch each fetch, decode and execute happen with your own eyes; press Run and watch it become a program.

Toy-CPU lab — a visible fetch–decode–execute machinesimulated · live
Memory · 16 cells
Registers
Press Step to run one phase of the loop, or Run to let it fly.
Violet = where the PC points · cyan = memory being read · magenta = memory being written.
Key takeaways
  • A CPU repeats one loop forever: fetch an instruction, decode it, execute it.
  • Instructions are ordinary numbers in memory — programs and data share the same pigeonholes.
  • The program counter is the bookmark; a branch is an instruction that rewrites it.
  • An instruction's number splits into an opcode (what to do) and operands (what to do it to).
Module 02 · Part I

Registers, memory & the ISA

GoalLearn why CPUs keep a tiny set of super-fast registers, what a load/store machine is, and what “PowerPC” actually names.

Our toy CPU had one working register, A. Real processors have a few dozen, and the reason is distance — literally. Memory is a huge structure physically far (in chip terms) from the circuits that do arithmetic, so every trip to it costs time. A register is a small storage slot built inside the CPU, right next to the arithmetic circuitry. It holds one value — 32 or 64 bits — and reading or writing it costs essentially nothing. Registers are the workbench; memory is the warehouse. There are only a handful of registers precisely because they're so close: you can't fit a warehouse on a workbench.

Analogy · The workbench and the warehouse

A carpenter doesn't walk to the warehouse for every screw. They fetch a tray of parts to the bench, work there — where everything is within arm's reach — and carry results back when done. A register is a spot on the bench; a load is the walk to the warehouse shelf; a store is the walk back. Good code, like a good carpenter, organises the work to stay at the bench as long as possible.

That analogy is baked into hardware as the load/store architecture: arithmetic instructions are only allowed to work register-to-register, and the only instructions that touch memory are explicit loads and stores. Want to add two numbers that live in memory? Load both into registers, add the registers, store the result back. Three-plus instructions instead of one — but each is simple, uniform, and fast, which (Module 04) is exactly what a pipeline wants. This philosophy has a name you've heard: RISC.

Memory millions of cells big · far · slow Registers — the workbench r3 = 12 r4 = 30 r5 = 42 … r31 ALU add · multiply · compare — registers only load — the only way in store — the only way out results go back to registers
A load/store machine. Arithmetic never touches memory directly. Explicit load and store instructions move values between memory and the register file; the ALU works only on registers. PowerPC — and therefore Gekko — is built exactly this way.

What “PowerPC” actually names

Here's a distinction the whole course leans on. An instruction set architecture (ISA) is a contract: the documented list of instructions, the registers, and exactly what each instruction must do. An implementation is a physical chip that honours the contract. PowerPC is an ISA — a RISC design from IBM, Motorola and Apple in the early '90s. The PowerPC 750 is one implementation of it (sold by Apple as the “G3”), and Gekko — our Part II star — is a 750 derivative. Any chip honouring the contract runs the same programs; how fast, with what pipeline and caches, is the implementation's own business. This split is also what makes emulation possible: Dolphin (Part III) implements the PowerPC contract in software.

The PowerPC contract that matters to us: 32 general-purpose registers (r0r31, 32-bit, for integers and addresses), 32 floating-point registers (f0f31, 64-bit — remember that width; Module 08 turns it into Gekko's party trick), and a few special ones:

RegistersWidthWhat they hold
r0 – r3132-bitGeneral-purpose: integers, addresses, loop counters — the workbench.
f0 – f3164-bitFloating-point values (Module 03). On Gekko, each can also hold two 32-bit floats (Module 08).
PC32-bitThe bookmark from Module 01 (PowerPC manuals call it the “current instruction address”).
LR · CTR · CR32-bitLink register (return address for function calls), count register (hardware loop counter), condition register (results of comparisons, read by branches).

And here's what real PowerPC code looks like — the same “add two numbers from memory” job we described, each line glossed in plain words. Every instruction is exactly 4 bytes, a fixed size that makes fetching and decoding beautifully predictable:

lwz r3, 0(r10)
load the word at the address in r10 into r3
lwz r4, 4(r10)
load the next word into r4
add r5, r3, r4
r5 = r3 + r4 — registers only
stw r5, 8(r10)
store r5 back out to memory
Key takeaways
  • Registers are a few dozen ultra-fast slots inside the CPU; memory is the big, slow warehouse.
  • In a load/store (RISC) machine, arithmetic is register-to-register; only loads and stores touch memory.
  • An ISA is a contract; a chip is an implementation of it. PowerPC is the contract, the 750 and Gekko are implementations — and Dolphin is one in software.
  • PowerPC gives you 32 general-purpose registers and 32 double-width floating-point registers.
Module 03 · Part I

Numbers in silicon

GoalSee how bits encode whole numbers, negative numbers, and — via IEEE-754 floating point — fractions. Honestly, including why 0.1 can't be stored exactly.

Everything a CPU touches is stored in bits — switches that are either 0 or 1. A row of bits reads as a number in binary, exactly like decimal but with columns worth 1, 2, 4, 8, 16… instead of 1, 10, 100. So 1011 is 8 + 2 + 1 = 11. With n bits you get 2n patterns: a byte (8 bits) covers 0–255, and the 32-bit registers from Module 02 cover a bit over four billion values. So far, so mechanical.

Negative numbers need a convention, and the universal one is two's complement: keep counting past the top and let the value wrap around, like a car's odometer rolling over — except we relabel the top half of the patterns as the negatives. In 8 bits, 11111111 is −1, 11111110 is −2, down to 10000000 = −128. The lovely consequence: the same adder circuit adds signed and unsigned numbers correctly without knowing which it's doing. The price: a fixed window (−128…+127 for a byte), and arithmetic that silently wraps when you leave it. Keep two's complement in your pocket — Module 09 stores whole vertex coordinates this way.

The hard part: fractions

Whole numbers were easy. But games live on fractions — positions, angles, velocities. The trick every modern CPU uses is floating point, and it's precisely scientific notation in binary. In decimal, scientific notation writes 42,500 as 4.25 × 104 — a significand (the digits) and an exponent (where the point sits). Binary floating point does the same with 2 as the base. The IEEE-754 standard — honoured by PowerPC, by your PC, and (imperfectly, see Module 14) between them — packs a 32-bit single-precision float into three fields:

  • 1 sign bit — 0 for positive, 1 for negative.
  • 8 exponent bits — where the binary point sits. Stored with a bias of 127: the field holds the true exponent + 127.
  • 23 mantissa bits — the significand's fraction digits. Because a normalised binary significand always starts “1.”, that leading 1 isn't stored — a free 24th bit of precision.

The decoded value is (−1)sign × 1.mantissa × 2exponent−127. A 64-bit double is the same idea with an 11-bit exponent and 52-bit mantissa — the format PowerPC's f0f31 registers hold natively. Singles are half the memory and (on most hardware) faster; doubles are far more precise. Games overwhelmingly choose singles — which is exactly the door Gekko's paired singles (Module 08) walk through.

sign · 1 bit exponent · 8 bits mantissa · 23 bits 0 1 0 0 0 0 0 0 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 sign = 0 → positive exponent = 10000001₂ = 129 → 129 − 127 = 2 mantissa = .0111₂ → significand 1.0111₂ = 1.4375 value = +1.4375 × 2² = 5.75
Anatomy of a float. The 32 bits of an IEEE-754 single split into sign, exponent and mantissa — here encoding 5.75. It's scientific notation, cast in silicon.
Analogy · A ruler with sliding tick marks

Floating point is a ruler whose tick marks squeeze together near zero and spread apart as numbers grow. Between 1 and 2 there are about eight million representable singles; between 16,777,216 and 16,777,218 there are none in between — the ticks are now 2 apart. Any real value gets snapped to the nearest tick; that snap is rounding, and the tiny gap it introduces is rounding error. Almost every float you'll ever meet is a nearest-tick approximation, not the true value.

And here's the honest part most introductions skip. In decimal, 1/3 has no finite representation — 0.333… forever. In binary the same fate hits some very innocent-looking decimals: 0.1 is 1/10, and since 10 has a factor of 5 (not a power of two), 0.1 in binary is 0.000110011001100… — repeating forever. A 32-bit float must cut it off after 24 significant bits, so what's actually stored is 0.100000001490116…. Close, but not 0.1. Every language, every chip, same story. Special bit patterns round out the format: two zeros (+0 and −0, the sign bit alone differing), infinities, NaN for invalid results like 0/0, and tiny denormals that fill the gap next to zero. File all of these away: the exact bits of rounding and NaNs are precisely where emulators bleed (Module 14).

Now go poke the format yourself. Every bit below is a button. Start from the presets — see how 1.0 is mostly zeros, why the biggest finite single is ~3.4 × 1038, what −0 and NaN look like — then click 0.1 and read the stored value's full decimal expansion. It isn't 0.1. It never was.

Float-anatomy lab — 32 bits you can flipIEEE-754 · live
sign exponent (bias 127) mantissa (implied leading 1)
Sign+
Exponent
Significand
Stored value
Note
Key takeaways
  • Binary counts in powers of two; two's complement wraps the pattern space so the same adder handles negatives.
  • A float is scientific notation in base 2: sign × 1.mantissa × 2exponent−127.
  • Singles (32-bit) trade precision for size and speed; doubles (64-bit) do the reverse. Games live on singles.
  • Most decimals — 0.1 included — cannot be stored exactly; every float is a rounded nearest-tick approximation.
Module 04 · Part I

The pipeline

GoalSee how overlapping the stages of many instructions multiplies speed — and why dependencies and branches keep stalling the works.

In Module 01 our toy CPU finished one instruction completely before starting the next: fetch, decode, execute, done, repeat. Real hardware noticed something wasteful about that. While an instruction is executing, the fetch circuitry is sitting idle — so why not have it fetch the next instruction at the same time? Push the idea to its conclusion and you get the pipeline: chop instruction processing into stages, and keep every stage busy with a different instruction at once.

Analogy · Laundry night

Washing a load takes an hour, drying takes an hour, folding takes an hour. Doing four loads strictly one-after-another takes twelve hours. But while load 1 is in the dryer, the washer is free — so start load 2. Overlap everything and four loads take six hours, and a new finished load emerges every hour once the pipeline fills. Nothing got faster — the dryer still takes an hour — but the throughput tripled. That's a pipeline: same work, staggered, so all the machines run at once.

The classic textbook pipeline has five stages, and the labels are worth memorising because every architecture book uses them: IF (instruction fetch), ID (decode and read registers), EX (execute in the ALU), MEM (touch memory, for loads and stores), WB (write the result back to a register). One instruction still takes five clock ticks to travel through — its latency — but a full pipeline completes one instruction every tick. Architects measure this as CPI: cycles per instruction. The dream is 1.0. (Real chips from the 750's era ran short pipelines of about four stages; modern desktop chips run fifteen-plus. Same idea, more stages.)

The dream dies two ways. First, a data hazard: if instruction 2 needs the result of instruction 1, but instruction 1 hasn't reached write-back yet, instruction 2 must wait. The pipeline inserts a do-nothing slot — a bubble — and the CPI creeps up. (Hardware shortcuts called forwarding paths hand results backwards between stages to shrink these waits.) Second, and worse, a control hazard: a branch. When a branch enters the pipe, the fetch stage faces an impossible question: which instruction comes next? The answer isn't known until the branch executes, several stages later — but fetch can't just stop and wait every time.

So the hardware gambles. It predicts which way the branch will go — even a simple “assume the loop repeats” heuristic is right most of the time, and real predictors track each branch's recent history — and keeps fetching along the guessed path. Guess right: no time lost at all. Guess wrong: everything fetched after the branch is wrong-path work and must be thrown away. That's a flush — the pipeline's contents behind the branch turn into bubbles, and fetch restarts from the correct address. The deeper the pipeline, the more work a flush wastes. This is why branch-heavy code runs slower than straight-line code, and (hold the thought) why Part III's JIT cares so much about chopping programs at branches.

cycle → 123456789 i1 add IF ID EX MEM WB i2 lwz IF ID EX MEM WB i3 beq ✈ IF ID EX! MEM WB i4 (wrong) IF i5 (wrong) t1 target IF ID EX MEM branch resolves in EX → i4 & i5 flushed two cycles of work thrown away = the misprediction penalty; a correct prediction would have fetched t1 immediately
A flush in slow motion. Instructions march diagonally through the five stages, one stage per cycle. The branch i3 (amber) is only resolved at EX in cycle 5 — by then the pipeline has already fetched two wrong-path instructions (red), which become bubbles. Fetch restarts at the branch target t1 (cyan).

The lab below is that diagram brought to life. A little program flows through five stages; every fifth instruction is a taken branch. With prediction off, each branch flushes the two instructions behind it and the CPI sags. Switch prediction on and the fetch stage guesses the target correctly — watch the bubbles vanish and the CPI settle back toward 1.0.

Pipeline lab — bubbles, branches & predictionsimulated · live
cycles 0 completed 0 CPI flushed 0
Key takeaways
  • A pipeline overlaps the stages of many instructions: same latency each, far higher throughput.
  • CPI (cycles per instruction) measures pipeline health; 1.0 is the simple-pipeline ideal.
  • Data hazards insert bubbles; branches are worse — the next fetch address isn't known yet.
  • Branch prediction keeps fetch busy on a guess; a wrong guess flushes the pipe and wastes the work.
Module 05 · Part I

Superscalar — more than one at a time

GoalUnderstand how a CPU issues two or more instructions per cycle to parallel execution units, and where the 750 family sits on the in-order/out-of-order spectrum.

The pipeline got us to one instruction per cycle. The next question is greedy and obvious: why not two? A superscalar processor fetches and decodes several instructions per cycle and — when they don't step on each other — issues them simultaneously to different execution units. An execution unit is a specialist circuit: an integer ALU, a floating-point unit, a load/store unit that talks to memory, a branch unit. They were always physically separate; superscalar design simply stops making them take turns.

Analogy · The supermarket checkout

One cashier, one queue: one customer per minute, however fast the cashier. Open a second till and — provided two customers are ready and independent — two leave per minute. But if customer B is paying with money customer A is about to hand them, B must wait whatever the till count. Superscalar issue is the second till; a data dependency is B waiting on A's change.

The catch is the same hazard logic from Module 04, multiplied. add r5, r3, r4 and mulli r6, r5, 3 cannot issue together — the multiply needs r5 first. Real code is a braid of such dependencies, so a 2-wide machine rarely sustains 2.0 instructions per cycle. How hard the hardware fights back defines a spectrum:

  • In-order machines issue instructions strictly in program order; if the next one must wait, everything behind it waits too. Simple, small, power-efficient.
  • Out-of-order machines keep a window of dozens (today, hundreds) of decoded instructions and issue whichever are ready, then use a re-order buffer to make the results appear in program order. Enormously effective, enormously expensive in silicon and watts.

The PowerPC 750 line — Gekko's ancestry — is a modest superscalar: it dispatches up to two instructions per cycle to six execution units (two integer units, a floating-point unit, a load/store unit, a system-register unit, and a branch unit), with only small out-of-order freedoms — a completion queue keeps retirement in program order. Its bet, a very deliberate one, was that a short pipeline plus good caches beats a deep, wide one for the branchy integer code real programs are made of: mispredictions (Module 04) cost only a few cycles instead of dozens. For 2001-era game code, it was a fine bet — and it kept the chip small, cool and cheap, which (Module 07) is exactly what Nintendo was shopping for.

Fetch & decode up to 2 instructions per cycle dispatch — if independent IU1integer IU2integer FPUfloats · Mod 08 LSUload / store SRUsystem regs BPUbranches · Mod 04 Completion queue results retire in program order
The 750's shape. A 2-wide front end dispatches independent instructions to six specialist execution units; a completion queue retires everything in program order, keeping the illusion of one-at-a-time execution intact.

One consequence matters enough to flag now: because issue width is precious, how you arrange instructions matters. Compilers interleave independent work so pairs can issue together. And if one instruction could do the work of two — say, operate on two floats at once — you'd effectively double your width for that code without adding a single issue slot. Hold that thought for exactly three modules.

Key takeaways
  • Superscalar = fetch, decode and issue 2+ instructions per cycle to parallel execution units.
  • Data dependencies, not unit count, are the real ceiling on parallelism.
  • In-order machines stall politely; out-of-order machines hunt for ready work at great silicon cost.
  • The 750 line is a modest 2-wide, mostly in-order design with six units — small, cool, short-pipelined.
Module 06 · Part I

Caches — hiding the memory wall

GoalSee why memory is the real bottleneck, and how caches exploit locality — lines, sets and ways — to hide it most of the time.

Here's the dirty secret of the last five modules: we've been pretending memory keeps up. It doesn't — not remotely. While CPUs got faster at a giddy rate through the '90s, memory latency barely moved; by the GameCube's day a processor could execute dozens of instructions in the time one read from main memory took to come back. This widening gap has a name — the memory wall — and without a fix, our beautiful pipeline would spend most of its life stalled at the load/store unit, waiting.

The fix rests on a lucky truth about programs, called locality, and it comes in two flavours. Temporal locality: if you touched a value recently, you'll probably touch it again soon (think of a loop counter). Spatial locality: if you touched an address, you'll probably touch its neighbours soon (think of walking an array). So the hardware keeps a small, fast copy of recently-used memory close to the CPU: a cache. Every load first checks the cache. Found there — a hit — the value arrives in a cycle or two. Not found — a miss — the hardware fetches it from main memory, keeps a copy, and the pipeline eats a stall of dozens of cycles.

Analogy · The librarian's desk

A librarian doesn't run to the stacks for every request. Books asked for recently stay in a small pile on the desk (temporal locality), and when a trip to the stacks does happen, the librarian brings back the whole shelf-section, because the next request is usually the book beside the last one (spatial locality). The desk is the cache. The pile's fixed size means keeping a new book evicts an old one — deciding which is the cache's whole art.

Lines, sets and ways

Three mechanisms turn that idea into hardware. First, caches move memory in fixed-size chunks called cache lines — 32 bytes on the 750 family — never single bytes. Miss on one byte and its 31 neighbours ride along free, which is spatial locality made mechanical. Second, each line can live in only a small number of places: the middle bits of its address pick a set (a slot group), and the top bits are stored alongside as a tag so the hardware can tell which line currently occupies the slot. A lookup is: index by the middle bits, compare the tag, hit or miss. Third, to soften the bad luck of two hot lines fighting over one slot — a conflict miss — real caches make each set hold several lines side by side; the number of side-by-side slots is the cache's ways. A cache where every set holds one line is direct-mapped; the 750's L1 caches are 8-way, meaning eight lines share each set and an LRU-style policy picks the victim on eviction.

A memory address, as the cache reads it tag — which line is this? index — which set? offset — which byte? middle bits pick the set the cache — 4 sets × 2 ways shown set 0 way 0 · tag + 32-byte line way 1 · tag + 32-byte line set 1 compare tag → hit? compare tag → hit? Main memory on a miss: fetch the whole 32-byte line miss → dozens of cycles
How a lookup works. The address's middle bits pick a set, the stored tags in that set are compared, and either it's a hit (a cycle or two) or a whole 32-byte line is fetched from main memory while the pipeline stalls. More ways per set means fewer conflict evictions.

Two last vocabulary items and you have the full kit. Caches themselves come in layers: a small, split L1 (one for instructions, one for data — so fetch and load never queue for the same door) and a larger, slower L2 behind it. And a miss isn't one thing: compulsory (first touch — nobody's seen this line yet), capacity (the working set is simply bigger than the cache), and conflict (two lines fighting over a set). The lab below makes all three visible: pick an access pattern, watch hits flash green and misses red, and — the key experiment — drag the line size up while running the sequential pattern and watch the hit rate soar as each miss drags more neighbours in. Then try strided and watch a pattern that defeats the cache entirely.

Cache lab — hits, misses & localitysimulated · live
Memory · 64 line-sized blocks
Cache · 8 lines, direct-mapped
hits 0 misses 0 hit rate

And with that, the Part I toolkit is complete: the loop, registers, number formats, the pipeline, superscalar issue, and caches. Everything in Part II is these six ideas — plus a handful of very deliberate Nintendo-flavoured modifications. Time to meet the chip.

Key takeaways
  • Main memory is dozens of CPU cycles away — the memory wall is the bottleneck that shapes everything.
  • Caches exploit temporal and spatial locality to answer most accesses in a cycle or two.
  • Data moves in 32-byte lines; sets and ways decide where a line may live and what gets evicted.
  • A hit is nearly free; a miss stalls the pipeline for dozens of cycles — hit rate is everything.
Part II

Gekko

Now we put the Part I toolkit into real silicon. Nintendo didn't design a CPU from scratch — it took a proven IBM desktop core and asked for a handful of surgical modifications aimed at one job: feeding a 3D game, sixty times a second. These six modules cover the chip itself and each modification in turn — paired singles, quantised loads, the write-gather pipe, the locked cache — then the unusual memory system the whole console hangs from. Every piece maps back to a fundamental you already know.

Modules 07–12Difficulty HardwareLabs 3 interactive
Module 07 · Part II

Meet Gekko

GoalMeet the actual chip — where it came from, its caches and clocks, and its place in the console next to Flipper.

Gekko is the GameCube's CPU: a custom IBM design derived from the PowerPC 750CXe — the same family Apple was shipping in Macs as the G3. It runs at 486 MHz (three times the 162 MHz system bus; Nintendo's own literature quotes 485), was fabricated on IBM's then-cutting-edge 0.18 µm process with copper interconnects — IBM's signature manufacturing advantage of that era, copper wiring between transistors instead of aluminium for lower resistance — and it embodies every Part I idea: the 4-ish-stage short pipeline (Module 04), 2-wide in-order superscalar issue to six units (Module 05), and a proper cache hierarchy (Module 06): 32 KB + 32 KB of split 8-way L1, with 256 KB of L2 on the die itself — a genuine luxury in 2001, when many CPUs still bolted L2 onto the motherboard.

Why start from an off-the-shelf desktop core at all? Because it's the sane engineering move. The 750 was proven, well-understood by compilers, cheap to manufacture, and — crucially for a small, fanless-quiet console box — small and cool: a short-pipeline, in-order design sips power compared to the deep, hungry designs of its contemporaries. What a desktop core lacked was game-specific muscle: 3D games spend their CPU time on streams of small floating-point vectors, and stock PowerPC processes those one float at a time. So Nintendo and IBM added a targeted set of roughly fifty new instructions — the paired-single family you'll meet in Modules 08 and 09 — plus the write-gather pipe (Module 10) and the locked cache (Module 11). Gekko is best understood as a sensible commuter car with a racing kit bolted on in exactly four places.

Analogy · The town around the station

Think of the console as a small town. Gekko is the workshop where all general thinking happens — but it is not the centre of town. The centre is Flipper, the companion chip: every road — to main memory, to the graphics hardware, to the audio DSP, to the disc drive and controllers — runs through it, like a central railway station. In PC terms Flipper is the northbridge (and the GPU, and more, all in one). When Gekko wants anything beyond its own caches, it asks Flipper.

GameCube system architecture around Gekko Gekko, with its L1 and L2 caches and paired-single FPU, connects over a 162 MHz front-side bus to Flipper. Flipper contains the graphics processor, memory controller, audio DSP and I/O, and connects outward to 24 MB of 1T-SRAM main memory, 16 MB of ARAM, and the video output. Gekko PowerPC 750CXe core · 486 MHz L1 I 32 KB L1 D 32 KB · Mod 11 L2 · 256 KB on-die 2nd chance before the bus FPU + paired singles 2 floats per op · Mod 08 0.18 µm · copper interconnect front-side bus 162 MHz · 64-bit · ~1.3 GB/s Flipper the northbridge — and much more GX graphics Memory ctrl all RAM traffic Audio DSP see the audio course I/O disc · pads · cards Embedded 1T-SRAM framebuffer + texture cache, ~3 MB GX FIFO commands arrive here — Mod 10 Main RAM 24 MB 1T-SRAM · ~10 ns “Splash” · Mod 12 ARAM 16 MB · slower · DMA only Video out to the TV
The whole system at a glance. Gekko talks to the world through one 64-bit, 162 MHz front-side bus into Flipper, which owns main memory, graphics, audio and I/O. Note what this means: every single cache miss from Module 06 crosses that bus. The audio DSP block has a whole companion course of its own.
Core
750CXe derivative
IBM PowerPC, custom-modified
Clock
486 MHz
3 × the 162 MHz bus clock
L1 caches
32+32 KB
Split I/D · 8-way · 32-byte lines
L2 cache
256 KB
On-die
Issue width
2/cycle
Six execution units, in-order retire
Process
0.18 µm
IBM copper interconnect

One number in that grid deserves a beat of silence: 486 MHz. The PlayStation 2's CPU clocked lower, the original Xbox's Pentium III higher — but raw clocks mislead. What made Gekko effective was the system around it: unusually low-latency main memory (Module 12) meant its cache misses hurt less, and the four custom modifications meant the hot loops of a 3D game — transform the vertices, build the command stream, keep the data flowing — each got a dedicated fast path. The next four modules take those four modifications one at a time.

Key takeaways
  • Gekko is an IBM PowerPC 750CXe derivative at 486 MHz — 0.18 µm, copper interconnects, small and cool.
  • Caches: 32 KB + 32 KB split 8-way L1, 256 KB on-die L2.
  • It reaches everything through a 162 MHz front-side bus into Flipper — the northbridge, GPU, DSP and I/O hub in one.
  • Nintendo chose a proven off-the-shelf core and added ~50 game-oriented instructions rather than design from scratch.
Module 08 · Part II

Paired singles — two floats per instruction

GoalUnderstand Gekko's headline modification: splitting each 64-bit float register into two 32-bit singles and operating on both at once.

Time for the racing kit. Recall two facts you already own: PowerPC's floating-point registers f0f31 are 64 bits wide, sized for doubles (Module 02) — and games overwhelmingly compute in 32-bit singles (Module 03). Which means that when a game uses those registers, half of every register is dead weight. Gekko's designers looked at that wasted half and asked the obvious question: what if each 64-bit register could instead hold two singles, and one instruction could operate on both at once?

That's paired singles. In paired-single mode a register like f1 is treated as two independent lanes, called ps0 (the top half) and ps1 (the bottom half). A new family of instructions — ps_add, ps_sub, ps_mul, ps_madd (multiply-add), ps_sum0, ps_merge and friends — performs the same arithmetic on both lanes simultaneously: ps_add f3, f1, f2 computes two sums, lane by lane, in the time the plain FPU computes one. One instruction, two results. This idea has a general name — SIMD — and Gekko's 2-wide version is often called SIMD-lite: narrower than the 4-wide SSE its PC contemporaries were growing, but cheap in silicon and aimed with a sniper's precision, as we're about to see.

Analogy · The double-bladed pasta cutter

A chef slicing pasta one cut at a time is the plain FPU. Gekko hands the chef a two-bladed cutter: one stroke, two parallel cuts. It only helps when you want two identical cuts — but pasta, like game maths, is nothing but identical cuts in a row. And note what didn't change: the chef, the arm, the speed of the stroke. Same clock, same pipeline — paired singles simply do twice the work per stroke.

Why is this a sniper shot and not a gimmick? Because of what 3D game code is. A frame of gameplay is thousands of small vector operations: every position, velocity and surface direction is a vec3 — an (x, y, z) triple of singles — and the engine's inner loops transform, add, scale and dot-product armies of them, identically, every frame. Skinning a character, animating particles, culling objects against the camera — all vec3 streams. Pair x and y in one register (z rides in another lane, or pairs with the next vertex's x) and a transform loop's multiply-adds — ps_madd, one multiply plus one add per lane, the fundamental atom of matrix maths — halve in count. In hot loops that approaches a clean 2× — an entire second issue slot's worth of width (Module 05), bought for a fraction of the silicon.

One 64-bit floating-point register, two personalities ps0 — a 32-bit single ps1 — another 32-bit single f1 as a double: one 64-bit value · f1 as paired singles: two independent lanes ps_add f3, f1, f2 — one instruction, two additions f1.ps0 = 1.5 f1.ps1 = 10.0 f2.ps0 = 2.25 f2.ps1 = −4.0 + + f3.ps0 = 3.75 f3.ps1 = 6.0 lanes never mix (unless you ask — ps_merge, ps_sum0)
The register split. In paired-single mode each 64-bit FPR carries two 32-bit lanes, ps0 and ps1. ps_add adds lane-to-lane in parallel; cross-lane instructions like ps_merge and ps_sum0 shuffle and combine lanes when a dot product needs to fold them together.
Instruction familyEffect (per lane)Typical use
ps_add · ps_sub · ps_mularithmetic on ps0 and ps1 in parallelvector add/scale — particles, velocities
ps_madd familymultiply-add, both lanes at oncematrix × vertex — the transform loop's atom
ps_sum0 · ps_mergefold or shuffle lanes across registersfinishing dot products; repacking x/y/z streams
psq_l · psq_stquantised load/store into both lanesModule 09 — this one gets a whole module

Instruction names as used in Dolphin and the homebrew toolchains; we won't reproduce encodings here — the shapes are what matter.

The lab makes the width visible. A cloud of points spins on screen, but the simulated CPU only has a fixed budget of float operations per frame — never enough for every point. In scalar mode each operation updates one value and the budget runs dry halfway through the cloud; stale points lag behind and the motion smears. Switch to paired and every operation moves two values: same budget, twice the points per frame, and the cloud snaps into smooth, coherent rotation. That difference is exactly what Gekko bought.

Paired-singles lab — one op budget, two widthssimulated · live
op budget/frame points updated/frame
Key takeaways
  • Games compute in 32-bit singles, so PowerPC's 64-bit FPRs were half-empty — Gekko fills them with two lanes, ps0 and ps1.
  • ps_* instructions (ps_add, ps_mul, ps_madd…) operate on both lanes at once: 2-wide SIMD, “SIMD-lite”.
  • 3D games are streams of vec3 maths, so the doubling lands exactly on the hottest loops.
  • Same clock, same pipeline — the win is pure width, like a second issue slot for float code.
Module 09 · Part II

Quantised loads & stores

GoalSee how psq_l and the Graphics Quantisation Registers stream compressed 8/16-bit fixed-point data straight into paired singles, scaling on the fly.

Paired singles doubled the arithmetic. But Module 06 taught you to ask the next question reflexively: can memory keep up? A vertex position is three singles — 12 bytes. A detailed character mesh has tens of thousands of vertices, each with positions, normals, texture coordinates… stored naively as floats, that's megabytes of vertex data marching through a 32 KB data cache and a 162 MHz bus, every frame. The arithmetic is no longer the bottleneck; the bytes are.

The fix comes straight from Module 03. A float's luxury is its enormous range — but a vertex coordinate doesn't need range. A character model fits in a box a few units wide; a texture coordinate lives between 0 and 1. Within a known, small range you can store a value as a small integer with an agreed scale — a fixed-point number. Pick a scale of 26 = 64: then the byte 36 means 36/64 = 0.5625. An 8-bit integer plus an implicit scale replaces a 4-byte float — a 4× saving; 16-bit halves it instead, with 256× finer steps. The mesh's precision drops, but within a small range, 8 or 16 bits of steps is routinely below what your eye could ever notice.

Any CPU can decompress such data — load byte, convert to float, multiply by 1/64 — but that's three-plus instructions per value, burning the very cycles paired singles just saved. Gekko instead does it in the load itself. The instruction psq_l (paired-single quantised load) reads two small integers from memory, converts both to floats, scales both by a power of two, and lands them in ps0 and ps1 of a register — one instruction, two decompressed values. Its mirror psq_st quantises and packs on the way out.

Where do the format and scale live? Not in the instruction — in eight dedicated Graphics Quantisation Registers (GQRs), each holding a type (signed/unsigned 8- or 16-bit, or plain float) and a scale exponent, for loads and stores separately. A psq_l names which GQR to obey. Eight of them means a game can leave them parked on its favourite formats — positions in one, normals in another, texture coordinates in a third — and stream everything through the right converter with zero setup in the hot loop.

Analogy · Flat-pack furniture

Shipping assembled wardrobes wastes the lorry — you're paying to transport air. Flat-pack them and one lorry carries four times as many; you assemble at the door. Quantised vertex data is flat-packed: compact in memory and across the bus (the lorry), expanded to full floats only on arrival at the register file. The GQR is the assembly sheet taped to the box: what this is, and how to unfold it.

Main memory 0x24 0x1B two s8 integers — 2 bytes total psq_l Dequantise — in hardware integer → float, then × 1 / 2⁶ both values, same cycle-cost as a load GQR2 · type = s8 · scale = 6 one of 8 — the assembly sheet Register f1 0.5625 0.4219 ps0 ps1
One instruction, two decompressions. psq_l pulls two packed integers from memory, and the dequantise hardware — steered by a GQR's type and scale — delivers ready-to-use singles into ps0/ps1. Here: 0x24 = 36 → 36/64 = 0.5625, 0x1B = 27 → 27/64 ≈ 0.4219. The reverse trip is psq_st.

The cost of all this is the one Module 03 taught you: the stored steps are coarse, so every value snaps to the nearest representable tick. The lab lets you feel exactly how coarse. Pick a coordinate value and watch its stored bits, reconstructed value and error change as you switch 8↔16 bits and slide the scale. The curve below it is a whole strip of vertices quantised with your settings — at 8 bits and a low scale you can see the wobble that quantisation injects into geometry; at 16 bits it vanishes below the pixel grid. Notice the trade the GQR scale makes: more scale = finer steps but a smaller representable range, the fixed-point bargain in one slider.

Quantisation lab — a vertex through the GQRsimulated · live
Stored bits
Stored integer
Reconstructed
Error
Memory
Key takeaways
  • Vertex data lives compressed — 8/16-bit fixed point — because bus bytes and cache lines are scarcer than arithmetic.
  • psq_l / psq_st convert and scale during the load/store itself: two values per instruction, free decompression.
  • Eight GQRs hold the recipes (type + power-of-two scale), so hot loops never stop to reconfigure.
  • The price is quantisation error — the Module 03 rounding story, now applied to geometry.
Module 10 · Part II

Feeding Flipper — the write-gather pipe

GoalFollow the draw commands from CPU to GPU: why ordinary stores are the wrong tool, and how the write-gather pipe turns a dribble of writes into 32-byte bursts.

The vertices are transformed (Module 08) and unpacked (Module 09). Now Gekko has to tell Flipper what to draw, and it does so the way all GPUs are fed: by writing a long stream of small commands — “set this state, here's a vertex, here's another” — into a queue in memory called the GX FIFO (FIFO = first in, first out; GX is the GameCube's graphics API). Flipper's command processor chews through the queue at its own pace while the CPU races ahead. Simple. Except: how should the CPU write it?

Both obvious answers are wrong. Write through the data cache and you poison it — the command stream is thousands of bytes you will never read back, evicting the game's actual working set (Module 06), and the bytes only reach Flipper when lines happen to flush. Mark the region uncached and each little 4-byte store becomes its own lonely trip across the 162 MHz bus — correct, but a hundred dribbles where one pour would do.

Gekko's third way is the write-gather pipe. The game points a special register — the write-gather pipe address register, WPAR — at one magic, uncached address. Every store the program makes to that same single address doesn't go to memory at all: the pipe swallows it into a small on-chip gather buffer, and each time 32 bytes accumulate — one full cache line's worth, no coincidence — the whole block leaves as a single burst transaction to the FIFO. The programming model is almost comic: the entire graphics stream of a GameCube frame is written to one address, over and over, and the hardware turns that dribble into a firehose running at close to the bus's full bandwidth.

Analogy · The postal crate

Driving to the depot with every single letter is madness — the journey costs the same whether the van carries one letter or a crate. So you drop letters in a crate by the door and the van leaves only when the crate is full. The write-gather pipe is the crate; the 32-byte burst is the full van; the bus makes one trip instead of eight.

Gekko stores to WPAR's one magic address 4-byte writes gather buffer — 32 bytes fills left to right · leaves only when full 32-byte burst GX FIFO command queue in main memory Flipper CP consumes at its own pace
Dribble in, firehose out. The program stores 4-byte commands to a single uncached address; the gather buffer packs them into 32-byte lines and bursts each full line into the GX FIFO, which Flipper drains asynchronously. CPU and GPU run decoupled, at full bus efficiency.

The rest of the cache-control toolbox

The write-gather pipe has cousins — ordinary PowerPC cache-control instructions that GameCube games leaned on unusually hard, because a console lets you schedule memory like a freight line:

  • dcbzdata cache block zero: claim a 32-byte line in the cache and fill it with zeros, without reading memory first. About to overwrite a whole line anyway? This skips the pointless fetch-before-write — a favourite for clearing and building buffers at cache speed.
  • dcbttouch: a polite hint to start fetching a line now because you'll want it soon — software prefetching for loops that know their future.
  • dcbf / dcbst — flush or write back a line, used when handing a buffer to hardware (like that FIFO) that reads real memory, not your cache.
  • icbi — invalidate an instruction-cache line, mandatory after writing new code into memory. File this one away carefully: it is a load-bearing plot point in Module 14.
Key takeaways
  • The CPU feeds Flipper by writing a command stream into the GX FIFO — a queue in main memory.
  • Cached writes poison the cache; plain uncached writes waste the bus one dribble at a time.
  • The write-gather pipe collects stores to one magic address (via WPAR) into 32-byte bursts — near-full bus efficiency with a trivial programming model.
  • dcbz, dcbt, dcbf and icbi give games manual control over cache traffic — and icbi returns in Part III.
Module 11 · Part II

The locked cache & DMA

GoalUnderstand Gekko's fourth trick: turning half the data cache into a private scratchpad with its own DMA engine — and why predictable beats fast-on-average.

Module 06 sold you the cache as a marvel, so here's its dark side: a cache is fast on average and unpredictable in the particular. Any individual load might hit (a couple of cycles) or miss (dozens), and which one you get depends on everything that touched memory before — including hardware you don't control. For most software, who cares; the average is what matters. But a game is a real-time system: it must finish every frame in 16.7 ms, and a physics routine that usually takes 1 ms but occasionally takes 3 is a stutter your thumbs can feel. For the innermost loops, engine programmers would happily trade a brilliant average for a boring, dependable worst case.

Gekko sells exactly that trade. Flip a bit in a hardware configuration register and the 32 KB data cache splits in half: 16 KB keeps being a normal cache, and the other 16 KB becomes a locked cache — a scratchpad mapped at a fixed address range. “Locked” means the cache machinery keeps its hands off: nothing is ever evicted, nothing ever misses, because it isn't caching anything — it's simply 16 KB of guaranteed-fast memory that belongs entirely to the program. Every access, every time, costs the same couple of cycles. (A dedicated dcbz_l instruction zero-claims lines inside it, cousin of Module 10's dcbz.)

Of course, 16 KB is tiny — the point is to stream work through it, and that's the second half of the design: a dedicated DMA engine that copies blocks between the locked cache and main memory by itself, in the background, while the CPU computes. The rhythm it enables is double-buffering: while the CPU crunches batch N in one half of the scratchpad, the DMA engine is simultaneously writing out batch N−1's results and pulling in batch N+1. Done right, the CPU never touches main memory at all in the hot loop — no misses, no stalls, no variance. Animation skinning, particle systems, sound mixing on the CPU: classic locked-cache customers.

Analogy · Mise en place

A chef mid-service never walks to the cellar. An assistant stages the next dish's ingredients at the counter while the current dish is cooking, and clears the finished plates away. The counter is the locked cache — small, close, always two-seconds away; the assistant is the DMA engine; the cellar is main memory. The chef's pace becomes perfectly steady — not because the cellar got closer, but because someone else now does the walking, during the cooking.

Gekko core the 32 KB data cache, split by one HID2 bit 16 KB · normal cache tags, sets, misses — Module 06 as usual 16 KB · locked scratchpad no tags, no misses — always ~2 cycles normal loads/stores fixed-address loads/stores locked-cache DMA engine block copies, in the background Main RAM (via Flipper) batches staged in · results out while the CPU computes
Half cache, half counter-top. One configuration bit re-purposes 16 KB of the D-cache as a directly-addressed scratchpad; a private DMA engine streams batches between it and main memory in the background. The CPU's inner loop touches only guaranteed-fast memory.

The lab races the two philosophies over one frame's worth of work, eight batches long. The top lane computes straight from the normal cache: every batch gets interrupted by whatever misses fate deals out (slide the miss-chance to change fate). The bottom lane computes from the scratchpad while DMA prefetches the next batch underneath. Watch the red stall slices — and notice something subtler than the finish time: replay it a few times. The top lane's finish moves; the bottom one lands on the same spot, every single run. That repeatability is the real product.

Locked-cache lab — cache luck vs DMA rhythmsimulated · live
through cache locked + DMA time lost to stalls
Key takeaways
  • Caches are fast on average but unpredictable per-access; real-time frames care about the worst case.
  • Gekko can lock half the 32 KB D-cache as a 16 KB scratchpad: fixed addresses, constant latency, zero misses.
  • A dedicated DMA engine streams batches between scratchpad and main RAM while the CPU computes.
  • The prize isn't just speed — it's determinism: the same loop takes the same time, every frame.
Module 12 · Part II

The memory system

GoalMap the memory the whole console shares: 24 MB of unusually fast 1T-SRAM, 16 MB of slower ARAM, and how everything hangs off Flipper.

We've spent five modules teaching Gekko to avoid memory. Time to be fair to the memory — because the GameCube's is genuinely unusual. Its 24 MB of main RAM, code-named “Splash”, is 1T-SRAM from a company called MoSys — a clever hybrid that stores bits in dense DRAM-style cells but hides all the usual DRAM ceremony (row opens, refresh pauses) behind SRAM-like interface circuitry. The result is main memory with about 10 ns latency — roughly five Gekko clocks — where contemporary PC DRAM of the era, with its row and bank management, could keep a CPU waiting several times longer, and less predictably. Sound familiar? It's the Module 11 value again: Nintendo consistently bought low, boring latency over headline bandwidth. A cache miss on GameCube simply hurts less than it did on its rivals.

Beside it sits an oddity: 16 MB of ARAM (auxiliary RAM) — plain, cheaper, slower DRAM hanging off Flipper on its own narrow interface. Gekko cannot address it directly: no load or store ever touches ARAM. Instead, blocks move between ARAM and main memory by DMA through Flipper, and the audio DSP streams instrument samples from it (that story is told properly in the companion audio course). Games treat it as a staging attic: audio banks live there, and clever engines park level chunks, animation data and pre-loaded assets in it — anything worth keeping closer than the optical disc but not worth precious Splash space. The disc, for scale, streams in at a few MB/s with seek times measured in tens of milliseconds: a different world, which is why ARAM earns its keep.

Analogy · Pantry, cellar, market

The kitchen's pantry (main RAM) is small but two steps away — anything in it is essentially at hand. The cellar (ARAM) holds more, but fetching from it is a planned trip — you send someone (DMA) and keep cooking meanwhile. And the market (the disc) has everything, once a day, if you queue. A good kitchen — and a good game engine — is mostly logistics between the three.

The full ladder — every step ~bigger and slower bar length ≈ how long one access keeps the CPU waiting registers 0 cycles · 32+32 slots · Module 02 L1 · 64 KB a couple of cycles · split I/D (± 16 KB locked — Module 11) L2 · 256 KB ~a dozen cycles · on-die Splash · 24 MB 1T-SRAM · ~10 ns — a few dozen cycles round-trip over the bus ARAM · 16 MB DMA only — plan the trip, overlap the wait disc · 1.5 GB tens of milliseconds to seek — millions of CPU cycles; everything above exists to keep the CPU from ever waiting on it
The memory ladder. Each rung trades speed for size. Gekko's four custom tricks are all rung-management: paired singles and quantised loads shrink traffic on the middle rungs, the write-gather pipe batches trips across the bus, and the locked cache turns the top rung's luck into a guarantee. Note the scale is mercifully not linear — the disc bar would otherwise be several kilometres long.
Main RAM
24 MB
“Splash” · MoSys 1T-SRAM · ~10 ns
ARAM
16 MB
Auxiliary DRAM · DMA access only
In Flipper
~3 MB
Embedded 1T-SRAM: framebuffer + texture cache
Bus to CPU
162 MHz
64-bit FSB · ~1.3 GB/s peak

Step back and admire the whole Part II machine at once. A frame of a GameCube game is: psq_l streams compressed vertices from Splash through the cache (09), paired singles transform them two floats at a time (08), the write-gather pipe fires the results into the GX FIFO for Flipper (10), the locked cache keeps physics and animation running on rails (11), DMA shuttles the next second of audio out of ARAM (12) — and underneath it all, the plain old fetch–decode–execute loop from Module 01 is just… running, 486 million times a second. Nothing mystical was added; every trick is a Part I idea, sharpened. Which sets up Part III's question perfectly: if it's all just the PowerPC contract plus four tricks — can a PC pretend to be it?

Key takeaways
  • Main memory is 24 MB of MoSys 1T-SRAM (“Splash”) at ~10 ns — unusually low, predictable latency for its day.
  • 16 MB of slower ARAM hangs off Flipper, reachable only by DMA — audio samples and staged assets live there.
  • Flipper is the hub: memory controller, GPU, DSP and I/O; all of Gekko's traffic crosses one 162 MHz bus.
  • Every Gekko modification is memory-ladder logistics — the console's consistent bet was predictability over peak numbers.
Part III

Emulating it in Dolphin

Finally: how does software on your PC stand in for that chip? Your processor speaks x86-64 or ARM64 — it cannot run a single PowerPC instruction. Dolphin's answer is to translate, and the way it translates — an interpreter for correctness, a just-in-time compiler for speed — is one of emulation's great stories. These three modules cover the machinery, the traps where “fast” and “accurate” part company, and the actual settings that map to everything you've learned.

Modules 13–15Difficulty EmulationLabs 1 interactive
Module 13 · Part III

Interpreters vs JITs

GoalUnderstand the two ways to run one CPU's code on another — re-decode every instruction forever, or translate each block once — and why the JIT wins by amortisation.

Strip the problem to its core: Dolphin holds the game's memory in a big array, and in that array sit PowerPC instructions — numbers, as Module 01 taught you. Your CPU can't execute them. The direct solution is an interpreter: a software re-enactment of the fetch–decode–execute loop. Keep a variable for the PC and one for each of Gekko's registers; loop forever: read the word at PC, look at its opcode bits, jump to the matching handler (“this is an add: sum these two array slots”), bump PC, repeat. It's Module 01's toy lab, scaled up — Dolphin genuinely contains one, and it is the reference: nearly perfectly faithful, easy to reason about… and brutally slow.

How slow? Count what one emulated add costs: fetch the word from the memory array, mask out the opcode bits, branch through a dispatch table (a branch your host's predictor hates — Module 04 again, one level up!), load two virtual registers, add, store the result, update the virtual PC, check for interrupts, loop. Ten to fifty host instructions for one guest instruction — and here's the crime — the same ten to fifty again the next million times that same add runs. A game loop is the same few hundred instructions repeated all frame. An interpreter re-derives the same conclusions about the same bytes, forever.

The fix is to stop re-deriving: translate once, remember, reuse. That's a JIT — a just-in-time compiler. The natural unit of translation is the basic block: a straight run of guest code from one branch target to the next branch (Module 04 taught you why branches are the natural seams — they're the only places control can leave). First time execution reaches a block, Dolphin translates its PowerPC instructions into equivalent x86-64 (or ARM64) machine code, writes that into a buffer, and stashes a pointer in the block cache, keyed by guest address. Every later visit is a lookup and a jump: the loop that cost 50× interpreter overhead now runs as a handful of native instructions — often just 2–5× slower than truly native code, hundreds of times faster than interpreting.

Analogy · The two translators

An interpreter is a live translator at a speech that's read out every night: each night they translate the same sentences again, from scratch, at speaking pace. A JIT writes the translation down the first night. Night one is actually slower — translating and writing carefully takes longer than just speaking — but every night after, they simply read. By night three they've won; by night ten thousand (a game loop's real repeat count) it's no contest.

PC arrives at a guest address say 0x80003100 — top of a loop Block cache lookup seen this address before? hit — usual case Jump into native code runs at host speed miss — first visit only Translate the basic block PPC → x86-64 / ARM64, to the next branch Store in block cache translated exactly once block ends at a branch → next address the interpreter, for contrast fetch → mask → dispatch → execute handler → bump PC → check interrupts → repeat every instruction, every time
Translate once, run forever. The JIT's steady state is the cyan path: look up, jump, run native, repeat. The violet compile path runs once per basic block — a one-off toll amortised over millions of executions. The interpreter (left) pays full price on every single instruction.

Two refinements complete the picture. First, there's a middle rung: a cached interpreter, which decodes each block once into a friendly internal list (skipping the re-decode cost) but still executes it by dispatching — faster than plain interpretation, far slower than a true JIT, and useful when the JIT itself is the thing you're debugging. Second, the block cache is only valid while the guest code doesn't change. Games load code overlays from disc, decompress engines into RAM, even patch themselves — and the moment those bytes change, a cached translation of the old bytes is a lie. The JIT must detect writes to translated code and invalidate the affected blocks — remember icbi from Module 10? That instruction is the guest politely announcing “I changed the code here” — and the next visit recompiles. Getting this wrong doesn't slow the emulator; it makes it wrong, which is Module 14's whole subject.

The lab races the two strategies over a hot loop. Watch the JIT lane pay its violet compile toll up front — at low repeat counts it actually loses — then drag the run-count slider up and watch amortisation do its work. The block-cache chip lights up the moment the translation exists.

JIT lab — one hot loop, two strategiessimulated · live
Interpreter0 inst/s
0 instructions executed · re-decodes every one, every pass
JIT recompilerblock cache · empty0 inst/s
0 instructions executed · compiles the block once, then runs native
Striped violet = one-off compile cost. Try 10 runs, then 3000.
Key takeaways
  • An interpreter re-runs fetch–decode–execute in software: faithful, simple, 10–50 host instructions per guest one.
  • A JIT translates each basic block to native code once, caches it, and reuses it — amortisation is the whole trick.
  • Basic blocks end at branches; the block cache maps guest addresses to translations.
  • When guest code changes (overlays, self-patching, icbi), stale blocks must be invalidated or the emulator lies.
Module 14 · Part III

The accuracy traps

GoalTour the places where a fast emulator and a truthful one pull apart: floating-point bits, timing, self-modifying code, and the MMU.

If Module 13 made JITs sound like a solved problem, this module is the cold shower. Translating an add is easy. The hard part is everything a real Gekko does implicitly — the exact float bits, the passage of time, the caches, the address translation — which software must now reproduce explicitly, and every explicit check costs cycles in the hottest loop on the machine. Accuracy isn't one dial; it's a series of specific traps, and each one is a place where Dolphin's developers had to choose speed, truth, or some painstaking third path that buys both.

Trap 1 · Floating point is a bit-exact contract

Module 03 warned that every float is a rounded approximation. Here's the sting: which approximation is part of the ISA contract, down to the last bit — and Gekko's paired singles are a custom extension with their own personality. Their operations round to single precision at each step, while a straight x86 translation might compute in double and round once — same maths, different final bit. ps_madd's fused-or-not rounding behaviour, the exact bit pattern a NaN propagates, denormal handling, the guest's rounding-mode flags: each can differ subtly from the host's native habits. Does one wrong low bit matter? In a screenshot, never. In a physics engine run 60 times a second for an hour — where this frame's output is next frame's input — divergence compounds: speedrun replays desync, and a famously fragile few games misbehave. Dolphin's JIT therefore massages float code with extra host instructions to force Gekko-truthful rounding — a real, measured cost paid for invisible correctness.

Trap 2 · Time is part of the machine

A real Gekko takes real time: 486 million cycles per second, no more, no less, and games are written against that meter. Consider the idle loop: when a frame's work is done early, the game literally spins — branch-to-self — until the video hardware signals the next frame. Run that “as fast as possible” and the emulator melts a host core doing nothing; so Dolphin detects idle spins and skips ahead in virtual time. But virtual time must stay coherent: game logic paced against the CPU clock, video interrupts, DMA completion, the audio DSP — all advance on one shared timeline, and code that finishes “too fast” relative to it can break assumptions the original developers never knew they were making. This is why an emulator can't simply hand the game more speed: correctness is keeping the clocks in step, and speed is only welcome where no one can observe it.

Trap 3 · Code that rewrites itself

Module 13 flagged it; here's why it's treacherous. On real hardware, writing to code memory just works — the game flushes with dcbf/icbi (Module 10) and the icache refills. The hardware doesn't need to notice writes; the JIT does, or it keeps executing a ghost. Dolphin leans on the guest's own icbi etiquette and on tracking writes to pages holding translated code — but games load overlays, unpack compressed code, and a few scribble over themselves with abandon. Miss one write and the symptom appears minutes later, somewhere else entirely: the classic hardest-bug-in-emulation.

Trap 4 · The MMU, or: which memory is that, exactly?

One piece of real hardware we've politely ignored: the MMU, which sits between every address a program uses and the physical RAM it lands in. Most GameCube games use PowerPC's simple block-mapping mode — a handful of big fixed windows onto the 24 MB — which an emulator can translate with one fast mask. Dolphin's fast path builds on that, backed by clever use of host page-fault machinery to catch the exceptions. But a minority of titles genuinely exercise page-granular mappings and expect precise page faults — memory exceptions delivered at exactly the right instruction — and for those, Dolphin offers a per-game full-MMU mode: more checks on every memory access, slower, and for a stubborn shortlist of games, the difference between booting and not.

Analogy · The flight simulator's checkride

A flight simulator that only looks right is a video game; one that a licensing examiner accepts must match the aircraft's numbers — fuel burn, stall speed, instrument lag — because pilots build reflexes against the real machine's behaviour, not its appearance. Games are the same pilots: a million accidental dependencies on the exact float bits, the exact cycle counts, the exact fault timing. Fast gets the plane in the air; accurate is what keeps two decades of software from noticing it ever changed aircraft.

Game code runs loads a new level's engine writes new code guest RAM at 0x80280000 old bytes (dead) NEW code — just written the guest runs icbi to flush its icache Dolphin's block cache 0x80280000 → x86 blob translation of the OLD bytes …thousands of live blocks… stale! the fix: detect the write (icbi + write tracking) → invalidate → recompile on next visit miss it, and the emulator keeps running code the game already replaced
The stale-block hazard. The guest replaced its own code, but the block cache still maps that address to a translation of the old bytes. Real hardware can't make this mistake — its icache is hardware-coherent-enough with the icbi etiquette. A JIT must reproduce that vigilance in software, on every write, forever.

Why not just always be maximally accurate? Because every trap above taxes the innermost loop. Bit-true float rounding adds host instructions to every ps_* op; full MMU adds checks to every load and store; paranoid write-tracking taxes every store. Each is a few percent — together they can halve the frame rate. Dolphin's art is defaults that are truthful where games can tell and fast where they can't, with per-game overrides for the stubborn exceptions. That's the exact trade you'll set with your own hands in the final module.

Key takeaways
  • Float accuracy is bit-exactness: paired-single rounding, NaN bits and mode flags must match, or divergence compounds frame over frame.
  • Time is architectural: idle loops can be skipped, but all virtual clocks must advance in step.
  • Self-modifying code makes cached translations lie; icbi etiquette plus write tracking is the JIT's lifeline.
  • Most games live happily on fast block-mapped memory; a few demand full MMU emulation with precise faults.
Module 15 · Part III

Try it in Dolphin

GoalTurn the whole course into settings you can toggle and A/B for yourself — then close the loop back to Module 01.

You now have the full picture, so let's make it hands-on. Dolphin's CPU-related options live in its general and advanced configuration (plus per-game property overrides); names shift slightly between versions, so we'll describe each control by what it does — which, pleasantly, you can now explain from first principles:

  • CPU emulation engine → JIT recompiler — the default, and Module 13's whole argument: translate each basic block once, run native ever after. There's one per host architecture (x86-64, ARM64). Leave it here unless you have a reason.
  • → Cached interpreter — the middle rung: decode once, dispatch forever. A handy A/B point when you suspect the JIT itself of a bug — same timing model, none of the code generation.
  • → Interpreter — the reference re-enactment. Wonderful for developers bisecting a miscompiled instruction; unplayably slow for actual games. Running it once, briefly, is the best visceral proof of Module 13 you'll ever get.
  • Clock override — a slider that changes the virtual CPU clock away from 486 MHz. Underclock and honest games drop frames; overclock and games whose logic is paced by frame interrupts stay identical while ones that busy-wait or self-pace can speed up, glitch — or fix a slowdown the real console always had. It's Module 14's “time is architectural” lesson with a physical handle.
  • MMU emulation (per-game) — full page-granular address translation with precise faults, for the short list of titles that demand it. Costs speed on every access; games that need it won't boot honestly without it.
  • Dual core / synchronise-GPU-style options — run the emulated CPU and GPU on separate host threads (fast, slightly loose timing) or tightly synchronised (slower, deterministic). Exactly the Module 14 trade: speed where games can't observe it, truth where they can.
Analogy · Behind the mixing desk

Before this course, Dolphin's settings panel was a wall of unlabelled faders — you moved one and hoped. Now you've been behind the desk and traced every wire: the engine dropdown is Module 13's interpreter-to-JIT ladder, the clock slider is Module 14's virtual time, the MMU box is trap four. Settings stop being superstition the moment you know what each fader is soldered to — the diagram below is simply that wiring, read in the order a real troubleshooting session walks it.

Pick a CPU engine Game runs correctly & fast? the overwhelmingly common case yes JIT recompiler the default — done no Won't boot / corrupts memory? check the wiki: MMU list, per-game INI try Enable MMU slower · precise faults hunting an emulator bug? Cached interpreter → interpreter slower and more faithful, one rung at a time
Which engine? Stay on the JIT unless something's wrong. Misbehaving titles may need per-game MMU emulation; emulator debugging walks down the accuracy ladder to the interpreter.

Three experiments worth actually running

  • Feel the interpreter. Take any game, switch the engine to interpreter, and watch the frame rate collapse — then flip back to JIT. That difference is Module 13: you just toggled amortisation on and off.
  • Bend time. Set the clock override to 150%, then 60%, in a game with a busy pause menu. Some things speed up, some don't — sorting which is a live lesson in what's paced by the CPU clock versus the frame interrupt (Module 14).
  • Read a JIT block for real. Dolphin ships a debugger with a JIT view showing guest PowerPC beside emitted host code. Find a loop, and you'll recognise everything: the basic block, the boundary branch, even ps_madd sequences from Module 08 — the whole course, on one screen.

And that's the course. You began with a cook and a stack of numbered cards; you end able to read a Gekko datasheet, explain why half a cache became a scratchpad, and predict which Dolphin checkbox fixes which symptom. The fetch–decode–execute loop is still down there, under everything, turning — on a 2001 games console, on the machine you're reading this with, and in the emulator faithfully pretending to be one on the other.

Course complete ✦
  • You can trace a frame end-to-end: quantised loads → paired-single maths → write-gather pipe → Flipper — with the locked cache keeping time.
  • You know what a JIT actually does, why it's fast, and the four traps where accuracy costs real speed.
  • You can map every CPU setting in Dolphin to a mechanism you understand — and predict what toggling it will do.
  • Want Gekko to run your code next? The homebrew course takes you from an empty folder to a program of your own on this CPU.

An interactive course built for the Dolphin project, companion to the GameCube audio course. Every lab on this page is a from-scratch simulation written for teaching — no game code, no copyrighted material, nothing emulated for real. Hardware and emulation details are grounded in public documentation and Dolphin's own source; figures we couldn't pin down precisely are marked “about” on purpose.

Source of truth · Source/Core/Core/PowerPC/ · Source/Core/Core/PowerPC/Jit64/ · Source/Core/Core/HW/