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.
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.
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
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 x 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 x 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 x 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 y ...
;; 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.