NicoLarve, on Sun Oct 12, 2008 3:15 AM, said:
How would you convert this mechanism into a BCD based one ?
Take a look at how most games do it. Whenever scoring points, a SED instruction takes place - instructing the processor to do subsequent ADC's and SBC's in
decimal mode. Normally, when $01 is added to $09 you get $0A. In decimal mode, hex numbers are treated as decimal...$01+$09=$10. Doing a CLD at the end of the scoring routine clears off this mode. So instead of updating each digit seperately, you are doing 2 at once.
Quote
Does it consists in a conversion from 3 BCD bytes to 12 Hex (the temp you described below) ones just before displaying the digits ?
Not necessarily "just before" (which can be tricky in the middle of a display kernal). But sometime before it's needed. What is involved is a loop that takes a BCD value, breaks it in half into seperate
nybbles, and uses each nybble to update temp ram where the bitmap addresses go.
NOTE: a nybble is a 4-bit portion of an 8-bit byte. The 4 leftmost bits is the "high" nybble, and the 4 rightmost bits is the "low" nybble. Hex numbers follow this same pattern. In value $19 for example, the high nybble = 1, the low nybble = 9. This works out really well when coupled with BCD...because [a] the digits are already the proper values that you want to see, and [b] digits 0 through 9 are the same value in both hex AND decimal.
But in order to display them, these digits need to correspond to addresses of bitmaps in ROM. That is where temporary ram comes in...
First, a BCD score value (2 digits, 0 through 9) is loaded into the accumulator. There, you can use an AND #$0F instruction to strip away the high nybble - we are not interested in it just yet. This gives you a value of 0 to 9 (remember, BCD addition does away with hex values A through F).
LDA score_variable ;load one of the 2-digit variables
AND #$0F ;keep only the low digit
There's a couple of standard ways to deal with getting the address from that point. Some games multiply the value by how many bytes are in each bitmap, for example. In that case, all that needs to be done is perform 3 ASL instructions if the bitmaps are all 8 bytes long.
ASL ;multiply by 2
ASL ;multiply by 4 (2^2)
ASL ;multiply by 8 (2^3)
This method assumes that your bitmaps are all 8 bytes apart...running consecutively.
Other games use a lookup table that holds the addresses of the bitmaps. In that case, you'd transfer the value of the accumulator to one of the index registers (X or Y) and use it as an offset to read from this table.
TAX ;move the digit to the X register
LDA LookupTable,X
The lookup table itself can be located anywhere. All it needs to contain is the LSB (least-significant byte) address of each digit bitmap...
LookupTable:
.byte <Zero_Bitmap
.byte <One_Bitmap
.byte <Two_Bitmap
.byte <Three_Bitmap
.byte <Four_Bitmap
.byte <Five_Bitmap
.byte <Six_Bitmap
.byte <Seven_Bitmap
.byte <Eight_Bitmap
.byte <Nine_Bitmap
The clear advantage of doing it this way is that each bitmap can be located anywhere...even share bytes between digits. The disadvantage is that you need to waste an index register to get it (your routine might have the X & Y registers already busy with something else).
Anyway, now we have our LSB for the first digit...and we can plunk it down into temp ram! Huzzah!
STA Print_This_11 ;more about this "11" in a minute...
The MSB is easy. As long as all bitmaps are in a single stretch of 256 bytes (1
page of ROM), you can just get it from any of them...don't forget the # to instruct the opcode that it's an immediate value...
LDA #>Zero_Bitmap ;load the page number where digit bitmaps are
STA Print_This_12 ;whats this 12 already??
The numbers 11 and 12 indicate that these 2 numbers you stored represent only ONE (1) of the digits to be printed. "Arrgh!" Don't start celebrating just yet.
To get the next digit in line...you again grab the score variable in question, but this time you keep the high nybble and work with it.
LDA score_variable ;load one of the 2-digit variables
Just as before, there's 2 common ways of dealing with it. You can either waste ROM by multiplying it by the number of bytes in a bitmap, or waste a register loading from a lookup table.
But first, you need to realise that the digit is ALREADY multiplied by 16 (it's in the high nybble spot). You need to move it lower. How much lower depends on the method. If you were multiplying by 8 for the low nybble...you can just
divide by 2 for the high...
AND #$F0 ;keep only the high nybble
LSR ;divide by 2
If you were using a lookup table, divide by the full 16 so it can be used in the index register (like before)...
LSR ;divide by 2
LSR ;divide by 4 (2^2)
LSR ;divide by 8 (2^3)
LSR ;divide by 16 (2^4)
TAX ;move the digit to the X register
LDA LookupTable,X
No need for an AND #$F0 on that one. 4 LSR's in a row wipe out the lower nybble and replace it with whatever was high.
Now it's handled like before...use the value to update the temp...
STA Print_This_9 ;store the LSB
LDA #>Zero_Bitmap
STA Print_This_10 ;store the MSB
Looks pretty good so far, eh?
Uh oh...this only did the first 2 digits of the score...but what if there are six?
Right, the whole shebang really belongs in a loop (unless you want to do all the digits with seperate code). Remember how I mentioned that you might have an index register busy with something else? Well, to do this loop efficiently you need to burn BOTH of them...
LDX #11 ;the temps Print_This are on 12 consecutive bytes
LDY #2 ;The score is on 3 consecutive bytes...index 0, 1, and 2
Loop: ;this is just a label to return to later
LDA #>Zero_Bitmap ;going in reverse order now...so do the MSB first...
STA Print_This1,X ;store the address using the X index
DEX ;reduce the Print_This index
LDA score_variable,Y ;load one of the 2-digit variables using the other index
AND #$0F ;keep only the low digit
ASL ;multiply by 2
ASL ;multiply by 4 (2^2)
ASL ;multiply by 8 (2^3)
STA Print_This1,X ;store the address
DEX ;reduce the Print_This index
LDA #>Zero_Bitmap ;load...
STA Print_This1,X ;store...
DEX ;reduce the Print_This index
LDA score_variable,Y ;load that same 2-digit variable using the Y index
AND #$F0 ;keep only the high nybble
LSR ;divide by 2
STA Print_This1,X ;yadda
DEX ;yadda
Now we are near the end of the loop. You just need a branch to tell it when to go back and when to stop. The Y index is already counting from 2 to 0...so use that.
DEY ;reduce the score index (finally!)
BPL Loop ;...and branch until it falls negative
But wait just one doggone minute. That only works for consecutive bitmaps that waste a lot of rom!
Since both indexes are going to be needed...you need to store one of them mid-loop to do it the other way. Fortunately, there are 2 handy RAM addresses that you can reuse to do this. The MSB never changes...we just need to stick it in the temps before needed. For simplicity, use the lowest ones...Print_This2 and/or Print_This4...
To save time, I'm just going to reduce X four steps at a time...
LDX #8 ;the loop is done in 4-byte chunks...so we only need the values 8, 4, and 0
LDY #2 ;The score is on 3 consecutive bytes...index 0, 1, and 2
Loop: ;this is just a label to return to later
LDA score_variable,Y ;load one of the score variables
STA Print_This1+3 ;store the full value temporarily
AND #$0F ;keep only the low nybble
STY Print_This1+1 ;store the Y index so we can reuse it mid-loop
TAY ;and there it goes...Y has been changed to the table offset
LDA LookupTable,Y ;load the LSB addy
STA Print_This1+2,X ;and store it to one half of the 4-byte chunk
LDA Print_This1+3 ;Get back the original 2-digit score value
LSR ;divide by 2
LSR ;divide by 4 (2^2)
LSR ;divide by 8 (2^3)
LSR ;divide by 16 (2^4)
TAY ;move the digit to the Y register
LDA LookupTable,Y
STA Print_This1+0,X ;and store it to the other half of the 4-byte chunk
STY Print_This1+1 ;Now that the Y register has been reused, we can reload the index
LDA #>Zero_Bitmap ;and do both of the MSB's in one go...
STA Print_This1+1,X
STA Print_This1+3,X
DEX ;...and reduce the X register in 4 steps...
DEX
DEX
DEX
DEY ;reduce the score index (finally!)
BPL Loop ;...and branch until it falls negative
On the passes though the loop, the Y and A temps are safely stuck in the last addresses updated. An effective reuse of the same ram. Not quite as short as the other way, but you save ROM in the long run. Cycle time used, however, is quite a bit more.
Either method gives you the stretch of 12 bytes to use in a kernal's printing routine. The names have been changed here for clarity...but you should be able to see how each loop accomplished the task.
The catch:
Cycle time. You only get 76 cycles per scanline...and the way that the score/fuel/logo gfx is set up doesn't allow for any. So the desired routine would need to be executed waaaay before those are drawn (most likely, before the top of the screen is drawn).
* drops the stick of chalk
Quote
Do you mean there's so enough free CPU cycles in River Raid to call a subroutine (spare too much time isn't it) ?
I didn't analyzed the code enough to answer this but the CPU seems to be really busy in this game !
Looks can be deceiving. You just need to find a spot where you can scrounge enough cycle time to handle it. Most games have a bit of "just-in-case" room for worst-case scenarios (i.e. when loops and things end up taking the long way on a single frame). Just need to find out where. Above time-wasting loops is a good hunch. Look for something like:
Loop:
LDA INTIM
BNE Loop
...and place your BCD conversion routine above it.
Another problem is that this only works for the first 6-digit deal. Fortunately, since the MSB's have already been dealt with...you can just rewrite the LSB's for an additional 6-digit deal
without a loop. That saves cycle time, and should only take a couple of scanlines to update. (up to) 6 reads and (up to) 6 stores. Wastes ROM going the long way...but oh well.
Edited by Nukey Shay, Sun Oct 12, 2008 7:31 AM.