Writing MIPS assembly is not necessary to make a Nintendo 64 game. You can make a game entirely using C, C++, or any other programming language you can get working on the Nintendo 64. However, you may want to be able to read MIPS assembly from time to time, and you may want to write small snippets of MIPS assembly.

This page will not teach you MIPS assembly, but just give you the highlights. See also MIPS III instructions, for a list of instructions.

Learning MIPS Assembly

MIPS is, still, commonly chosen as an architecture for teaching assembly language to computer science students. It has been used in countless college courses on assembly language. There several reasons why MIPS is a good language for teaching computer science classes, but that’s not really important for game developers—what is important is that you can easily find high-quality guides for programming in MIPS that teach the associated concepts and theory, and that don’t assume that you have any prior experience with MIPS.

MIPS Versions

The VR4300 in the Nintendo 64 console uses the MIPS III architecture. This is somewhat older than architectures like MIPS32 and MIPS64. Even though MIPS III is 64-bit, it is a different (older) architecture from MIPS64.

MIPS Quirks

The one big quirk of MIPS is that it contains (depending on the specific version) various non-interlocked hazards. This is what the MIPS acronym originally stood for, supposedly: “Microprocessor without Interlocked Pipeline Stages”.

A hazard happens when the results of one instruction are not complete by the time the next instruction executes.

An interlock prevents an instruction from executing until the inputs are ready, creating a pipeline stall—the pipeline stall delays the instruction so it can execute correctly.

In most other processors, you don’t have to know about these things, because the hazards are protected by interlocks. MIPS has some common hazards which are not protected by interlocks, so you must structure your assembly code to avoid the hazards. In some cases, the assembler can help you by automatically reordering the instructions to avoid hazards.

Warning: MIPS assemblers (except Bass) will reorder your instructions by default. This means that the assembly you write will not exactly match what you get when you disassemble the code afterwards. You can disable this with .set noreorder, or you can simply let the MIPS assembler make your job easier.

Three hazards you might commonly encounter on the VR4300 are the branch delay slot, the integer multiply and division instructions, and the floating-point multiply instruction (VR4300 multiply bug). There are other hazards that you would only typically see in kernel code, like changes to the TLB—refer to the CPU manual if you are doing anything special.

Branch Delay Slot

The instruction after most branch instructions is executed, regardless of whether the branch is taken. The exceptions are the “branch likely” instructions, such as bnel, which behave differently—refer to the CPU manual. For example, consider the C function:

int add(int x, int y) { return x + y; }

The actual MIPS generated by the compiler may be:

        .set    noreorder
add:
        jr      $31
        addu    $2,$4,$5

Note that the addu instruction appears after the function returns—it is in the branch delay slot, and will be executed anyway. However, if you are writing assembly, you can write the function like this instead:

add:
        addu    $2,$4,$5
        jr      $31

When you assemble it, you will get the same result as above… because by default, the assembler will reorder instructions so you don’t have to think about the delay slot. Disassembling the code from the second snippet will show you the first snippet.

Integer Multiplication and Division

After a mfhi, the next two instructions may not modify the HI register. After mflo, the next two instructions may not modify the LO register. Operations which modify those registers are:

  • ddiv
  • ddivu
  • div
  • divu
  • dmult
  • dmultu
  • mthi
  • mtlo

Like the branch delay slot, the assembler will fix this for you. For example, if you try to assemble the following code:

my_function:
        mfhi $2
        mthi $2

The assembler will insert two nop instructions between mfhi and mthi to avoid the hazard. This can be disabled with .set noreorder, just like the branch delay slot.

VR4300 Multiplication Bug

TODO: What is the exact nature of this bug?

The assembler will not fix this one for you. After mul.s or mul.d, insert two nop instructions to avoid triggering the bug.