Skip to content

Integers on every platform ​

Signed division edge cases on six CPUs and in four port languages

Integer arithmetic looks the same everywhere until a value reaches an edge, such as a division by zero, the most negative number divided by -1, a shift by the full width of the register or a sum that does not fit. At those edges each CPU does something different. Each language that a port may be written in also does something different. The original game shows one behaviour. The player may see it as a crash, a wrapped score or a unit that jumps across the map. The port must show the same one.

This page lists the edges on the CPUs that games ran on, and the ways to reproduce each in a port. Calling conventions and floating point covers the instructions, signed and unsigned compares and C's integer promotion. The facts here come from the CPU manuals and the language references.

Division ​

CPUDivide by zeroMost negative value / -1
x86 (div, idiv)Divide error (interrupt 0)Divide error, because the quotient does not fit
68000 (divs, divu)Trap through vector 5Overflow flag set, the destination left unchanged
MIPS (div, divu)No exception; the result in HI and LO is unpredictableNo exception; the result is unpredictable
PowerPC (divw)No exception; the result is undefinedNo exception; the result is undefined
ARM (sdiv, udiv, ARMv7 and later)Returns 0Returns the most negative value
6502, Z80, SH-1, early ARMNo divide instructionThe game's own routine decides

On x86 a bad division ends the program. Under DOS, the default handler for interrupt 0 prints "Divide overflow" and returns to DOS. Under Windows the program gets an EXCEPTION_INT_DIVIDE_BY_ZERO or EXCEPTION_INT_OVERFLOW exception, and without a handler it closes. If the original can reach such a division, the player sees a crash, and the port must decide whether to copy it. Record the decision as a deviation if the port does something else.

On MIPS the compiler adds the check. GCC for MIPS places a teq or break 7 after a division to trap on a zero divisor. A PlayStation or Nintendo 64 game built without that check gets whatever HI and LO hold. Measure that value on the emulator the project trusts, or show that the game never divides by zero.

Without a divide instruction, the routine is the specification. 8-bit games and many ARM games before ARMv7 divide in a loop or call a library routine. The Game Boy Advance has a divide function in its BIOS. Each routine has its own answers for zero and for negative values. Port the routine, or run it under emulation on the edge cases and copy its results.

The sign of the remainder. C99 and C++11 round the quotient toward zero, so the remainder takes the sign of the dividend: -7 % 2 is -1. C89 left this to the compiler for negative operands. Compilers for x86 followed idiv, which also rounds toward zero. Python's % takes the sign of the divisor instead, so -7 % 2 is 1 there.

Shifts ​

Shift by N, register of W bitsResult
x86 from the 80186 onThe count is masked to 5 bits (6 bits for 64-bit operands), so a 32-bit shift by 32 shifts by 0
8086 and 8088The count is not masked; a large count shifts every bit out
ARM, count in a registerThe bottom byte of the count is used; a count of 32 or more gives 0 for lsl and lsr, and all sign bits for asr
C and C++Undefined when N is negative or at least W
Java, JavaScript, WebAssemblyThe count is masked to the width (5 bits for 32-bit values)

A game compiled for x86 that shifts by a computed count gets the masked result. The same C code compiled for ARM gives a different one. Read the instruction in the original, and write the port's shift so that it masks the count the same way.

sar keeps the sign and shr does not. A signed right shift rounds toward minus infinity, so -7 >> 1 is -4. A division by 2 rounds toward zero and gives -3. Compilers turn a signed division by a power of two into a shift with a correction for negative values. The correction is missing when the source used a shift.

Wrapping, saturating and decimal arithmetic ​

  • Wrapping. Most integer instructions keep the low bits of the result. A 16-bit score that passes 65,535 starts again at 0. Copy the width of every variable, including the narrow ones in saves and in network messages.
  • Saturating. MMX (paddsw, paddusb), ARM (qadd), NEON and many DSPs stop at the largest or smallest value instead of wrapping. Software mixers and colour arithmetic use them. A port that wraps where the original saturates gives loud clicks in sound and dark spots in pictures.
  • Binary-coded decimal. The 6502 in decimal mode (sed), the Z80's daa and x86's daa keep two decimal digits in a byte. Scores and timers on 8-bit machines often use it. The NES CPU has no decimal mode, so NES games do the same work in their own code.
  • Signed overflow in C. The C standard calls it undefined. An optimising compiler may then remove a test that the source seemed to contain, such as if (x + 1 < x). The machine code is what ran, so read the behaviour from the instructions.

The same results in the port ​

Port languageWrappingDivision edgesShifts
RustOverflow panics in debug builds and wraps in release builds; use wrapping_add and the other wrapping_* methods to wrap in both/ and % panic on zero and on i32::MIN / -1; use checked_div and decide the result yourself<< panics on a large count in debug builds; wrapping_shl masks the count
JavaScriptNumbers are doubles; x | 0 gives a 32-bit signed result, x >>> 0 an unsigned one; Math.imul multiplies as 32-bit integers/ gives a fraction; use Math.trunc(a / b) | 0; a zero divisor gives Infinity, which | 0 turns into 0The count is masked to 5 bits
WebAssemblyi32.add and the other operations wrapi32.div_s traps on zero and on overflow; i32.rem_s traps on zero but gives 0 for the most negative value % -1The count is taken modulo the width
C#Wraps unless the code is in a checked blockDivide by zero throws DivideByZeroException; int.MinValue / -1 throws OverflowExceptionThe count is masked to 5 bits for int
PythonIntegers never overflow; mask with & 0xFFFFFFFF and convert the sign yourself// rounds down; divide by zero raises ZeroDivisionErrorNo width, so mask after each shift

Put the original's integer operations in a few small helper functions in the port. Write a division with the original's rounding and edge cases, a shift with its masking, and an add with its width. Test the helpers against the original under emulation on the edge cases (re-emulate), then use them everywhere.

What to record ​

  • The width and signedness of every variable that the port reproduces.
  • Every division that can see a zero or the most negative value, and what the original does there.
  • Every shift by a computed count, and how the original's CPU masks it.
  • Any saturating or decimal arithmetic, with the routine that does it.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.