NOTE: I have a really great comment to share with you from a recent installment, but it deserves an article of it’s very own. Thank you, Jim, for the assembly sample. I’ll be getting to that soon…
Meanwhile, William Astle once again comments with some explanations to move this topic forward:
The KEYBUF thing is actually a lot more straight forward than it looks. It basically holds the result of reading FF00 for each keyboard column as of the last time KEYIN ran. More specifically, the last time it got to actually reading that column since it stops as soon as it finds a new key press and, so, it won’t necessarily update all eight entries in KEYBUF every time. It will when no new keys are pressed, though.
Basically, what it does is this:
Read the data from FF00
EOR it with the relevant byte in KEYBUF; this sets any bit where the value has changed since the last read; that is, a key was pressed or released. Exclusive OR yields a 1 if its two inputs are different and a 0 if the two inputs are the same.
AND the result of (2) with the KEYBUF value. This will keep only bits where the previous result was a “1” (not pressed). That ignores any keys that were released or are held down (previous state being a “0” in both of those cases).
Store the value read from FF00 into the KEYBUF byte using a copy of the original read from FF00.
If The result of (3) is nonzero, it stops reading the PIA and decodes the new keypress, does the debounce thing, etc.
KEYIN also masks off the joystick comparator input so you won’t see that in KEYBUF. Further, when reading the column with SHIFT in it, it masks that as well. So you can’t test for shift by looking in KEYBUF.
Basically, what the POKEs do is trick KEYIN into thinking that no keys in those particular columns were previously pressed by setting the bits to “1”.
As a side note, you can detect keys that are currently pressed by PEEKing KEYBUF. As long as Basic is doing its BREAK check, KEYIN is getting called at least once before every statement so KEYBUF gets updated.William Astle
I’m glad there’s someone around here who understands this stuff. But it was his last paragraph that caught my attention. If we can just PEEK those values, INKEY$ isn’t even necessary (offering a slight speedup, perhaps).
Let’s look at what’s inside those eight KEYBUF locations. To make them fit on a 32 column screen, I will print them out as HEX values:
0 REM keyboard.bas 10 FOR A=338 TO 345:POKE A,&HFF 20 PRINT HEX$(PEEK(A))" "; 30 NEXT:PRINT:GOTO 10
Run this and you will see eight columns of hex values that represent the KEYBUF status of most of the keys. And by POKEing each value to 255 (&HFF for speed) before they are read, it allows detecting the key as long as it is being held down. You can now see the status of the arrow keys, including if you pressed two at the same time (Up+Left).
Let’s display this in bits so we can visualize which bits we care about:
0 REM keyboard2.bas 5 POKE 65495,0:CLS:FOR BT=0 TO 7:BT(BT)=INT(2^BT):NEXT 10 PRINT@0,;:FOR A=338 TO 345:POKE A,&HFF:PRINT A;:V=PEEK(A) 20 FOR BT=7 TO 0 STEP-1:IF V AND BT(BT) THEN PRINT "1"; ELSE PRINT "0"; 30 NEXT:PRINT:NEXT:GOTO 10
Running this will display eight lines representing the bits in each of those memory locations. Here is what it looks like when all four arrow keys are held down at the same time:
I see that SPACE is also in its own column, likely by design for this very purpose – games. One would want to be able to detect ARROWS and SPACE independently which would not be possible if any of them used the same column.
With this in mind, we can try to just read those keys (Up, Down, Left, Right and Space) as fast as possible.
Read those keys as fast as possible
To be nice and flexible, we should take the PEEK of one of those values and AND off the bit(s) we care about and act upon that. This allows detecting just the key we want even if something else in that column is also being held down. Since we are going for speed, we’ll just say the user isn’t allowed to do that. Press Up and that’s okay. Press Up and S (another key in the same column) and the player won’t move. Maybe we can improve that later.
Using the first keyboard.bas listing as reference, it looks like it will be easy to detect these five keys by checking the PEEK value against &HF7. As long as only the Up, Down, Left, Right or Space is held down in that column, the value will be &HF7. This gives us something like this:
0 REM arrows.bas 10 CLS:L=16+32*8 20 POKE&H155,&HFF:POKE&H156,&HFF:POKE&H157,&HFF:POKE&H158,&HFF:D=. 30 IF PEEK(&H155)=&HF7 THEN D=-&H20 40 IF PEEK(&H156)=&HF7 THEN D=&H20 50 IF PEEK(&H157)=&HF7 THEN D=D-&H1 60 IF PEEK(&H158)=&HF7 THEN D=D+&H1 70 IF L+D<&H1FF THEN IF L+D>=. THEN L=L+D 80 IF PEEK(&H159)=&HF7 THEN PRINT@L,"O"; ELSE PRINT@L,"X"; 90 GOTO 20
For speed, decimal values have been replaced with hex, and zeros have been replaced with “.” (yeah, it’s weird, but it’s faster). We could further optimize by removing spaces and combining some of the lines (and cheating by replacing the GOTO loop with a FOR/NEXT/STEP 0 hack), but for now, we’ll start with this.
When you run this program, it prints an “X” in the center of the screen. Using the arrow keys you can move the X around, leaving a trail. Hold down space, and you leave a trail of Os. There is limited checking to make sure you don’t try to print off the top or bottom of the screen.
We are now directly PEEKing the KEYBUF keyboard rollover table looking for those specific keys. We have key repeat, and the ability to detect multiple keys at the same time (such as UP+LEFT+SPACE).
Here’s what the code is doing:
- Line 10 clears the screen and sets the PRINT@ location variable to the center of the screen.
- Line 20 resets the four KEYBUF column values of the arrow keys. We don’t do this with the SPACE since it doesn’t seem to be needed (why not???). The movement delta value is set to 0 (this will be added to the PRINT@ location later).
- Line 30-40 checks for UP and DOWN, and set the delta (movement) value to either -32 (moving up) or +32 (moving down).
- Line 50-60 checks for LEFT and RIGHT, and ADD them to any existing delta value. This means UP could have set it to -32 and RIGHT could have added +1 to that, resulting in a delta of -31 (up and to the right).
- Line 70 checks to see if the result of current location plus new delta is within the screen by being less than PRINT@511 or greater or equal to PRINT@0. If valid, it adds the delta to the location variable. Note we are using the “IF THEN IF” optimization trick that I learned about in this video from Robin @ 8-bit Show and Tell.
- Line 80 checks for SPACE and then PRINTs an “X” or an “O” depending on if space is pressed or not.
- Line 90 goes back and does it all again.
Here we have a very simple routine for moving something around the screen using PRINT@ coordinates (0-511). For a game, we would probably want to change this to using POKE screen locations (1024-1535) so we could PEEK to detect walls or enemies and such.
Since the title of this article series has the word “benchmark” in it, I suppose the next thing we should do it look at different ways to do this and find the ones that are faster.
To be continued…