Skip to main content

Math Library

R65 provides a standard math library for operations that exceed the 65816's native capabilities: scalar math functions (math.r65), an unsigned 32-bit integer type (U32.r65), a signed 32-bit integer type (I32.r65), a 32-bit floating-point type (F32.r65), and two fixed-point types — signed Q10.r65 (10.6) and unsigned Q8.r65 (8.8).

include!("lib/sneslib.r65")  // Required: hardware register definitions
include!("lib/math.r65") // Scalar math functions
include!("lib/U32.r65") // Unsigned 32-bit integer
include!("lib/I32.r65") // Signed 32-bit integer
include!("lib/F32.r65") // 32-bit floating point
include!("lib/Q10.r65") // Signed 10.6 fixed point
include!("lib/Q8.r65") // Unsigned 8.8 fixed point
note

r65x init copies only sneslib.r65, math.r65, and 65816.r65 into a new project's src/lib/. Copy the other files across from stdlib/ yourself, or add stdlib/ to the include path with r65c -I path/to/stdlib.

Scalar Math Functions

math.r65 provides multiplication, division, modulo, and variable shift operations. All are far fn.

On SNES targets (#[cfg(snes)]) multiplication and division use the hardware math units. Only mul8, mul16, and div8 have software fallbacks; div16, mod16, and mod8 are SNES-only, and the non-SNES build offers mod_shift in their place.

Multiplication

FunctionSignatureDescription
mul8(multA @ A: u8, multB @ B: u8) -> u168x8 unsigned multiply, 16-bit product
mul16(multA @ A: u16, multB: u16) -> u1616x16 unsigned multiply, low 16 bits of the product

On SNES, these use the hardware multiplication unit (WRMPYA/WRMPYB) and complete in ~8 cycles. Software fallbacks use shift-and-add (~150-300 cycles).

Division and Modulo

FunctionSignatureDescription
div16(dividend @ A: u16, divisor: u8) -> u1616-bit / 8-bit unsigned division. SNES only
div8(dividend @ A: u8, divisor @ X: u16) -> u88-bit / 8-bit unsigned division
mod16(dividend @ A: u16, divisor: u8) -> u1616-bit % 8-bit unsigned modulo. SNES only
mod8(dividend @ A: u8, divisor @ X: u16) -> u88-bit % 8-bit unsigned modulo. SNES only
mod_shift(dividend @ A: u8, divisor: u8) -> u88-bit modulo by shift-and-subtract. Non-SNES only

On SNES, these use the hardware division unit (WRDIVL/WRDIVH/WRDIVB) and take ~30 cycles. Only div8 has a software fallback (restoring division, ~200-400 cycles); a non-SNES build that needs a modulo uses mod_shift.

Variable Shifts

FunctionSignatureDescription
shl8(value @ A: u8, amount @ X: u16) -> u88-bit left shift by variable amount
shr8(value @ A: u8, amount @ X: u16) -> u88-bit logical right shift
shri8(value @ A: u8, amount @ X: u16) -> u88-bit arithmetic right shift (sign-preserving)
shl16(value @ A: u16, amount @ X: u16) -> u1616-bit left shift by variable amount
shr16(value @ A: u16, amount @ X: u16) -> u1616-bit logical right shift

These are software loop implementations. Cost scales linearly with the shift amount.

U32 — Unsigned 32-bit Integer

A 4-byte packed struct stored as two 16-bit words in little-endian order:

struct U32 {
lo: u16, // Offset 0: Low 16 bits
hi: u16 // Offset 2: High 16 bits
}

All methods take a far *self pointer (the far is on each method's self, not the impl block). Operations modify the value in place.

Literal Initialization

The U32! macro initializes a U32 from a compile-time constant, automatically splitting it into lo/hi halves:

#[ram] static mut SCORE: U32 = U32!(100000);   // lo=0x86A0, hi=0x0001
#[ram] static mut MAX: U32 = U32!(0xFFFFFFFF); // lo=0xFFFF, hi=0xFFFF
#[ram] static mut ZERO: U32 = U32!(0); // lo=0x0000, hi=0x0000

This is equivalent to writing the struct literal manually:

#[ram] static mut SCORE: U32 = U32 { lo: 0x86A0, hi: 0x0001 };

Conversion

MethodSignatureDescription
from_u16(far *self, value @ X: u16)Initialize from u16 (zero-extends high word)
to_u16(far *self) -> u16Truncate to u16 (returns low word only)
copy(far *self, src: far *U32)Copy value from another U32

Arithmetic

MethodSignatureDescription
+=(far *self, other: far *U32)self += other with overflow wrapping (impl AddAssign)
-=(far *self, other: far *U32)self -= other with underflow wrapping (impl SubAssign)
*=(far *self, other: far *U32)self *= other (32x32, low 32 bits kept) (impl MulAssign)
/=(far *self, other: far *U32)self /= other (yields 0xFFFFFFFF on divide by zero) (impl DivAssign)
mod(far *self, other: far *U32)self %= other (unchanged on divide by zero)
add_u16(far *self, value @ A: u16)self += value (u16 operand, zero-extends)
sub_u16(far *self, value @ A: u16)self -= value (u16 operand, zero-extends)
mul_u16(far *self, value @ A: u16)self *= value (u16 operand, low 32 bits kept)
div_u16(far *self, value @ A: u16)self /= value (u16 operand; returns 0xFFFFFFFF on divide by zero)
mod_u16(far *self, value @ A: u16)self %= value (u16 operand, zero-extends; unchanged on divide by zero)

Whole-U32 arithmetic is reached through the compound-assignment operators, not through named methods — U32 implements AddAssign, SubAssign, MulAssign, and DivAssign. mod is the one named method, because there is no %= operator trait.

SCORE += BONUS;      // impl AddAssign
SCORE /= DIVISOR; // impl DivAssign
SCORE.mod(&MODULUS);

Comparison

MethodSignatureDescription
cmp(far *self, other: far *U32) -> i8Returns 1 if self > other, 0 if equal, -1 if self < other

Bitwise Shifts

MethodSignatureDescription
shl(far *self, count @ X: u16)Shift left by n bits in place
shr(far *self, count @ X: u16)Logical shift right by n bits in place

SNES Hardware Division

Available only with #[cfg(snes)]. Uses the hardware division unit for faster small-divisor operations.

MethodSignatureDescription
div_u8(far *self, divisor @ X: u16) -> u8Divide by 8-bit value, returns remainder
mod_u8(far *self, divisor @ X: u16)Modulo by 8-bit value

I32 — Signed 32-bit Integer

Same memory layout as U32, using two's complement representation:

struct I32 {
lo: u16, // Offset 0: Low 16 bits
hi: u16 // Offset 2: High 16 bits (bit 15 = sign bit)
}

Range: -2,147,483,648 to 2,147,483,647.

Literal Initialization

The I32! macro initializes an I32 from a compile-time constant, handling two's complement automatically:

#[ram] static mut OFFSET: I32 = I32!(-42);      // lo=0xFFD6, hi=0xFFFF
#[ram] static mut GRAVITY: I32 = I32!(-256); // lo=0xFF00, hi=0xFFFF
#[ram] static mut POSITIVE: I32 = I32!(1000); // lo=0x03E8, hi=0x0000

Conversion

MethodSignatureDescription
from_i16(far *self, value: u16)Initialize from i16 (sign-extends high word)
from_u16(far *self, value: u16)Initialize from u16 (zero-extends, positive values only)
to_i16(far *self) -> i16Truncate to i16 (returns low word only)
copy(far *self, src: far *I32)Copy value from another I32

Sign Operations

MethodSignatureDescription
is_negative(far *self) -> boolReturns true if sign bit is set
neg(far *self)Negate in place (two's complement: ~self + 1)
abs(far *self)Absolute value: negates if negative

Arithmetic

MethodSignatureDescription
+=(far *self, other: far *I32)self += other (two's complement addition) (impl AddAssign)
-=(far *self, other: far *I32)self -= other (two's complement subtraction) (impl SubAssign)
*=(far *self, other: far *I32)self *= other (signed, low 32 bits kept) (impl MulAssign)
/=(far *self, other: far *I32)self /= other (rounds toward zero; yields MIN_I32 on divide by zero) (impl DivAssign)
mod(far *self, other: far *I32)self %= other (remainder sign matches dividend; unchanged on divide by zero)
add_i16(far *self, value @ A: u16)self += value (i16 operand, sign-extends)
sub_i16(far *self, value @ A: u16)self -= value (i16 operand, sign-extends)
mul_i16(far *self, value @ A: u16)self *= value (i16 operand, low 32 bits kept)
div_i16(far *self, value @ A: u16)self /= value (i16 operand; returns MIN_I32 on divide by zero)
mod_i16(far *self, value @ A: u16)self %= value (i16 operand, sign-extends; unchanged on divide by zero)

As with U32, whole-I32 arithmetic goes through +=, -=, *=, and /=; mod is the one named method.

Comparison

MethodSignatureDescription
cmp(far *self, other: far *I32) -> i8Signed comparison: 1 if self > other, 0 if equal, -1 if self < other

Bitwise Shifts

MethodSignatureDescription
shl(far *self, count @ X: u16)Shift left by n bits in place
sar(far *self, count @ X: u16)Arithmetic shift right (preserves sign bit)

SNES Hardware Division

Available only with #[cfg(snes)].

MethodSignatureDescription
div_i8(far *self, divisor @ X: u16) -> i8Divide by signed 8-bit value, returns remainder
mod_i8(far *self, divisor @ X: u16)Modulo by signed 8-bit value

F32 — 32-bit Floating Point

A 32-bit floating-point type using the Woz/Rankin format (Dr. Dobb's Journal, 1976) adapted for the 65816. Precision is ~6–7 significant decimal digits with a range of roughly 10-38 to 1038.

struct F32 {
mant_lo: u16, // Offset 0: mantissa low 16 bits
exp_hi: u16 // Offset 2: low byte = mantissa high (sign + 7 bits),
// high byte = exponent (excess-128 biased)
}

Internally the value is a 1-byte excess-128 exponent plus a 24-bit two's complement mantissa; zero is represented by an exponent byte of 0.

Values and Constants

There is no decimal-literal macro for F32. You obtain values three ways:

// 1. At runtime, from a signed integer:
V.from_i16(7 as i16); // V = 7.0

// 2. From a pre-defined constant (used as an operand):
RADIUS *= F32_PI; // multiply by 3.14159

// 3. From raw components, for a static initializer or custom constant:
#[ram] static mut HALF: F32 = F32_RAW!(0x80, 0x40, 0x0000); // 0.5

F32_RAW!(exp, mant_hi, mant_lo) builds a value from its encoded bytes. The library predefines these constants:

ConstantValueConstantValue
F32_ZERO0.0F32_TEN10.0
F32_ONE1.0F32_HUNDRED100.0
F32_NEG_ONE-1.0F32_PI3.14159
F32_HALF0.5F32_E2.71828
F32_TWO2.0  
note

A constant is a const F32 — usable as an operand (x += F32_ONE) but not as a whole value. A static mut initializer must therefore use F32_RAW!, not a named constant.

Conversion

MethodSignatureDescription
from_i16(far *self, value: u16)Set from a signed 16-bit integer
to_i16(far *self) -> i16Truncate toward zero to a signed 16-bit integer
copy(far *self, src: far *F32)Copy value from another F32

Sign and Tests

MethodSignatureDescription
is_zero(far *self) -> boolReturns true if the value is zero
is_negative(far *self) -> boolReturns true if the sign bit is set
neg(far *self)Negate in place
abs(far *self)Absolute value in place

Arithmetic and Comparison

F32 exposes arithmetic through the operators — there are no named add/sub methods. Both operands must be F32 (a value, a static, or a constant):

V += W;            // add
V -= W; // subtract
V *= W; // multiply
V /= F32_TWO; // divide (any operand — no power-of-2 restriction)

if V < W { ... } // also == != <= > >=

For an explicit three-way result, cmp(far *self, other: far *F32) -> i8 returns the sign of self - other (negative, 0, or positive).

Q10 — Signed 10.6 Fixed Point

Q10.r65 is a 16-bit signed fixed-point type: 10 integer bits (−512 to +511) and 6 fractional bits, so a precision of 1/64.

Q10 is a newtype over i16 — a distinct type at compile time, an ordinary 16-bit value at runtime. The generated code is identical to using a bare i16, and it passes and returns by value in a register.

struct Q10(i16);

The distinction is one-directional. A Q10 will not pass where an i16 is wanted and will not mix with another newtype, so a scaled value cannot silently be consumed as a raw count. An i16 flowing the other way, into a Q10, is accepted — wrap deliberately at the point a raw number becomes a fixed-point one.

Constants

ConstantValueMeaning
Q10_ONE641.0
Q10_HALF320.5
Q10_1_64TH1The smallest representable step
Q10_FRAC_MASK0x3F (i16)Selects the fraction bits. A plain i16: it is a mask, not a value

Associated Functions

FunctionSignatureDescription
Q10::from_int(n: i16) -> Q10Whole number to fixed point
Q10::from(n: i16, f: u8) -> Q10Integer part plus a 0–63 fraction
Q10::lerp(a: Q10, b: Q10, t: Q10) -> Q10Linear interpolation, t in 0.0–1.0. SNES only

Methods

All take self by value.

MethodSignatureDescription
to_int(self) -> i16Integer part, rounding toward negative infinity
to_frac(self) -> i16Fraction bits, 0–63
round(self) -> i16Nearest whole number, ties up
mul(self, other: Q10) -> Q10Scaling multiply, (a * b) >> 6. SNES only
div(self, other: Q10) -> Q10Scaling divide. Saturates to ±511.984 on divide by zero
div_u8(self, d: u8) -> Q10Divide by a small whole number. SNES only
abs(self) -> Q10Absolute value
to_string(self, buf: far *u8) -> u16Decimal text into buf; returns the byte count

The clamp! Macro

Q10 also defines a method macro:

VELOCITY = VELOCITY.clamp!(Q10::from_int(-4), Q10::from_int(4));

Why mul and div Are Methods

Q10 inherits +, -, and the comparisons from its i16 payload, and those are correct as-is. Multiply and divide are not: a raw a * b lands 64× too high, and a raw a / b cancels the scale entirely.

They stay named methods because a newtype cannot override an inherited operator. An impl MulAssign for Q10 compiles but never dispatches — a *= b still lowers to the payload's multiply.

#[ram]
static mut VELOCITY: Q10;

fn step() {
let player_x: Q10 = Q10::from_int(100); // 100.0
VELOCITY = Q10::from(0, 32); // 0.5 (32/64)

let next: Q10 = player_x + VELOCITY; // + and < inherited from i16
let scaled: Q10 = next.mul(VELOCITY); // scaling multiply
let whole: i16 = next.round();
}

Q8 — Unsigned 8.8 Fixed Point

Q8.r65 is a 16-bit unsigned fixed-point type: 8 integer bits (0–255) and 8 fractional bits, so a precision of 1/256. Like Q10 it is a newtype, here over u16.

struct Q8(u16);
ConstantValueMeaning
Q8_ONE2561.0
Q8_HALF1280.5
Q8_1_256TH1The smallest representable step
Q8_FRAC_MASK0x00FF (u16)Selects the fraction bits
Function / MethodSignatureDescription
Q8::from_int(n: u8) -> Q8Whole number to fixed point
Q8::from(n: u8, f: u8) -> Q8Integer part plus a 0–255 fraction
Q8::lerp(a: Q8, b: Q8, t: Q8) -> Q8Linear interpolation. SNES only
to_int(self) -> u8Integer part, truncating
to_frac(self) -> u8Fraction bits, 0–255
round(self) -> u8Nearest whole number, saturating at 255 rather than wrapping
mul(self, other: Q8) -> Q8Scaling multiply. SNES only
div(self, other: Q8) -> Q8Scaling divide
div_u8(self, d: u8) -> Q8Divide by a small whole number. SNES only
to_string(self, buf: far *u8) -> u16Decimal text into buf; the fraction is truncated, not rounded

Q8 defines clamp! as well. There is no abs — the type is unsigned.

caution

Q10 and Q8 both define a clamp! method macro. When both are included, .clamp!() on a let local whose type the compiler cannot resolve falls back to name lookup and reports ambiguous method macro 'clamp' — defined in: Q10, Q8. Calling it on a typed static resolves fine.

Strings and format!

U32, I32, F32, Q10, and Q8 all provide to_string(buf, ...) -> u16, so all five are reachable from format!:

let q: Q10 = Q10::from(3, 16);
format!(BUF, "v={s}!", q); // newtype: by value

format!(BUF, "score {s}", &SCORE); // U32/I32/F32: by pointer

to_string writes no NUL terminator — the returned count is what delimits the string. format! appends a single NUL once the whole result is assembled. A direct caller that wants a C string writes the NUL itself at the returned offset.

Operators

U32, I32, and F32 work with the arithmetic and comparison operators directly, so you rarely need to call the methods above by name. Both sides must be the same type (a U32 with a U32, an F32 with an F32). For F32 the operators are the only arithmetic surface — it has no named add/sub methods:

  • Arithmetic: +=, -=, *=, /=
  • Comparison: ==, !=, <, <=, >, >=
SCORE += BONUS;
if PLAYER_SCORE >= HIGH_SCORE {
new_record();
}

To combine a 32-bit value with a u16 (for example, add a small constant), keep the _u16/_i16 methods — operators only work between two values of the same type. %, <<, and >> likewise have no operator form; use mod, shl, and shr.

Copy a value with Clone instead of a bare = (structs are never assigned by value):

let snapshot = SCORE.clone();      // new copy
HIGH_SCORE.clone_from(&SCORE); // copy into an existing value

See Operator Overloading for the full rules.

Examples

Basic Arithmetic

#[ram] static mut SCORE: U32 = U32!(0);
#[ram] static mut BONUS: U32 = U32!(10000);

fn add_points(amount @ A: u16) {
SCORE.add_u16(amount); // u16 operand → method
}

fn award_bonus() {
SCORE += BONUS; // U32 + U32 → operator
}

Signed Computation

#[ram] static mut VELOCITY: I32 = I32!(0);
#[ram] static mut POSITION: I32 = I32!(10000);

fn apply_gravity() {
VELOCITY.sub_i16(256); // scalar operand → method
POSITION += VELOCITY; // I32 + I32 → operator
}

fn reverse_direction() {
VELOCITY.neg();
}

Comparison and Branching

#[ram] static mut PLAYER_SCORE: U32 = U32!(0);
#[ram] static mut HIGH_SCORE: U32 = U32!(100000);

fn check_high_score(result @ A: u8) -> u8 {
if PLAYER_SCORE > HIGH_SCORE {
// New high score!
HIGH_SCORE.clone_from(&PLAYER_SCORE);
return 1;
}
return 0;
}

Converting Between Sizes

#[ram] static mut TOTAL_DAMAGE: I32 = I32!(0);

fn apply_damage(amount @ X: u16) {
TOTAL_DAMAGE.from_i16(amount as i16);

// Clamp to u16 range for display
let display_damage @ A: u16 = TOTAL_DAMAGE.to_i16() as u16;
}

Multiplication and Division

#[ram] static mut TILE_OFFSET: U32;

fn compute_tile_address(row @ A: u16) {
TILE_OFFSET.from_u16(row);
TILE_OFFSET.mul_u16(64); // row * 64, handles overflow
}

Floating-Point Physics

#[ram] static mut VELOCITY: F32 = F32_RAW!(0x00, 0x00, 0x0000);   // 0.0
#[ram] static mut POSITION: F32 = F32_RAW!(0x00, 0x00, 0x0000); // 0.0
#[ram] static mut GRAVITY: F32 = F32_RAW!(0x80, 0x40, 0x0000); // 0.5

fn physics_step() {
VELOCITY += GRAVITY; // accelerate downward
POSITION += VELOCITY; // integrate position

if POSITION < F32_ZERO { // hit the floor — bounce
POSITION.from_i16(0 as i16);
VELOCITY.neg();
}
}

Complete Example: Frame Counter and FPS Calculation

This example demonstrates most U32/I32 features in a practical scenario — tracking elapsed frames and computing frames per second.

include!("lib/sneslib.r65")
include!("lib/U32.r65")

const FRAMES_PER_SECOND: u16 = 60;
const FRAMES_PER_MINUTE: u16 = 3600; // 60 * 60

// Frame counter starts at 0
#[ram]
static mut FRAME_COUNT: U32 = U32!(0);


// NMI fires every frame (~60Hz)
#[interrupt(nmi)]
fn vblank_handler() {
FRAME_COUNT.add_u16(1);
}

// Calculate seconds elapsed since start
fn get_elapsed_seconds(result @ A: u16) -> u16 {
// Clone the frame count so the original is not modified
let mut seconds_elapsed = FRAME_COUNT.clone();

// Divide by 60 to get seconds
seconds_elapsed.div_u16(FRAMES_PER_SECOND);

// Return as u16 (truncate if > 65535 seconds)
return seconds_elapsed.to_u16();
}

// Calculate minutes and remaining seconds
fn get_time_display(minutes @ A: u16, seconds @ X: u16) -> u16, u16 {
// Total minutes
let mut minutes_elapsed = FRAME_COUNT.clone();
minutes_elapsed.div_u16(FRAMES_PER_MINUTE);

// Remaining seconds: (frame_count / 60) % 60
let mut seconds_elapsed = FRAME_COUNT.clone();
seconds_elapsed.div_u16(FRAMES_PER_SECOND);
seconds_elapsed.mod_u16(FRAMES_PER_SECOND);

return minutes_elapsed.to_u16(), seconds_elapsed.to_u16();
}

// Check if a specific milestone frame has been reached
fn check_milestone(milestone_frames: u16, result: u8) -> u8 {
let mut milestone: U32;
milestone.from_u16(milestone_frames);

if FRAME_COUNT >= milestone {
return 1; // Milestone reached
}
return 0;
}

// Reset frame counter for new level/session
fn reset_timer() {
FRAME_COUNT.from_u16(0);
}