Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fixed-Point Math

The GBA ARM7TDMI has no floating-point unit. Floating-point is emulated in software, so fixed-point arithmetic is the usual choice for gameplay math, camera transforms, and register-facing values.

The fixed<> type

#include <gba/fixed_point>
using namespace gba::literals;

// 8.8 format (good for small ranges, fine sub-pixel steps)
gba::fixed<short> position = 3.5_fx;

// 16.16 format (high precision for world-space values)
gba::fixed<int> velocity = 0.125_fx;

// 24.8 format (common GBA-friendly choice, tonclib-style)
gba::fixed<int, 8> angle = 1.5_fx;

fixed<Rep, FracBits, IntermediateRep> stores a scaled integer in Rep.

  • Rep controls storage width and sign.
  • FracBits controls precision (step = 1 / 2^FracBits).
  • IntermediateRep controls multiply/divide intermediate width.

Precision and range

For a signed representation:

  • precision step: 1 / (1 << FracBits)
  • minimum: -2^(integer_bits)
  • maximum: 2^(integer_bits) - step

where integer_bits = numeric_limits<Rep>::digits - FracBits (digits excludes the sign bit).

For unsigned representations, minimum is 0.

Common formats

TypeFormatApprox rangePrecision step
fixed<short>8.8-128 to 127.996093751/256
fixed<int>16.16-32768 to 32767.99998474121/65536
fixed<int, 8>24.8-8388608 to 8388607.996093751/256
fixed<short, 4>12.4-2048 to 2047.93751/16

Introspecting format traits

using fx = gba::fixed<int, 8>;
using traits = gba::fixed_point_traits<fx>;

static_assert(traits::frac_bits == 8);
static_assert(std::is_same_v<traits::rep, int>);

The _fx literal

The _fx suffix creates fixed-point literals at compile time:

using namespace gba::literals;

gba::fixed<short> a = 3.14_fx;
gba::fixed<short> b = 2_fx;

auto c = a + b;
auto d = a * b;

_fx is format-agnostic until assignment, then converts to the destination fixed<> type.

Arithmetic and overflow behaviour

Standard operators are supported:

gba::fixed<short> a = 10.5_fx;
gba::fixed<short> b = 3.25_fx;

auto sum = a + b;
auto diff = a - b;
auto prod = a * b;
auto quot = a / b;

auto neg = -a;
bool gt = a > b;

Multiplication and division use IntermediateRep internally.

  • fixed<short> uses a 32-bit intermediate by default.
  • fixed<int> defaults to int intermediate (faster on ARM, lower headroom).

If you need safer large products/quotients, use precise<>, which switches to a 64-bit intermediate:

using fast = gba::fixed<int, 16>;
using safe = gba::precise<int, 16>;

fast a = 100.0_fx;
fast b = 400.0_fx;
auto fast_prod = a * b;   // may overflow in edge cases

safe x = 100.0_fx;
safe y = 400.0_fx;
auto safe_prod = x * y;   // wider intermediate

Mixed-type arithmetic and promotion API

Operations require compatible types. For different fixed<> formats, use the promotion wrappers in <gba/fixed_point> to make intent explicit.

Why wrappers exist

using fix8 = gba::fixed<int, 8>;
using fix4 = gba::fixed<int, 4>;

fix8 a = 3.5_fx;
fix4 b = 1.25_fx;

// auto bad = a + b; // incompatible formats
auto ok  = gba::as_lhs(a) + b;

Promotion wrappers

WrapperResult steeringTypical use
as_lhs(x)convert other operand to wrapped typekeep left-hand format
as_rhs(x)convert wrapped operand to other typematch right-hand format
as_widening(x)keep higher fractional precisionavoid precision loss
as_narrowing(x)match the narrower sideintentional truncation
as_average_frac(x)average fractional bitsbalanced precision
as_average_int(x)average integer-range bitsbalanced range
as_next_container(x)promote storage to next wider containerheadroom for mixed small types
as_word_storage(x)use int/unsigned int storageARM-friendly word math
as_signed(x)force signed storage typesign-aware operations
as_unsigned(x)force unsigned storage typenon-negative domains only
with_rounding(wrapper)rounding meta-wrapper for conversionsexplicit rounding policy path

Practical examples

using fix8 = gba::fixed<int, 8>;
using fix4 = gba::fixed<int, 4>;

fix8 hi = 3.53125_fx;
fix4 lo = 1.25_fx;

auto keep_hi = gba::as_lhs(hi) + lo;        // fix8 result
auto keep_lo = gba::as_rhs(hi) + lo;        // fix4 result
auto wide    = gba::as_widening(lo) + hi;   // fix8 result
auto narrow  = gba::as_narrowing(hi) + lo;  // fix4 result (truncating conversion)

Container promotion example:

using small = gba::fixed<char, 4>;
using med   = gba::fixed<short, 4>;

small a = 3.5_fx;
med   b = 2.0_fx;

auto r1 = gba::as_next_container(a) + b;
auto r2 = gba::as_word_storage(a) + b;

Converting to and from integers

gba::fixed<short> pos = 3.75_fx;

int whole = static_cast<int>(pos);   // truncates toward zero
short raw = gba::bit_cast(pos);      // raw scaled storage bits

bit_cast is useful for register writes that expect fixed-point bit patterns.

tonclib comparison

stdgbatonclib
fixed<int, 8> x = 3.5_fx;FIXED x = float2fx(3.5f);
auto y = x * z;FIXED y = fxmul(x, z);
auto q = x / z;FIXED q = fxdiv(x, z);
int i = static_cast<int>(x);int i = fx2int(x);

stdgba uses operators plus explicit promotion wrappers, so expressions stay readable while still making precision/range trade-offs visible in code.