Skip to content
Techniques & Technology

Fixed-Point Maths

Smooth movement without floating point

Fixed-point arithmetic gives 8-bit systems sub-pixel precision for smooth movement, physics, and animation—all with fast integer operations.

C64zx-spectrumAmigaNES programmingmathsperformance 1960–present

Overview

8-bit processors don’t have floating-point hardware. Moving a sprite by 0.5 pixels per frame seems impossible when positions are whole numbers. Fixed-point maths solves this by treating integers as fractions—the upper bits hold the whole part, lower bits hold the fractional part.

The concept

Split a 16-bit value into whole and fractional parts:

16-bit fixed point (8.8 format):
WWWWWWWW.FFFFFFFF

W = whole number (0-255)
F = fraction (0/256 to 255/256)

The value $0180 represents 1.5:

  • High byte: $01 = 1
  • Low byte: $80 = 128/256 = 0.5

Common formats

FormatRangePrecisionUse case
8.80-2551/256Positions, velocities
4.40-151/16Compact, less precision
12.40-40951/16Large coordinates
1.70-11/128Interpolation, percentages

Basic operations

Addition and subtraction

Same as regular integer operations:

; Add velocity to position (16-bit)
    clc
    lda pos_lo
    adc vel_lo
    sta pos_lo
    lda pos_hi
    adc vel_hi
    sta pos_hi

Getting the whole part

Just read the high byte:

    lda pos_hi      ; whole number for screen position
    sta sprite_x

Setting a value

; Set position to 100.5
    lda #100
    sta pos_hi
    lda #128        ; 0.5 = 128/256
    sta pos_lo

Multiplication

More complex—typically done with shifts and adds.

Multiply by constant (shift)

; Multiply by 2
    asl pos_lo
    rol pos_hi

; Multiply by 4
    asl pos_lo
    rol pos_hi
    asl pos_lo
    rol pos_hi

General multiplication

For 8.8 × 8.8, the result is 16.16—keep the middle 16 bits:

; Simplified: multiply A by fixed-point B
; Result = (A × B) >> 8
multiply_8x8:
    ; Uses 16-bit intermediate
    ; ... (platform-specific implementation)

Division

Divide by power of 2 (shift right)

; Divide by 2
    lsr pos_hi
    ror pos_lo

; Divide by 4
    lsr pos_hi
    ror pos_lo
    lsr pos_hi
    ror pos_lo

General division

More expensive—often avoided by multiplying by reciprocal.

Practical example: smooth movement

Moving 1.5 pixels per frame:

; Velocity = 1.5 = $0180
velocity_hi:  .byte $01
velocity_lo:  .byte $80

; Position starts at 50.0 = $3200
position_hi:  .byte $32
position_lo:  .byte $00

update_position:
    clc
    lda position_lo
    adc velocity_lo
    sta position_lo
    lda position_hi
    adc velocity_hi
    sta position_hi

    ; Use whole part for sprite
    lda position_hi
    sta sprite_x
    rts

After 2 frames: 50.0 → 51.5 → 53.0

Gravity simulation

; Gravity adds to velocity each frame
gravity_lo:    .byte $40     ; 0.25 pixels/frame²
gravity_hi:    .byte $00

apply_gravity:
    ; velocity += gravity
    clc
    lda vel_lo
    adc gravity_lo
    sta vel_lo
    lda vel_hi
    adc gravity_hi
    sta vel_hi

    ; position += velocity
    clc
    lda pos_lo
    adc vel_lo
    sta pos_lo
    lda pos_hi
    adc vel_hi
    sta pos_hi
    rts

This creates smooth, realistic falling motion.

Negative numbers

For signed fixed-point, use two’s complement:

; -1.5 in 8.8 signed = $FE80
; $FE = -2, $80 = +0.5, total = -1.5

Subtraction and signed comparison require care.

Lookup tables alternative

For complex functions (sine, square root), use pre-calculated tables:

; 256-entry sine table (0-255 → sin×127)
sine_table:
    .byte 0, 3, 6, 9, 12, ...  ; sin(0°) to sin(90°)

Tables trade memory for speed—essential on 8-bit systems.

Platform notes

NES

Limited RAM makes 16-bit variables expensive. Use 8.8 sparingly.

ZX Spectrum

No index registers for 16-bit—IX/IY help but are slow.

C64

Zero page makes 16-bit operations faster. Use it for frequently-accessed fixed-point values.

See also