Indexed Indirect Addressing
Posted on
By Kodiak
Many, if not all, 6502 coders on the Commodore 64 are familiar with Indirect Indexed Addressing, but by all accounts very few have ever had reason to use its more esoteric cousin, the mysterious Indexed Indirect Addressing.
However, in the course of developing the "swarm" effect for Parallaxian, a rare case use emerged, specifically with regard to setting the MSB condition of each plexed sprite within the swarm.
The sprites in the swarm are plexed by the NMIs which operate on a minimalist "fast-in, fast-out" basis, with y- and x-positions set by the NMI along with pointers and colours and, of course, the tricky MSB conditions, as per the schematic below:
As intimated above, the coding brief for this required that everything be as lightweight and fast as possible within the NMI handlers, so I wanted to perform the logic for the MSB conditions outside the NMIs to avoid losing CPU time with branch testing inside the NMIs.
We also must update each plexed sprite's MSB condition once per frame, independently of the others in the swarm, and do so by using just one subroutine to keep RAM overhead to a minimum .
So the idea is to set the MSB condition using a zero page value with an AND or ORA instruction that would be hard-written into each NMI handler once per frame by the raster interrupt (IRST) handler that deals with the lateral (x-direction) position updates for the swarm, thus requiring the IRST to be able to quickly modify the instruction dealing with the MSB condition inside each NMI handler.
(The actual value used by the relevant logical instruction in the NMI could much more easily be written into, and then read from, a ZP variable).
Where the MSB = 0, the relevant NMI handler would have to do this:
LDA $D010
AND #%10111111 ; (Mask out sprite #06 as that's the one used in the swarm)
STA $D010
And where the MSB = 1, the relevant NMI handler would have to do this:
LDA $D010
ORA #%01000000 ; (Mask in sprite #06)
STA $D010
But remember, we need to write the actual instruction into the NMI handler once per frame, in what is a pretty normal case of self-modifying coding practice; for example, in the truncated snippet from the NMI handler below, the instruction in NMIMSBSETROW0 has to be written to that address once per frame by the IRST that sets the MSB positions for all of the sprites in the swarm:
NMIHANDLER2 | STA ZPNMIHOLDA | ; "Stack" the A-reg |
; Set sprite y-pos + pointer | ||
LDA #48 | ||
STA VICSPR6YPOS | ||
NMISETPOINTROW0 | LDA #103 | ; SELF-MODIFIED value |
STA SPRPOINTER6 | ||
; Set sprite MSB | ||
LDA $D010 | ||
NMIMSBSETROW0 | ORA ZPSWARMMSBROW0 | ; SELF-MODIFIED instruction |
STA $D010 | ||
; Set sprite x-pos + colour | ||
LDA ZPSPR6XPOSROW0 | ||
STA VICSPR6XPOS | ||
NMISETCOLORROW0 | LDA #00 | ; SELF-MODIFIED value |
STA VICSPR6COLOR | ||
; Set NMI "vectors" | ||
LDA #<NMIHANDLER3 | ||
STA $FFFA | ||
LDA #>NMIHANDLER3 | ||
STA $FFFB | ||
; NMI exit tasks | ||
LDA ZPNMIHOLDA | ; Recover A-reg | |
JMP $DD0C | ; = BIT $DD0D + RTI |
Naturally, the IRST has to be able to exactly write to the correct address in memory within each NMI handler for that fast MSB-setting code, and it must do so from a single subroutine, so what we do is make the actual instructions (AND or ORA) to be written into the NMI handler done so indirectly through vectors held in a LO/HI byte format in dedicated zero page variables.
In other words, we use indexed indirect addressing to do it, as per the dumbed-down snippet from within the relevant IRST handler, shown below:
SWARMSETXJ1 | (tasks @ loop start) | ; Y-reg = loop counter |
; MSB = 0 @ this stage | ||
SWARMSETMSB0 | LDA #%10111111 | ; Mask for sprite 06 with MSB = 0 |
STA ZPSWARMMSBROW0,Y | ||
LDA #$25 | ; #$25 = AND in ZP mode | |
JMP SWARMNEXTROWTEST0 | ||
; MSB = 1 @ this stage | ||
SWARMSETMSB1 | LDA #%01000000 | ; Mask for sprite 06 with MSB = 1 |
STA ZPSWARMMSBROW0,Y | ||
LDA #$05 | ; #$05 = ORA in ZP mode | |
SWARMNEXTROWTEST0 | LDX #00 | ; SELF-MODIFIED value |
STA (ZPMSBLOINSROW0,X) | ; INDEXED INDIRECT! | |
SWARMNEXTROWTEST | INY | ; Increment swarm row counter |
CPY #$06 | ; 6 rows to update | |
BEQ SWARMRESETROTEST | ; Quit loop if counter = 6 | |
TYA | ||
ASL A | ; Multiply counter by 2 | |
STA SWARMNEXTROWTEST0+1 | ; Store as index | |
BCC SWARMSETXJ1 | ; Branch to start of loop | |
SWARMRESETROTEST | (continue with IRST) |
CONCLUSION: Where you need a fast operation within a loop to write to an absolute (i.e. 16-bit) address in RAM, where the target absolute address changes with each
iteration of the loop as a function of the loop counter, indexed indirect addressing is ideal (so yes, it's something of an outlier case use).
In the case of Parallaxian, the (unspoken) brief stated:
- To minimise impact on the IRST handlers' on-screen operations, the NMI handlers had to execute ultra fast and thus consume a minimal amount of CPU cycles, to which end logic tests + branching within the NMIs had to be avoided; instead, any logic should be performed outside the NMIs and the results hard-written back into the NMIs in the form of an ORA instruction or an AND instruction, complete with appropriate masks, to ensure the MSB value is always correct for each sprite plexed by the NMIs.
- The code writing to the NMI handlers to modify the instructions as described would be executed from within the IRST schema once per frame for each of the 6 NMI plex zones and it too, given that it had to run from a RAM-efficient loop, had to execute as quickly as possible, so extraneous or bloaty instruction sequences had to be avoided (which, btw, is a general principle I use in Parallaxian).
- A C128 version of the game would not need the limitation of performing this via a loop, as RAM is much more abundant on that platform.
- The "heavy lifting" calculations for the swarm effect were to be performed by the game's main loop.
So hopefully by now you can see why I ended up using indexed indirect addressing for the swarm effect; it's the only thing that meets both the CPU cycle consumption and RAM efficiency
requirements of the coding brief!
There is a slight downside, though; I had to sacrifice 6 x 2 = 12 zero page locations to hold the target addresses within the NMI handlers and I also had to have a subroutine during the game's
initialisation
that writes the addresses into those zero page locations in LO/HI byte form, but it still represents a RAM saving compared to unrolling the loop that updates the MSB instruction writes
to the NMIs from the IRST and remember, the number one priority was a performance gain in terms of CPU cycle expenditure during the NMI handlers plexing the sprites and during the IRST
that modifies the logic instructions within the said NMI handlers.
____
PS: Don't forget to check the home page regularly for more articles like this.
And of course, kindly
subscribe to my YouTube channel!
Last of all, for additional short snippets of content, check out the posts on my Ko-fi page.
Help Make Parallaxian Happen!
...and get special perks!
Progress on Parallaxian has slowed down since summer 2021 for several reasons, one of which has been the very low level of support from
the C64 scene which has made it difficult to continue justifying to my family the long hours of hard work a project as complex as this requires.
Now, I understand these are difficult times and I admit I am not entitled to any support at all, but it really does encourage me to continue developing
this sensational game when you make a Paypal donation.
And as a special thank you, all who do this can enjoy the following perks:
- Your name credited in the game (unless you opt out of it if you have the same kind of incognito hermit tendencies I do).
- Access to the ongoing beta-testing of the game (unless you would prefer not to see it before its release date).
- The finished game on media (e.g. cartridge) for FREE one week before its release.