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 cycles. At 60 fps with 1369 cycles per scanline, 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.
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 intentionally capped at 4 fractional bits — the 32-bit shift needed for full 8 bits of fraction would currently truncate under the QBE 32-bit codegen gap (catalogue chantier A7); we chose deterministic 4-bit precision over deceptive 8-bit output that's only correct for small inputs.
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.