Draw score ($C131—C157)
The easiest way to understand what this block of code is doing is look at the state of memory before and after. The main difference is some new entries in the video RAM, from $1CF8
to $1DDF
. Recalling our earlier trick from "Load glyph data ($D86C—D8C6)" (TODO: link) to peek at the video memory, we end up with a set of bytes looking like this:
1CF8: 11111111 11111111 11111111 11111111
1D18: 11 1111 11 1111 11 1111 11 1111
1D38: 11 11 11 11 11 11 11 11 11 11 11 11
1D58: 11 11 11 11 11 11 11 11 11 11 11 11
1D78: 11 11 11 11 11 11 11 11 11 11 11 11
1D98: 11 11 11 11 11 11 11 11 11 11 11 11
1DB8: 1111 11 1111 11 1111 11 1111 11
1DD8: 11111111 11111111 11111111 11111111
This looks exactly like the player's score, when set to zero. Knowing this, it becomes much easier to guess at what our variables are.
C131: 86 03 LDA #$03 A = 3
C133: 97 51 STA $51 Set $2051 = 3
...except this one. $2051
is likely another global variable but it does not seem to be read or changed during a game.
C135: 8E 1C F8 LDX #$1CF8 X = $1CF8 (video_address, video RAM)
C138: 10 8E 20 4F LDY #$204F Y = $204F (score_address)
C13C: BD D8 29 JSR $D829 Jump to $D829
We saw the score start at $1CF8
, so X clearly indicates the video address into which data will be written. $204F
indicates an area of memory which starts with zeroed data but which, when we run the game, reflects the player's score in real-time. We therefore need no hesitation in labelling these variables video_address
and score_address
.
In fact byte $204F
contains the hundreds and thousands,while byte $2050
contains the tens and ones. Each digit uses four bits (officially, but not popularly, know as a nibble). For example, if the score is 1550, then the individual bits will look like this:
Bits: 0001 0101 0101 0000
Number: 1 5 5 0
D829: 34 30 PSHS ,Y,X Push video_address, score_address to stack
D82B: 86 02 LDA #$02 A = 2
D82D: 34 02 PSHS ,A Push A to stack (loop_count_outer)
We push these variables to the stack, including a simple 2 which I know will only be used for looping. I call it loop_count_outer
because there will shortly be an inner loop too.
D82F: A6 A4 LDA ,Y A = score_address->val
D831: 8D 10 BSR $D843
The accumulator is loaded with the byte containing the 2 right digits of the player score.
D843: 34 30 PSHS ,Y,X Push video_address, score_address to stack again
X and Y, which still contain the video_address
and score_address
respectively, are pushed to the stack again only for convenience; we want to access these again soon, but if we wanted to do so without making copies, we would have to navigate up the stack beyond loop_count_outer
. Fiddling with the stack like that could be considered bad form, as it does make it easier to introduce subtle bugs.
We can call these copies video_address2
and score_address2
.
D845: 44 LSRA A >> 1
D846: 44 LSRA A >> 1
D847: 44 LSRA A >> 1
D848: 44 LSRA A >> 1
We know that A contains a byte of the player score, with each of the two nibbles accounting for a digit each. Right-shifting A four times effectively isolates the high nibble digit, by moving it into the low nibble position and implicitly zeroing the high nibble. The entire byte in A now represents that one digit instead of two.
We then call - with needing to actually call it, as it happens to start from the next instruction - the function draw_digit(A)
, with the first digit that we want to draw.
# draw_digit(A)
D849: C6 10 LDB #$10 B = 16
D84B: 3D MUL digit * B
D84C: C3 D8 D7 ADDD #$D8D7 D = D + $D8D7 (ROM address)
D84F: 1F 02 TFR D,Y Y = D
D851: AE E4 LDX ,S X = video_address2
We start by finding where the glyph data for this digit exists on the cartridge ROM, by calculating an offset from $D8D7. From this we can extrapolate exactly where each number's data is stored. As it turns out they are simply store 16 bytes apart.
number | Glyph in ROM |
---|---|
0 | $D8D7 |
1 | $D8E7 |
2 | $D8F7 |
3 | $D907 |
4 | $D917 |
5 | $D927 |
6 | $D937 |
7 | $D947 |
8 | $D957 |
9 | $D967 |
D853: 86 08 LDA #$08 A = 8
D855: 34 02 PSHS ,A Push A to stack (loop_count_inner)
D857: EC A1 LDD ,Y++ D = Y, Y = Y + 1
D859: ED 84 STD ,X X->val = D
D85B: 30 88 20 LEAX +$20,X X = X + 32
D85E: 6A E4 DEC ,S loop_count_inner = loop_count_inner - 1
D860: 26 F5 BNE $D857 Loop until loop_count_inner = 0 (8 loops)
This is a simple function; re-written naively in C it would look more like this:
for (int i = 0; i < 8; i++)
{
memory[X] = memory[Y];
memory[X+1] = memory[Y+1];
X += 32;
Y += 1;
}
This small loop copies the data from cartridge ROM into video memory. Reading the ROM data is easy, as we just loop 8 times, reading 2 bytes each time. When writing it, we separate every 2 bytes with a 32-byte gap; as a complete horizontal line across the screen uses 32 bytes, this displays as 2 bytes per line.
In any case it's clear that we're populating a collection of 8 (still unknown) data structures that are 32 bytes wide.
D862: 32 61 LEAS +$01,S Clean-up loop_count_inner
D864: 35 B0 PULS ,X,Y,PC Clean-up video_address2 & score_address2, return to caller
We clean up all the stack variables, simultaneously using the trick of pulling the PC off the stack to simulate an RTS
, and continue where we left off.
D833: 30 02 LEAX +$02,X X = X + 2 (video_address + 2)
D835: A6 A0 LDA ,Y+ A = Y->val, Y = Y + 1
D837: 8D 2D BSR $D866
We've now popped the stack back to the point where X holds the initial video_address
of $1CF8
, and Y holds the initial score_address
of $204F
. We offset the video address by 2 bytes - which positions us 8 pixels to the right, at the insertion position for the next digit. The score address will actually remain as $204F
, though Y is then incremented. This means that the next time it's accessed it will be pointing to $2050
, which contains the two left-most digits of the score.
D866: 34 30 PSHS ,Y,X Push video_address2 & score_address2 to stack
D868: 84 0F ANDA #$0F A = A & 00001111
D86A: 20 DD BRA $D849 Call draw_number(A) on second digit
We call the draw function again, but this time we isolate the second digit. This is more simply done; recall that to isolate the high nibble we needed 4 right-shifts, but to isolate the low nibble we can simply remove the high nibble. This can be done with a simple AND mask.
A note on unorthodox branching
Note that we use a BRA
here instead of a JSR
, so the address of the current execution point is not placed on the stack. This means that when copy_rom_to_ram1()
finishes and calls RTS
(or uses PULS ,PC
simulates it), we'll be returning to wherever we last used a BSR
or JSR
. In this case that will be at $D839
. This execution flow is not something that can usually be achieved in a higher-level language, though it's really the same as:
JSR $B849
RTS
This form makes the execution flow explicit, at the cost of one extra byte and one extra instruction.
D839: 30 02 LEAX +$02,X X = X + 2
D83B: 6A E4 DEC ,S loop_count_outer = loop_count_outer - 1
D83D: 26 F0 BNE $D82F Loop until loop_count_outer = 0 (2 loops)
Having now drawn two digits, the entire code from $D82F
is repeated, but by now
score_address
will be 1 byte higher at$2050
instead of$204F
, andvideo_address
will be 4 bytes higher at$1CFC
instead of$1CF8
.
This will draw the remaining two digits of the player's score onto the screen.
D83F: 32 61 LEAS +$01,S Clean-up loop_count_outer
D841: 35 B0 PULS ,X,Y,PC Clean-up video_address, score_address and return to caller
Clean-up, as usual, before continuing. Here is the fruit of our labours: