Skip to content

Calling conventions, data layout and floating point ​

This page lists what you need to read a game's machine code correctly and to call its functions under the emulator. The tables list only the details games use. The authoritative sources are linked at the end.

Calling conventions ​

PlatformInteger and pointer argumentsFloat argumentsReturnthisStack
Windows x64 (all 64-bit Windows games)rcx, rdx, r8, r9, then stackxmm0-xmm3 (by position: the 2nd argument is rdx or xmm1)rax, xmm0rcxcaller reserves 32 bytes of shadow space; stack 16-byte aligned at the call
System V x64 (Linux, macOS)rdi, rsi, rdx, rcx, r8, r9xmm0-xmm7 (counted separately from integers)rax (rdx:rax), xmm0rdi16-byte aligned; 128-byte red zone below rsp
Windows x86 __cdeclstack, right to leftstack (float as 4 bytes, double as 8)eax (edx:eax), x87 st(0) for floatsn/acaller cleans the stack
Windows x86 __stdcall (WinAPI)stack, right to leftstackeax, st(0)n/acallee cleans (ret N)
Windows x86 __thiscall (MSVC C++ methods)stackstackeax, st(0)ecxcallee cleans
Windows x86 __fastcallecx, edx, then stackstackeax, st(0)n/acallee cleans
ARM64 (consoles, Apple silicon, Android)x0-x7v0-v7x0, v0x016-byte aligned

Structures larger than 8 bytes are usually passed by hidden pointer on Windows x64, and returned through a hidden pointer passed as the first argument (so this moves to rdx).

Callee-saved (non-volatile) registers on Windows x64: rbx, rbp, rdi, rsi, r12-r15, xmm6-xmm15. If pseudocode seems to use a value from nowhere, it may be one of these, kept across a call.

C++ layout (MSVC) ​

  • An object with virtual methods starts with a pointer to its vtable (+0x00). Multiple inheritance adds one vtable pointer per base with virtual methods.
  • A vtable is an array of function pointers; MSVC puts a pointer to the RTTI "complete object locator" just before it (at vtable - 8 on x64), which leads to the class name (.?AVCitizen@@).
  • Virtual destructors take a flag argument (scalar deleting destructor): FUN(this, 1) deletes.
  • std::string (MSVC, x64): 32 bytes; up to 15 characters stored inline, otherwise a pointer. std::vector: three pointers (begin, end, capacity end).
  • Member functions are named ?Method@Class@Namespace@@... when exported; any C++ demangler (c++filt understands Itanium names; for MSVC use llvm-undname or Ghidra) turns them back.

Floating point ​

A reimplementation is exact only if it copies these details:

  • SSE (all x64 games). float operations are 32-bit (addss, mulss), double 64-bit (addsd). Each instruction rounds its result once. Copy the precision of every operation.
  • x87 (many 32-bit games). The FPU computes in 80-bit precision and rounds only when a value is stored to memory as 32 or 64 bits (fstp dword). Intermediate results are more precise than either float type; match where the original stores. The precision control word (set by Direct3D 9 to 24-bit by default unless D3DCREATE_FPU_PRESERVE is used) can make x87 round every result to float precision.
  • Conversions. cvttss2si truncates toward zero (a C cast); cvtss2si rounds with the current mode (lrintf). Ghidra writes both as a cast; the instruction tells you which.
  • min and max. minss a, b returns b when either is NaN, so keep the original's operand order.
  • Fused multiply-add. A compiler may turn a * b + c into one FMA instruction with one rounding instead of two. Look for vfmadd in the original; avoid accidental FMA in the reimplementation (compiler flags such as -ffp-contract=off).
  • Maths functions. sinf, cosf, atan2f, powf, expf come from the C runtime the game shipped with. They differ in the last bit between runtimes. Emulate them and reimplement the original's versions when the simulation depends on them.
  • Denormals and rounding mode live in MXCSR (SSE) or the x87 control word. Games sometimes enable flush-to-zero for speed; check the start-up code for ldmxcsr or _controlfp.

Integers ​

  • Division rounds toward zero in C (-7 / 2 == -3); Python's // rounds down (-4).
  • sar is an arithmetic (signed) shift, shr a logical one; Ghidra shows both as >>.
  • Overflow wraps in machine code; languages that check or widen (Rust debug builds, Python) need explicit wrapping.
  • imul/idiv are signed, mul/div unsigned. Take the type from the instruction, whatever type Ghidra declares.

Further reading ​

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