The E-Reader API uses a nice and simple fixed point system

Fixed point is a reality on older hardware, especially gaming systems. The Nintendo E-Reader is no different, it uses a simple 16-bit fixed point system. I like how intuitive it is, and it has helped hammer the point home that fixed point at its core is a simple concept.

Fixed point is a way to do decimal numbers on a CPU that doesn't normally support them. If you want to know more, I wrote an article about using fixed point on the Neo Geo here. It talks about the Neo Geo, but the concepts of fixed point are universal.

Why so big?

The E-Reader's z80 API (commonly called ERAPI, and I'll call it that from now on) has functions like SpriteAutoMove. It takes an x velocity and y velocity, and then moves a sprite for you over time. A common x velocity for this function is something like 0x300. When I was first starting out I thought "huh, that's a big number", and that was about it. Fixed point is still not really on my radar, as it's virtually never used at all in modern software.

It's so big because this function is using a simple fixed point system. All ERAPI functions that involve velocities, deltas, angles, etc, use this same fixed point system.

ERAPI's fixed point system

The z80 is an 8-bit processor, and can stretch to work with 16-bit numbers. Nintendo took advantage of this when creating the ERAPI fixed point system. It's simply 8 bits of integer, and 8 bits of decimal.

The E-Reader's 8-8 fixed point system

Whole numbers

This makes whole numbers easy to write and reason about. 0x0100 is 1, 0x0200 is 2, etc. In hexadecimal, each digit is 4 bits. A 16-bit number will have four hexadecimal digits, so here the top two digits are the integer, and bottom two are the decimal.

The simplicity here is only present if you express the number in hexadecimal. So getting comfortable with hex helps here.

Simple fractions

In hexadecimal, 8 is the halfway point (like 5 is in decimal), so 0x0180 is 1.5, and 0x0240 is 2.25.

Into and out of the fixed point system

Inevitably, you need to leave fixed point and convert your value to a simple integer. A good example of this is ERAPI's SetSpritePos, which takes an x in pixels and y in pixels. If you accidentally stay in the fixed point system and give it 0x100 for x, that is off the screen and usually not what you want.

To get from fixed point to integer, we just need to move the number down 8 bits to chop off the decimal portion. The z80 cannot do division, but fortunately this can be accomplished with a simple bitshift instead. Just move the number over by 8 bits and we've got it. But the z80 can only shift one bit at a time, and even worse, only on 8-bit numbers!

A clever solution

The z80 has 8-bit registers, such as a, h and l. But you can combine two registers to get a 16-bit register, such as hl. Let's say we have our player's x position in the hl register

;; moves the 16-bit number from the _player_x variable into hl
ld hl, (_player_x)

Once a number is in hl, we can work with the two bytes that make up the number individually by working with the h and l registers.

To chop it down to 8 bits, we just move the bytes around.

;; move the integer portion of our fixed number down to l
ld l, h
;; clear out h
ld h, 0
Going from fixed to integer
We lost the .5 part of the number, but that is expected. Commonly when a computer goes from a decimal number to an integer, it does so by just chopping the decimal part off.

And the other direction

Going the other way is just as simple. Since the top 8 bits of the 16-bit number is the integer, we again just need to do some register loads.

;; our starting integer 8, in a
ld a, 8
;; move it to the top byte of our fixed point system
ld h, a
;; clear out the decimal byte
ld l, 0

And now hl is 0x0800, which is 8 in our fixed point system.

A concrete example

And to put it all together, here is how we move our player 1.5 pixels whenever right on the d-pad is pressed

First, our variable for the x location, and a constant for our movement delta

;; 1.5 in the fixed point system
DELTA_X = 0x180
;; the variable for storing the player's location
_player_x:
    .dw 0

We define a word (a 16-bit number) and start it out at 0.

Then whenever right is pressed on the d-pad, we call this function

player_go_right:
    ;; load the current player's x
    ;; from memory into the hl register
    ld hl, (_player_x)
    ;; get our delta into another 16-bit register
    ld bc, DELTA_X
    ;; change the value
    ;; this add call is hl hl bc
    add hl, bc
    ;; and save the new value back to memory
    ld (_player_x), hl
    ;; return from the function
    ret

And finally, render the player's current location onto the screen

player_render:
    ;; get the player's current fixed point location
    ld hl, (_player_x)
    ;; convert from fixed point to integer
    ld l, h
    ld h, 0
    ;; move it to the register that SetSpritePos is expecting
    push hl
    pop de
    ;; do the same with ...
    ;; and finally tell the E-Reader to move our sprite
    ;; get the sprite handle loaded from memory
    ld hl, (_player_sprite_handle)
    ;; call SetSpritePos
    rst 0
    .db ERAPI_SetSpritePos
    ;; return from the function
    ret

The rst 0 followed by .db ERAPI_SetSpritePos is a very z80 specific way of basically calling SetSpritePos(handle, x, y)

Negative gotcha

Things can get tricky with negative numbers (when are negative numbers not tricky in assembly?). When converting from the fixed system to an integer, we move h down to l, and zero h out. That is fine if you plan to just use l by itself as an 8-bit number. If it was negative in the fixed system, then l will be negative too.

But if you are converting from 16-bit fixed to a 16-bit integer and need to account for negatives, then making h zero is incorrect if the value was negative in fixed point. In that case, h needs to become 0xff.

Conclusion

And that's all there is to it. If you read my Neo Geo fixed point article, there I constructed a somewhat elaborate fixed point "system". But here we're really just working with 16-bit numbers and shifting down and up as needed. It's a nice reminder that at the end of the day, fixed point really isn't very complex.

Oh and if you're curious about my E-Reader games, check out https://retrodotcards.com.