4) Timing and Horizontal Positioning (Minikernel Developer's Guide)
Up until now, I've avoided talking about timing and cycles in too much detail, but now we will take a closer look, as it will be needed to understand horizontal positioning of objects.
Imagine for a moment that you are decorating a house, and you are trying to position a picture on a wall. You ask a friend to hold up the picture against the wall, then walk from the left side to the right as you watch. When it is where you want it, you ask him to stop, and then maybe ask him to adjust it a few more inches to the right or left. That is more or less how horizontal positioning works on the Atari 2600. Instead of setting the horizontal position of an object explicitly, you set a register when the beam is near the position you want for the object, then set another register to fine tune that position.
Scanline Timing and Horizontal Blank
As mentioned briefly in the first lesson, each scanline takes 76 machine cycles to complete. As you may know, the horizontal resolution of the Atari 2600 is 160 pixels (color clocks). Not only are there more color clocks than cycles, but the numbers are not evenly divisible. Why is this?
The first part of the scanline is the "horizontal blank" portion. This is the short period of time after the beam has finished drawing the previous scanline, and before it has begun drawing objects on the current scanline. The horizontal blank period ends after 22 machine cycles.
More precisely, the horizontal blank period ends after 68 color clocks. For each machine cycle, 3 color clocks pass, so 76 machine cycles per scan line equals 228 color clocks, 160 of which are not displayed, and the other 68 occur during the horizontal blank period.
Setting "Coarse Position" with RESP0, RESP1
Let's start with positioning our player 0 lives counter first. We want it to start on the left side, but give a little bit of a margin, so let's say we want to start it at an X position of 5. How do we do this?
To set the horizontal position or the player 0 object, we write to the RESP0 (RESet Player 0) register when the beam is where we want it to be. This is a "strobe register" like WSYNC, so the actual value written to the register is unimportant.
To set the position of the player 0 object correctly, we will have to write to the RESP0 register after the correct number of cycles has passed. An X position of 5 means that 5 visible color clocks have passed. There are 68 color clocks in the horizontal blank period, for a total of 73 color clocks. As mentioned, there are 3 color clocks per cycle, so dividing by 3 will tell us about what cycle we want to hit to set the position correctly.
73 / 3 = 24 ⅓, so we will round to cycle 24. We can fine tune this to hit the exact screen position in the next step.
How do we set the position at that cycle? If we were doing a whole game in assembly with objects that could appear anywhere on the screen, we would need to set the position by running a loop to waste the correct amount of cycles, set the position, then fine-tune the position.
Since we are dealing with static placement, we can set the object position in a simpler way: start a new scanline with WSYNC, wait the correct number of cycles, then set the position. To do this, we use the sleep macro, which uses the specified number of cycles. So, something like this:
sta WSYNC ; Start a new scanline at cycle 0 sleep 24 ; Wait 24 cycles sta RESP0 ; Set player 0 position to current locationThe above code does not take into account the amount of time it takes to execute the "sta WSYNC" command, however. We will talk more about instruction timing later, but for now I will tell you know that this takes 3 cycles. Because of this, to end on cycle 24, we will want to wait only 21 cycles, and the "sta WSYNC" command will account for the other 3:
sta WSYNC ; Start a new scanline at cycle 0 sleep 21 ; Wait 21 cycles sta RESP0 ; Set player 0 position to current locationLet's try adding this to our minikernel right before the loop. Here's the code after making this addition:
minikernel ldy player0height lda #0 sta NUSIZ0 sta NUSIZ1 sta COLUBK sta WSYNC ; Start a new scanline at cycle 0 sleep 21 ; Wait 21 cycles sta RESP0 ; Set player 0 position to current location GraphicsLoop sta WSYNC lda (player0pointer),y sta GRP0 lda (player1pointer),y sta GRP1 dey bpl GraphicsLoop sta WSYNC lda #0 sta GRP0 sta GRP1 rtsSave the changes, compile cannons.bas again, and run the program:
You will notice now that moving player 0 does not change the position of the icon in the minikernel, but moving player 1 still does.
We can set the other player object similarly. We will want to set it so that it is 5 pixels from the right edge, but we need to account for the width as well. To display all 3 lives, we will later be setting the NUSIZx value to 3 copies, close spacing, for a total width of 40 (8 for each copy, and 8 for each space in between). So, starting with 159, subtracting 5 for the margin, then 40 for the maximum width of the tripled player object gives us a starting position of 114. Using the above formula, we add 68 color clocks and divide by 3, giving 60 ⅔, rounding up to cycle 61.
Rather than starting a new scanline, we will add to the existing one. Our "sta RESP0" ended at cycle 24, so we will need to wait 37 more cycles. 3 of these will be used for the "sta RESP1" command, so we will sleep for 34 cycles. Add the following lines after "sta RESP0":
sleep 34 sta RESP1
Save the changes, recompile, and try the game again:
Success! Now the cannons move independently of the life icons.
Note - The above formula is good enough to get us in the ballpark of the correct cycle for our "coarse" object positioning step, but if you wish to determine this exactly, you will need to take into account a color clock (pixel) delay after writing to the appropriate RESxx register. For the missile and ball objects, the delay is 4 color clocks. For the player objects, the delay is 5 color clocks, unless the object is doubled or quadrupled in width, in which case the delay is 6 color clocks. This number would be subtracted from the numerator in the equation above before dividing by 3, and the remainder would be the positive value for the fine adjustment (see next section for details).
Fine Positioning with HMP0, HMP1
We have set the position of our player objects in our minikernel to near where we want them. In this step, we adjust their positions to be exactly where we want them to be. Fortunately this step is a lot simpler than the last one.
By writing a value to the HMP0 (Horizontal Motion Player 0) and the HMP1 (Horizontal Motion Player 1) registers, we can adjust the horizontal position of these object by up to 7 color clocks to the left [-7], or up to 8 to the right [+8]. The table below shows what value should be stored in the appropriate register based on the desired adjustment:
So, how much of an adjustment to each player object do we need to make? To determine that, we need to know where they are now after our coarse adjustment. We could either compute this via the method described in the note above, or we can use the Stella debugger for this purpose.
Run the cannons game in Stella, and hit the backtick key "`" (the one below the tilde "~" key on US keyboards) to enter the debugger.
We will want to see where the player 0 and player 1 objects are placed after setting the coarse position. To do this, we will first need to get the program to stop execution and return to the debugger after the appropriate commands have been run. Looking at our minikernel code, the GraphicsLoop label appears right after the writes to RESP0 and RESP1. We can tell the debugger to stop at this point by typing the following in the debugger prompt:
Having done that, we tell the debugger to continue to run the game until it hits this breakpoint. We can either do that by hitting the same key, or typing "run" at the debugger prompt.
After doing this, the debugger comes up again almost immediately, and we can see in the right pane that we are at the GraphicsLoop label.
Now, in the left pane, switch to the TIA tab, and look at the values displayed in Pos# for P0 and P1. We see the position of P0 is 9, and the position of P1 is 120:
We want those values to be 5 and 114, respectively, so we will want to shift the player 0 object to the left by 4 pixels (-4), and player 1 object to the left by 6 pixels (-6). Looking at the chart above, we need to put the value $40 into HMP0 and the value $60 in HMP1:
lda #$40 sta HMP0 lda #$60 sta HMP1One more thing needs to be done for the fine position adjustment to take effect. We need to do a write to the "HMOVE" register. This is another "strobe register" that is activated by writing to it, regardless of the value. HMOVE needs to be written to right after a WSYNC, so we will add the following code after writing to the fine positioning registers:
sta WSYNC sta HMOVE
Here's all of the minikernel code after our additions:
minikernel ldy player0height lda #0 sta NUSIZ0 sta NUSIZ1 sta COLUBK sta WSYNC ; Start a new scanline at cycle 0 sleep 21 ; Wait 21 cycles sta RESP0 ; Set player 0 position to current location sleep 34 sta RESP1 lda #$40 sta HMP0 lda #$60 sta HMP1 sta WSYNC sta HMOVE GraphicsLoop sta WSYNC lda (player0pointer),y sta GRP0 lda (player1pointer),y sta GRP1 dey bpl GraphicsLoop sta WSYNC lda #0 sta GRP0 sta GRP1 rtsCompile the changes, enter the debugger, set the breakpoint at GraphicsLoop again, run until it stops at GraphicsLoop, and look at the object positions in the TIA tab again. In this case, the positions haven't changed yet, but we see some information in the "Queued Writes" box. Hit the "Step" button on the upper right once:
We see that the "Queued Writes" have cleared, and that our player objects are now at the desired positions: 5 and 114.
Summary and Next Lesson
Objects are positioned horizontally by writing to the appropriate RESxx registers when the beam is near the desired position, and then adjusted to the exact position by writing the appropriate value to the corresponding HMxx register. A scanline consists of 76 cycles, or 228 color clocks (3 pixels, or color clocks, pass for every machine cycle). The first 68 color clocks, or a little over 22 cycles, of a scanline does not get displayed. This is referred to as the horizontal blank period. The TIA tab of the Stella debugger can show us exactly where our objects are positioned.
In our next lesson, we will wrap up the lives portion of our minikernel by displaying the correct number of lives, as well as other miscellaneous enhancements.