Endian Swapping

This time I was reading the chapter “Data, Code and Memory Layout” of Game Engine Architecture and I found a very interesting discussion about Endianess of multi-byte values.

There is two common ways of organizing multi-byte values in memory:

  • Little-endian: The data's least-significant byte is stored in a lower address than the most-significant byte. Example: the value 0xABCD1234 would be stored in the memory following this order: 0x34, 0x12, 0xCD, 0xAB.
  • Big-endian: It's basically the inverse of Little-endian, so the data's least-significant byte is stored in a higher address than the most-significant byte. Example: 0xABCD1234 would be stored in this order: 0xAB, 0xCD, 0x12, 0x34.

This is important for game programmers because they interact with a lot of platforms, and not all of them have the same endianess. For example: Windows or Linux running on x86-64 processors use little-endian, but game consoles like Playstation 3 or Xbox 360 are, by default, big-endian. So, when working with multi-platform games, the developers must be carefull with the endianess of their binary assets.

One reasonable approach would be to not use binary data, and instead, write text files using decimal or hexadecimal digits. This way, their assets would be portable to any platform, with the price of wasting some disk space.

Other approach, with more efficient results, is to swap the endianess of binary data accordingly to the target platform. This requires additional tools for endian-swapping.

Endian-swapping for Integers

Integers have solid representation in memory, with all bits representing the numeric value. For example, to swap the endianess of an integer with 32 bits, like 0x1234ABCD we simply swap each byte with its reflected pair. The process would be:

  • 0x34, 0x12, 0xCD, 0xAB
  • 0xAB, 0x12, 0xCD, 0x34
  • 0xAB, 0x12, 0xCD, 0x34
  • 0xAB, 0xCD, 0x12, 0x34

This lead us to the following code:

// I32 is just an alias to integer with 32 bits
I32 swapI32(I32 v) {
    // Mask and shift each byte then logical OR the results 
    return ((v & 0x000000FF) << 24) 
          |((v & 0x0000FF00) << 8)  
          |((v & 0x00FF0000) >> 8)  
          |((v & 0xFF000000) >> 24); 
}

The reason why we are shifting each byte is to “move” them to their reflected position, so the LSB is shifted 4 bytes to the left, the LSB+1 is shifted 1 byte to the left and so on…

The same algorithm can be used to integers of other sizes like 16 or 64 bits.

Endian-swapping for Floats

Unlike the Integer representation, where all bits have the same meaning, the floating-point values have the signal-byte, the mantissa and the expoent. This “non-uniform” representation of floating-points is not defined in C/C++, so the bitwise operators that we use to swap the endianess of integers can’t be applied to floats.

What we do instead, is simply make our program belives that it is operating on integers, a technique called Type Punning. We can do this by casting the adress of our float to an adress of integer with the same size. For our lucky, C++ have a proper cast operator to this, the “reinterpret_cast”. But, in order to achieve a more portable solution and make our code more stylish we can make the type punning using an union hack:

// F32 is an alias to float with 32 bits
union I32F32 {
    I32 asI32;
    F32 asF32;
};

Since all values of our union share the same address, we can set asF32 to our float value and use bitwise operations on the asI32.
So to actually swap the endianess of our float we just “cast” it to integer, swap the integer and “cast” back to float.

F32 swapF32(F32 v) {
    I32F32 u;
    u.asF32 = v;
    u.asI32 = swapI32(u.asI32);
    return u.asF32;
}

And the final code is:

// for platform-independent sized integers
#include <cstdint> 

using I16 = std::int16_t;
using I32 = std::int32_t;
using I64 = std::int64_t;
using F32 = float;
using F64 = double;

I16 swapI16(I16 v) {
    return ((v & 0x00FF) << 8)  
          |((v & 0xFF00) >> 8); 
}

I32 swapI32(I32 v) {
    return ((v & 0x000000FF) << 24)
          |((v & 0x0000FF00) << 8)
          |((v & 0x00FF0000) >> 8)
          |((v & 0xFF000000) >> 24);
}

I64 swapI64(I64 v) {
  return ((v & 0x00000000000000FF) << 56)
        |((v & 0x000000000000FF00) << 40)
        |((v & 0x0000000000FF0000) << 24)
        |((v & 0x00000000FF000000) << 8)
        |((v & 0x000000FF00000000) >> 8)
        |((v & 0x0000FF0000000000) >> 24)
        |((v & 0x00FF000000000000) >> 40)
        |((v & 0xFF00000000000000) >> 56);
}

union I32F32 {
    I32 asI32;
    F32 asF32;
};

union I64F64 {
      I64 asI64;
      F64 asF64;
};

F32 swapF32(F32 v) {
      I32F32 u;
      u.asF32 = v;
      u.asI32 = swapI32(u.asI32);
      return u.asF32;
}

F64 swapF64(F64 v) {
      I64F64 u;
      u.asF64 = v;
      u.asI64 = swapI64(u.asI64);
      return u.asF64;
}