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.
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):
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 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.
Two non-obvious bits:
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.
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.
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.
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:
Use these when:
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.
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.
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).
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:
A seed of 0 is replaced by a fixed non-zero state (an LFSR stuck at 0 would stay there).
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:
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.
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.
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.
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".
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.
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) 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:
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.
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).
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:
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.
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.
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:
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.)
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.
| 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.