OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
Fixed-Point Math Tutorial

This tutorial covers <snes/math.h>: the lib's fixed-point arithmetic, sin/cos lookup tables, and safe integer multiply/divide. The SNES has no floating-point unit — every smooth movement, every rotation, every interpolation in your game is built on fixed-point or LUTs.

It assumes you have read the Graphics tutorial. The Mode 7 tutorial is the natural pair — Mode 7's matrix maths goes through fixSin/fixCos/fixMul constantly.

Why fixed-point on the SNES

Floating point on the SNES is possible but expensive. cproc / QBE can emit software-float code (the lib avoids it deliberately — PHILOSOPHY.md calls out "no printf in core lib" partly because formatted-output helpers force software floats), but a single float multiply costs ~500 CPU cycles — more than two scanlines of CPU time (a scanline is 1364 master cycles, roughly 227 CPU cycles at 3.58 MHz FastROM). A few floats per frame is fine; per-sprite or per-particle is infeasible.

Fixed-point is the canonical alternative: integers that interpret some bits as fractional. With 8.8 fixed-point (16-bit total), the high byte is the integer part and the low byte is n/256. A multiply is a 16 × 16 → 32-bit integer multiply followed by a shift — the SNES has hardware for the first part (multiplier registers $4202-$4206) and the lib does the shift.

Cost comparison (rough):

  • float multiply: ~500 cycles.
  • fixMul 8.8 × 8.8 → 8.8: ~30 cycles.
  • fixSin(angle): a table read in assembly, about 260 master cycles (34 CPU cycles) plus the call. It was inlined in the header until 2026-10-03; a call now costs about 96 master cycles more than the inlined lookup did.
  • mul16(a, b) (16 × 16 → low 16): ~10 cycles (hardware).

Order-of-magnitude: fixed-point is 10–25× faster than software floats. For real-time game logic, that's the difference between "playable" and "choppy".

The 8.8 fixed-point format

The lib's fixed type is a 16-bit signed value (s16) interpreted as:

Bits Meaning Range
15–8 Integer part (signed) -128 to 127
7–0 Fractional part 0 to 255 (representing 0.0 to 0.996…)

