Skip to content

Nailing the Point Down

Fix the point at an agreed scale and a fraction becomes an ordinary whole number, with explicit precision and range limits.

  • Foundations

Unit 4 showed a flexible representation with finite precision. Another option is fixed-point: agree on one scale and store a whole number of those smaller steps. That can simplify the arithmetic, but it does not make every fraction exact.

Agree on a scale

Pick a scale, say 256, and agree that each stored unit means one 256th. For this example, use a sixteen-bit unsigned integer, from 0 to 65535. That gives us two bytes to hold both whole and fractional parts.

Then 1.5 is not stored as “1.5” at all. It is stored as 1.5 × 256, which is 384. That is an ordinary whole number.

To read the real value back, divide by the agreed scale:

Code
SHOW 384 / 256
Output
1.5

The point is not floating. It is fixed at “divide by 256”, and everyone who touches the value agrees to it.

The fraction is being carried as a whole number. That is the trick, and it is also the name: fixed-point.

Adding at one scale

When two values share a scale, their stored integers can be added directly. The answer keeps that scale, provided it still fits in the chosen width.

Add 1.5 and 0.5. Store both scaled, as 384 and 128, then add the integers:

Code
SHOW 384 + 128
Output
512

512 is 2 at our scale, so 1.5 + 0.5 came to 2 using nothing but whole-number addition.

Reading the value is not dropping the fraction

A power-of-two scale makes the split visible. At scale 256, the high byte counts whole units and the low byte counts 256ths. For 384, those bytes are 1 and 128: one whole plus 128/256, or 1.5.

A right shift by eight keeps only the whole part: 1. It does not decode the full value 1.5. The discarded low byte held the fraction.

Multiplying an integer by 256 can be expressed as a left shift by eight if the result fits. That does not mean the full operation of reading a fractional value is the same as an integer right shift.

At scale 256, does shifting stored 384 right by eight give 1.5?

No. It gives the whole part, 1. Reading the full stored value means 384/256 = 1.5. The low byte, 128, accounts for the half that a right shift removes.

Keep the fraction between moves

Try this on paper. A position starts at zero and moves half a pixel on each update. At our scale, the stored step is 128. We keep the full position, but choose a whole-pixel coordinate for drawing it.

This is teaching notation, not a program to paste into a machine. LET changes a named value; SHOW displays it. The loop visits updates 1 through 4 and adds the step each time. Doing It Again offers more loop practice. No installation is needed for this trace.

Code
LET position = 0
LET step = 128
FOR update = 1 TO 4
  LET position = position + step
  LET pixel = position SHIFTRIGHT 8
  SHOW pixel
END

Here SHIFTRIGHT 8 extracts the whole part of our nonnegative, scale-256 value. Putting it in pixel leaves position unchanged. Predict all four displayed coordinates before reading on.

For a deliberately broken version, add this line immediately after SHOW pixel, before END:

Code
LET position = pixel * 256
A hint before the explanation

Which value does the next update read? After the first drawing, compare how much of the half-pixel move each version keeps in position.

Both versions first show 0. What happens over all four updates, and which value must the next update keep?

Show the explanation

The original keeps the fraction between updates:

UpdateStoredFull pixelsDrawn
11280.50
225611
33841.51
451222

It shows 0, 1, 1, 2. The broken version shows 0, 0, 0, 0: after each drawing it replaces position with zero, so every move starts from zero again. Its stored state first differs after drawing update 1; its visible result first differs at update 2. One correct first drawing did not show that the change was safe.

Remove the added assignment to repair it. Keep the full stored position for the next update; use the extracted whole coordinate only for drawing. A picture at one pixel cannot show every fraction that the program is keeping.

For a variation, change step to 64, a quarter-pixel move. Predict the four coordinates again, then check your reasoning.

Check the quarter-pixel moves

The stored positions are 64, 128, 192 and 256: full positions 0.25, 0.5, 0.75 and 1. The drawings are 0, 0, 0, 1. Keeping each quarter lets four updates reach the next pixel. Discarding it after each drawing loses that progress.

These examples stay within our stored range, 0 to 65535. A larger position or step needs a range check and a decision about what happens if the sum will not fit; do not silently assume wrapping or stopping. This paper trace counts updates, not elapsed time, and does not measure smoothness or speed.

What it costs you

Fixed-point makes several choices explicit.

You remember the scale. The machine sees 384 and thinks 384. Only you know it means 1.5.

You keep the scales matching. Every value in a sum has to share one scale, or the answer is nonsense.

You choose how to round. At scale 256, 0.1 would need a stored integer of 25.6. If we round to the nearest integer, we store 26, which means 26/256 = 0.1015625. A half fits exactly; a tenth does not at this scale.

The scale need not be a power of two. At scale 10, a tenth is exactly one stored unit. Pick the scale and rounding rule for the job. Operations such as multiplying two scaled values also need care with the resulting scale and intermediate range.

Choosing the scale

The scale decides what you get and what you give up.

A bigger scale buys finer fractions. At ×256 you can hold 256ths, which is far finer than a pixel.

It costs range. Our largest stored integer is 65535, so at scale 256 the largest value is 65535/256 = 255.99609375. More fractional steps mean fewer whole units in the same storage. Choose the width and scale together.

Matching-scale integer addition can be cheaper than software floating-point on a Z80. That is a reason to consider fixed-point, not a guarantee that it is best for every game. Check the precision, range and work needed on the target machine.

For another explicit conversion from fractions to whole numbers, see Meet BASIC’s secret-number expression. Its table explains how INT removes the fractional part; you can read that section without following the preceding game lessons.

When it’s wrong, see why

  • A value printed as a big whole number, like 384 rather than 1.5. You forgot to divide by the scale. The machine only ever sees integers.
  • Two values fought. Adding a ×256 number to a ×16 number gives nonsense. Everything in a sum must share the same scale.
  • The numbers ran out of room. A scale eats range, so a value that fitted as a plain integer may not fit once scaled.
  • A fraction vanished. Scaled values are still whole numbers, so values between the steps of your scale need rounding. Check the chosen rule and allowed error.

What you’ve learnt

  • Fixed-point stores a fraction as a whole number at an agreed scale.
  • Addition at one scale uses integer addition, while the result fits.
  • Extracting the whole part with a shift is different from reading the full fraction.
  • You carry the bookkeeping: remembering the scale, and keeping every value in a sum on the same one.
  • Its precision, range and rounding depend on the chosen width and scale.
  • Its speed depends on the target and operations, so measure the work that matters.

Where this leaves you

That is the primer. You can look at a number the way the machine does, whole or fractional, positive or negative, in bits or in hex, and you can mask it, shift it and scale it.

From here the road goes down into a real machine, where hex addresses, bit flags, signed offsets and fixed-point are not background facts. They are the language the code is written in.