So fixed value = 0x0140 represents 1.25 (integer = 1, fraction = 0x40 = 64/256 = 0.25). A fixed with value 0xFF80 represents -0.5 (the standard two's-complement sign-extension applies).

Total range: roughly -128.0 to +127.996. Precision: 1/256 ≈ 0.0039. Good enough for screen-space coordinates (the SNES displays 256 × 224, so 8.8 covers the full screen with sub-pixel precision). Not enough for distances over a few hundred units — in that case, drop to integer pixel coordinates and reserve fixed for velocity/acceleration.

The conversion macros

#include <snes/math.h>
fixed pos = FIX(50); /* 50.0 → 0x3200 */
fixed half = FIX(1) / 2; /* 0.5 → 0x0080 */
fixed neg = -FIX(3); /* -3.0 → 0xFD00 */
s16 ipart = UNFIX(pos); /* 50 (truncate toward zero) */
s16 ipart_round = UNFIX_ROUND(pos + 128); /* 51 (round to nearest) */
u8 fpart = FIX_FRAC(pos); /* 0 (the fractional byte) */
fixed combined = FIX_MAKE(2, 64); /* 2.25 — int=2, frac=64/256 */
signed short s16
16-bit signed integer (-32768 to 32767)
Definition types.h:50
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47
SNES Fixed-Point Math.
#define UNFIX(x)
Convert fixed-point to integer (floor).
Definition math.h:100
#define FIX_MAKE(i, f)
Create fixed-point from integer and fraction.
Definition math.h:132
#define FIX_FRAC(x)
Get fractional part of fixed-point.
Definition math.h:119
#define FIX(x)
Convert integer to fixed-point.
Definition math.h:86
#define UNFIX_ROUND(x)
Convert fixed-point to integer (rounded).
Definition math.h:112
s16 fixed
8.8 signed fixed-point type
Definition math.h:69

Two non-obvious bits:

  1. UNFIX truncates toward zero — same behaviour as integer C division. UNFIX(FIX(-1) / 2) = 0, not -1. For round-to-nearest, use UNFIX_ROUND(x + 128) (the +128 adds half before truncating).
  2. FIX(x) is (x << 8) — only safe for x in -128..127. FIX(200) overflows the high byte and wraps to -56.0. The compiler doesn't warn; check your inputs.

Fixed-point arithmetic

fixed pos = FIX(100);
fixed velocity = FIX(1) / 4; /* 0.25 */
pos = pos + velocity; /* 100.25 — addition is plain int+ */
pos = pos - FIX(50); /* 50.25 — subtraction is plain int- */
fixed scale = FIX(1) / 2; /* 0.5 */
fixed half_pos = fixMul(pos, scale); /* 25.125 — needs fixMul! */
fixed fixMul(fixed a, fixed b)
Multiply two fixed-point values.

The trap: pos * scale (using the C * operator) treats both as plain s16s and gives you (pos × scale) >> 0 — an enormous wrong number. Always use fixMul when both operands are fixed. The function does a 32-bit intermediate multiply and re-shifts back to 8.8 format.

For division: fixDiv(a, b) produces a / b as fixed-point. Returns 0 on divide-by-zero — silent failure mode, but predictable. Validate divisors before passing them.

Trigonometry (sin/cos lookup tables)

The SDK uses 8-bit angles: 0–255 maps linearly to 0°–360°. So 64 = 90°, 128 = 180°, 192 = 270°. Wrapping is automatic — adding 1 to angle 255 wraps to 0 in u8.

u8 angle = 0;
while (1) {
fixed dx = fixCos(angle); /* x component, 8.8 fixed */
fixed dy = fixSin(angle); /* y component */
fixed speed = FIX(2);
fixed move_x = fixMul(speed, dx); /* 2 cos(angle), 8.8 */
fixed move_y = fixMul(speed, dy);
player_x = player_x + move_x;
player_y = player_y + move_y;
angle++; /* rotate 1.4° per frame */
}
static s16 player_y
Player Y position in screen coordinates.
Definition main.c:58
static s16 player_x
Player X position in screen coordinates.
Definition main.c:56
void WaitForVBlank(void)
Wait for next VBlank period.
fixed fixCos(u8 angle)
Get cosine value for angle.
fixed fixSin(u8 angle)
Get sine value for angle.
static u16 angle
Mode 7 ground: 9-tile prefab track (original art).
Definition main.c:46

Both fixSin and fixCos return values in [-256, +256] (i.e., -1.0 to +1.0 in 8.8 fixed). The result of fixMul(speed, fixSin(a)) is a velocity vector component in 8.8 fixed.

The LUTs are 256-entry, 2 bytes per entry, 512 bytes total. They live in ROM in lib/source/math.asm. Lookups are O(1) — ~20 cycles per call.

Hardware multiplier and divider

The SNES CPU has a hardware 8-bit unsigned multiplier (WRMPYA / WRMPYB at $4202/$4203, result in RDMPYL/RDMPYH) and a 16-bit unsigned divider (WRDIVL/WRDIVH/WRDIVB at $4204–$4206, result in RDDIVL/RDDIVH for quotient and RDMPYL/RDMPYH for remainder).

The lib's mul16 / div16 / mod16 use these:

u16 a = 100, b = 50;
u16 product = mul16(a, b); /* 5000 — uses $4202/$4203 */
u16 quotient = div16(a, b); /* 2 — uses $4204-$4206 */
u16 remainder = mod16(a, b); /* 0 — same divide, reads RDMPY */
static u16 b
Definition main.c:156
static u16 a
Definition main.c:156
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
u16 div16(u16 dividend, u16 divisor)
Safe 16-bit division.
u16 mod16(u16 dividend, u16 divisor)
Get remainder of division.
u16 mul16(u16 a, u16 b)
Safe 16-bit multiplication.

Use these when:

  • You need genuine 16 × 16 → 32-bit intermediate (handled internally).
  • The compiler's * / / operator may produce slow code or call a runtime helper.

The lib functions wait the required cycle delay (8 cycles for multiply, 16 for divide) before reading the result. Hand-rolled access to the hardware registers must respect those delays — read too early and you get garbage.

Utilities

fixed v = FIX(-3) + 64; /* -2.75 */
fixed positive = fixAbs(v); /* 2.75 */
fixed clamped = fixClamp(v, FIX(0), FIX(10)); /* 0.0 — clamped to min */
fixed lerped = fixLerp(FIX(0), FIX(100), 128); /* 50.0 — t=128/256=0.5 */
fixed fixClamp(fixed x, fixed min, fixed max)
Clamp value to range.
fixed fixAbs(fixed x)
Absolute value of fixed-point.
fixed fixLerp(fixed a, fixed b, u16 t)
Linear interpolation.

fixLerp(a, b, t) interpolates between a and b with t from 0 (returns a) to 256 (returns b). Linear; the canonical way to animate values smoothly between two endpoints.

Easing curves

easeInQuad(t) and easeOutQuad(t) map t from 0-255 to a curve 0-255: the first starts slow and accelerates, the second starts fast and settles. They read a 256-byte table in their own module, so add math_ease to LIB_MODULES (without it the link fails, naming the table).

/* start_x < end_x, both u16 */
u8 t;
for (t = 0; t < 255; t += 5) {
u16 x = start_x + (u16)(((u32)(end_x - start_x) * easeOutQuad(t)) >> 8);
oamSetX(0, x); /* slides in, then settles */
}
unsigned int u32
32-bit unsigned integer (0 to 4294967295)
Definition types.h:90
u8 easeOutQuad(u8 t)
Ease-out quadratic: 1 - (1-t)² curve, output [0, 255].
void oamSetX(u16 id, u16 x)
Set sprite X position.

Random numbers

rngNext() returns the next value of a 16-bit linear feedback shift register, 1-65535 (never 0). consoleInit() seeds it; for a different sequence each game, seed it from something the player does:

if (padPressed(0) & KEY_START) {
rngSeed(getFrameCount()); /* the moment START was pressed */
}
u16 enemy_x = rngNext() % 256;
static s16 enemy_x[4]
X positions of the four static enemy sprites.
Definition main.c:63
void rngSeed(u16 seed)
Seed the generator (the srand() of 0.x, removed 2026-10-05).
u16 getFrameCount(void)
Get frame counter.
Definition console.h:269
u16 rngNext(void)
Next pseudo-random 16-bit number.
#define KEY_START
Definition input.h:77
#define padPressed(pad)
Definition input.h:159

A seed of 0 is replaced by a fixed non-zero state (an LFSR stuck at 0 would stay there).

16.16 when 8.8 runs out of room

fixed (8.8) holds ±127.99 in steps of 1/256. That is the right size for a velocity, a screen coordinate or a fade level, and it costs almost nothing. It is the wrong size for a world coordinate on a large map, an accumulator that integrates over many frames, or anything that must stay precise while also being large — 8.8 overflows at 128.

<snes/fixed32.h> is the same idea one register wider: fixed32 is an s32 split 16 bits of integer and 16 of fraction, so ±32767 in steps of 1/65536. Opt in with the module:

LIB_MODULES += fixed32 # pulls in math for the sine table
fixed32 x = FIX32(100); /* 100.0 */
fixed32 third = FIX32_MAKE(0, 21845); /* 0.333... — integer and fraction parts */
u16 frac = FIX32_FRAC(third); /* the raw fractional half */
#define FIX32_MAKE(i, f)
Build a fixed32 from integer + fractional parts.
Definition fixed32.h:122
#define FIX32(x)
Convert integer to fixed32 (shift left 16).
Definition fixed32.h:82
#define FIX32_FRAC(x)
Get fractional part of a fixed32 as a u16 (0..65535).
Definition fixed32.h:110
s32 fixed32
16.16 signed fixed-point type
Definition fixed32.h:68

The arithmetic you would expect is inline and free — fix32Abs, fix32Clamp, fix32Min, fix32Max, fix32Lerp — and addition and subtraction are plain C operators, because two 16.16 values add like any other integer.

Four operations are not free, and their cost is the whole reason to think before reaching for 16.16:

Operation Cost Notes
fix32Mul(a, b) ~280 cycles four hardware 8×8 multiplies behind three partial products; overflow wraps modulo 2^32
fix32Div(a, b) ~1500 cycles one quotient bit per iteration over an 80-bit working register — use sparingly in a loop
fix32Sin(angle) table lookup 8-bit angle, so 256 steps around the circle
fix32Cos(angle) table lookup same table, quarter-turn offset

fix32Lerp is inline on top of fix32Mul, so it costs the multiply and nothing more, and it extrapolates happily outside [0, FIX32(1)].

A rule of thumb that holds on this machine: keep the world in 16.16 and the screen in 8.8. examples/basics/fix32_orbit does exactly that — the orbit is computed with fix32Sin/fix32Cos at 16.16 and only the final screen position is narrowed — and examples/basics/aim_target uses the wider type for the aiming maths behind an 8.8 presentation.

Note
fix32Sin and fix32Cos are implemented in assembly. Two compiler bugs once made the obvious C body wrong, which is why; both were fixed long ago, and testing/fixtures/libtests now computes that C expression alongside the assembly one and asserts they agree. The assembly stays because there is no reason to churn a working routine.

Worked patterns

Smooth sprite movement

fixed pos_x = FIX(64);
fixed pos_y = FIX(112);
fixed vel_x = FIX(1) / 2; /* 0.5 px/frame */
fixed vel_y = FIX(1) / 4; /* 0.25 px/frame */
while (1) {
/* Bounce off screen edges */
if (UNFIX(pos_x) < 0 || UNFIX(pos_x) > 240) vel_x = -vel_x;
if (UNFIX(pos_y) < 0 || UNFIX(pos_y) > 208) vel_y = -vel_y;
/* Render: convert fixed to integer screen coords */
}
static s16 vel_y
Definition main.c:176
static s16 pos_x
Definition main.c:177
static s16 pos_y
Definition main.c:178
static s16 vel_x
Ball X velocity (-2 to +2 pixels/frame).
Definition main.c:175
#define oamSetXYFast(_id, _x, _y)
Update sprite position only (most common per-frame operation).
Definition sprite.h:905

The fixed-point representation gives sub-pixel precision so the sprite advances 0.25 px/frame visibly — every fourth frame the integer coordinate increments by one. With pure-integer velocity this would round to 0 per frame, and the sprite would never move.

Rotation around a point

u8 angle = 0;
s16 cx = 128, cy = 112; /* rotation centre on screen */
s16 radius = 32;
while (1) {
fixed dx = fixMul(FIX(radius), fixCos(angle)); /* x offset */
fixed dy = fixMul(FIX(radius), fixSin(angle));
s16 sprite_x = cx + UNFIX(dx);
s16 sprite_y = cy + UNFIX(dy);
oamSetXYFast(0, sprite_x, sprite_y);
angle += 2; /* ~2.8° per frame */
}
static u8 cy
Definition main.c:75
static u8 cx
Definition main.c:75

This pattern (radius × cos / radius × sin) is used everywhere a sprite needs to orbit, swing, or rotate around a fixed point. Mode 7 matrix construction is conceptually the same operation, just emitted to the M7A–M7D registers instead of OAM coordinates.

Smooth interpolation

/* Camera follows player, but lags by 25 % each frame for smoothness */
while (1) {
fixed target = pos_x; /* where the camera wants to be */
camera_x = fixLerp(camera_x, target, 64); /* t=64/256=0.25 */
}
void bgSetScrollX(u8 bg, u16 x)
Set horizontal scroll only.
static s16 camera_x
Definition main.c:137

The lerp toward the target with t = 0.25 produces a critically-damped smooth follow — every frame the camera covers a quarter of the remaining distance. Common pattern for "the camera glides instead of snapping".

HDMA gradient table generation

The HDMA gradient example uses fixed-point under the hood: a brightness ramp from level 15 at the top to level 0 at the bottom of the screen, computed as a fixed-point linear interpolation over 224 scanlines. The lib's hdmaBrightnessGradient(channel, top, bottom) helper does this internally.

For custom HDMA tables, the same pattern applies: compute fixed-point fractional values per scanline, UNFIX to integer when writing the table byte.

Gotchas

🟢 int and long sizes are correct on this target (since v0.20.0)

sizeof(int) == 2, sizeof(long) == 4. Bare int is the native 16-bit word; long is 32 bits. C convention says int is the natural word size of the machine, and on the w65816 that's now true.

For portability across compilers (cross-platform tooling, code shared with PC-side helpers), keep using s16, u16, s32, u32, fixed from lib/include/snes/types.h — they make intent explicit and work identically everywhere. Bare int speed = 5; no longer doubles your cycle cost on cc65816, but a build of the same code with a different compiler may behave differently.

The fixed type is still s16. No change for fixed-point users.

🔴 FIX(x) overflows for |x| ≥ 128

FIX(x) is (x << 8). For x = 128, the result is 0x8000 = -32768 interpreted as s16 = -128.0 in fixed-point — wrap around. There's no compile-time error and no runtime warning.

If you need values larger than ±127, either:

  • Switch to integer pixel coordinates for position and reserve fixed-point for velocity / acceleration (which stay small).
  • Use a custom 16.16 format in s32 for higher range. The lib doesn't ship 16.16 helpers — write them per-game.

🔴 fixMul truncates fractional precision

fixMul(0x0080, 0x0080) (0.5 × 0.5) returns 0x0040 (0.25). Correct. But fixMul(0x0001, 0x0001) (~0.004 × ~0.004) returns 0x0000 — the result underflows to zero. For very small fixed values, the shift back to 8.8 throws away precision.

This is intrinsic to 8.8 fixed-point: ~0.0039 is the minimum representable non-zero. Multiplying two such values gives ~1.5e-5, unrepresentable. If you need tiny scale factors (e.g., very slow acceleration), use a wider fixed-point format or accumulate without multiplying.

🟠 fixDiv returns 0 on divide-by-zero

Silent failure: fixDiv(FIX(10), FIX(0)) returns 0, not a sentinel, not a trap. Validate divisors before calling — especially for user-derived inputs (e.g., dividing by a velocity that could be zero when the player isn't moving).

🟠 Compiler * vs mul16

The C * operator works correctly on s16/u16 operands; the "runtime can have bugs" caveat in mul16's docstring is historical (early QBE backend bugs that have shipped fixes per compiler/PINS.md). Today, a * b where both are u16 is generally safe and faster than mul16(a, b) (the compiler inlines the hardware multiplier write).

Use mul16 when:

  • You need the explicit "I am invoking the hardware multiplier" path.
  • You're paranoid about a specific corner case (the 32-bit intermediate is guaranteed).
  • You're matching a PVSnesLib port byte-for-byte.

🟡 Hardware multiplier read timing

The hardware multiplier needs 8 CPU cycles between writing WRMPYB and reading RDMPYL/RDMPYH. The hardware divider needs 16 cycles between WRDIVB and RDDIVL/RDDIVH/RDMPYL. The lib inserts the right delays in mul16/div16/mod16. Custom assembly that touches the multiplier registers must respect this — read too early and you get partial results.

🟡 Sin/cos LUTs are unsigned-angle, signed-result

fixSin(angle) and fixCos(angle) take u8 (no sign — the 8-bit angle wraps naturally). They return fixed (signed s16). This is the right shape for game code, but if you're translating from a different engine, watch the signedness boundary.

atan2 and sqrt ship in the lib (since 2026-05-09)

The full inverse-trig and square-root surface arrived together so the canonical "where is the target relative to me, and how far?" question is one call away. Three entry points cover almost every game-side use:

Function Returns Cost
u16 sqrt16(u16 n) floor of sqrt(n) (always 0–255) ~80 cycles, no LUT
fixed fixSqrt(fixed x) sqrt(x) in 8.8 fixed ~80 cycles, 4 fractional bits
u8 atan2_8(s16 dy, s16 dx) 8-bit angle (0–255 = 0°–360°) ~120 cycles, 65-byte LUT

atan2_8 uses the same 8-bit angle convention as fixSin/fixCos, so the natural pattern works:

u8 aim = atan2_8(dy, dx);
/* Now use aim with the trig LUTs to project a velocity vector */
fixed vx = fixMul(speed, fixCos(aim));
fixed vy = fixMul(speed, fixSin(aim));
static s16 enemy_y[4]
Y positions of the four static enemy sprites.
Definition main.c:65
u8 atan2_8(s16 dy, s16 dx)
8-bit two-argument arctangent

atan2_8 is scale-invariant — pass any 16-bit signed dy/dx and the function reduces magnitudes by power-of-two right shifts internally to keep its single 16-bit divide overflow-free. Precision degrades by ≤ 1 angle unit (≈ 1.4°) when reduction fires, which is below the perceptual floor for projectile aiming or sprite rotation in any 256-pixel-wide playfield.

sqrt16 is the canonical "how far is that thing" helper. The result is bounded to 255 (since sqrt(65535) ≈ 255.99), so it fits in u8 for tile-grid distances. For the 8.8 fractional variant use fixSqrt. Note that fixSqrt's precision is capped at 4 fractional bits: it is one 16-bit root shifted, which is what keeps it at about 80 cycles. The full 8 bits would need the root of a 24-bit value, a 32-bit loop. (When it was written the compiler also truncated that shift; it no longer does, since v0.21.2, so the cap is now a speed choice.)

What's still missing: pow, exp, log

These remain off the SDK's beaten path because no shipped example needs them. Integer exponentiation for small exponents is two lines of repeated fixMul; everything else (logarithm, general power) is per-game territory — bake a LUT for the specific range your game cares about.

The PHILOSOPHY non-goal "no printf in core lib" extends here in spirit: the lib provides what most games need, and skips the heavy weight that most games don't.

Cycle cost summary

Operation Cycles (approx)
FIX(x) / UNFIX(x) (macros) 0–2 (compile-time or shift)
fixed + fixed (addition) 4–8
fixMul(a, b) ~30
fixDiv(a, b) ~80
fixSin(angle) / fixCos(angle) ~20 (LUT lookup)
fixLerp(a, b, t) ~40
mul16(a, b) (hardware multiply) ~10
div16(a, b) (hardware divide) ~20
sqrt16(n) ~80 (bit-by-bit)
fixSqrt(x) ~80 (delegates to sqrt16)
atan2_8(dy, dx) ~120 (LUT + one 16-bit divide)

A typical sprite-physics frame (position update + sin/cos rotation + two fixMul calls per sprite × 64 sprites) is ~6 K cycles, well under 1 % of the frame. Fixed-point is cheap; the budget is rarely binding.

See also

  • lib/include/snes/math.h — full API reference.
  • lib/source/math.asm — implementation (273 LOC; the sin/cos LUTs live here).
  • Mode 7 tutorial — the natural application: matrix building via fixSin/fixCos/fixMul.
  • HDMA tutorial — for runtime HDMA table generation that uses fixed-point under the hood.
  • Animation tutorial — uses lerp and angle-based patterns for sprite animation curves.
  • Collision tutorial — the bounding-box collision module uses integer math, but advanced collision (circle, swept, ray-cast) goes through fixed-point.
  • KNOWN_LIMITATIONS.md — covers the historical int = 32 bits trap (closed in v0.20.0, 2026-05-08).