How do I jump to a subroutine using indirect addressing?
Moderator: Moderators
- SecretServiceDude
- Posts: 99
- Joined: Wed Oct 22, 2008 3:49 pm
- Location: Los Angeles, CA
How do I jump to a subroutine using indirect addressing?
I'd like to be able to jump to certain subroutines (e.g. different enemy updates) in my game using indirect addressing in order to avoid wasteful if-else checks. So far, I've been using the JSR instruction to jump to subroutines; however, that instruction appears to support absolute addressing only.
I'm thinking it's possible to use the JMP instruction (which supports indirect addressing), but that would require pushing the address - 1 of the following instruction onto the stack in order to "play nice" with the RTS instruction that terminates the subroutine, wouldn't it?
I'm afraid of getting really nasty "off by one" errors if I push the wrong address onto the stack. What is the proper way to set up a correct jump?
Or is there a better method altogether?
I'm thinking it's possible to use the JMP instruction (which supports indirect addressing), but that would require pushing the address - 1 of the following instruction onto the stack in order to "play nice" with the RTS instruction that terminates the subroutine, wouldn't it?
I'm afraid of getting really nasty "off by one" errors if I push the wrong address onto the stack. What is the proper way to set up a correct jump?
Or is there a better method altogether?
- SecretServiceDude
- Posts: 99
- Joined: Wed Oct 22, 2008 3:49 pm
- Location: Los Angeles, CA
- SecretServiceDude
- Posts: 99
- Joined: Wed Oct 22, 2008 3:49 pm
- Location: Los Angeles, CA
Let's say you have several different types of enemies in your game, and you want to update them all. One way is to check each enemy to see what type it is, then select the appropriate update routine. In pseudocode:Roth wrote:SSD, I have recently heard about the technique your describing actually, what weird timing! I still don't understand how it works though. But I'm interested in what Blargg just said... what the? Can someone explains how that works?
Code: Select all
if (enemyType == SOLDIER)
JSR UpdateSoldier
else if (enemyType == NINJA)
JSR UpdateNinja
else if (enemyType == BOSS)
JSR UpdateBoss
etc...A better way is to store an array of addresses of the various update routines. Then use the enemy type as an index into that array. In pseudocode:
Code: Select all
SOLDIER = 0
NINJA = 1
BOSS = 2
etc...
; Initialization (performed once)
UpdateRoutine[SOLDIER] = UpdateSoldier
UpdateRoutine[NINJA] = UpdateNinja
UpdateRoutine[BOSS] = UpdateBoss
etc...
; Update (performed every frame)
JSR UpdateRoutine[enemyType]
I just wasn't sure how to pull that technique off in 6502 assembly.
Did that answer your question?
JMP indirect doesn't seem all that useful for jump tables, since you have to copy the address to a fixed address first. Pushing the address (minus one!) on the stack and then returning to it is simplest. Note the subtlety: the caller JSRs to do_action, pushing the final return address on the stack. Then the table-based do_action routines push the address of the routine to "jump" to and "return" to that. At that point, one of the three action_n routines is being executed, with the original return address on the stack for it to return to when done.
Sorry for the length of the code below.
Sorry for the length of the code below.
Code: Select all
; Example that does actions 0, 1, and 2
main:
; Loop through values of A
lda #0
: pha
jsr do_action_a ; test one of the methods
pla
clc
adc #1
cmp #3
bne :-
rts
; The actions performed
action_0:
print_str "0"
rts
action_1:
print_str "1"
rts
action_2:
print_str "2"
rts
; Different ways to invoke action based on value of A
do_action_a:
cmp #0
bne :+
jmp action_0
: cmp #1
bne :+
jmp action_1
: cmp #2
bne :+
jmp action_2
: rts
do_action_b:
asl a ; x = a * 2
tax
lda table+1,x ; push address to "jump" to
pha
lda table,x
pha
rts
table:
.addr action_0-1, action_1-1, action_2-1
do_action_c:
tax
lda table_h,x ; push address to "jump" to
pha
lda table_l,x
pha
rts
table_h:
.byte >(action_0-1), >(action_1-1), >(action_2-1)
table_l:
.byte <(action_0-1), <(action_1-1), <(action_2-1)- SecretServiceDude
- Posts: 99
- Joined: Wed Oct 22, 2008 3:49 pm
- Location: Los Angeles, CA
-
Celius
- Posts: 2159
- Joined: Sun Jun 05, 2005 2:04 pm
- Location: Minneapolis, Minnesota, United States
- Contact:
In my game, my NMI routine looks like this:
$20/$21 hold the address of "-" initially. The routine is fixed so it never goes beyond a page boundary, so I don't have to do 16-bit additions. Anyways, basically I have all the stuff I want to do in the NMI routine in a list. This list is made up of the addresses of the routines I want to go to, and they're stored in $0-$1F, so that's 16 different routines I can go to in the NMI. Instead of doing an "RTS" at the end of each routine, I go "JMP Return". $20/$21 basically holds the position of where the program last was. Though I have to add 3 to it to get past the instruction it was just at. I designed this specifically for like what you were saying. I hated all those ugly If/Else statements. And I also designed this for heavy gear-shifting in the code. By that I mean like when you switch from going into game-mode where you're jumping around killing enemies to going to the menu, where the engine will be doing very different things.
Oh, and if any routine in that list isn't doing anything, it just jumps directly to "Return". I have it like this for my game loop, too. Though it's a bit different.
Code: Select all
NMI:
pha
txa
pha
tya
pha
-
jmp ($00)
jmp ($02)
jmp ($04)
...
jmp ($1C)
jmp ($1E)
lda #<-
sta $20
pla
tay
pla
tax
pla
rti
Return:
clc
lda $20
adc #3
sta $20
jmp ($20)
Oh, and if any routine in that list isn't doing anything, it just jumps directly to "Return". I have it like this for my game loop, too. Though it's a bit different.
- SecretServiceDude
- Posts: 99
- Joined: Wed Oct 22, 2008 3:49 pm
- Location: Los Angeles, CA
In order to have vastly different NMIs through my game, I have the NMI vector point to RAM. I reserved 3 bytes of RAM for it, and usually I have a "JMP $XXXX" instruction there, and I change the address depending on what section of the game is being executed. Sometimes I place an RTI in place of the JMP, to "disable" NMIs without touching the PPU registers.
In order to avoid the catastrophic case of an NMI firing while the address is partially updated (something that would crash the program), I first change the JMP to an RTI, and then I change the address and restore the JMP.
In order to avoid the catastrophic case of an NMI firing while the address is partially updated (something that would crash the program), I first change the JMP to an RTI, and then I change the address and restore the JMP.
-
Celius
- Posts: 2159
- Joined: Sun Jun 05, 2005 2:04 pm
- Location: Minneapolis, Minnesota, United States
- Contact:
Yeah, that's one thing I have to consider when using my method. In general, the NMI and Game Loop sharing sections of RAM is dangerous. For example, I didn't even think about it until a little while ago. In my NMI I need to use different temporary variables than I do in the game loop. If the NMI is fired and destroys those values, my game = worse catastrophe than when the meteor killed the dinosaurs. Anyways, I rarely change the routines in that list, so I disable the NMI before I update them, and turn it on right after that. Though it's pretty impractical sometimes.
Yeah. I've dealt with exactly that issue before...in a commercial game.Celius wrote:If the NMI is fired and destroys those values, my game = worse catastrophe than when the meteor killed the dinosaurs.
(granted, the game was never released, but that doesn't excuse some of the amateur programming errors they made...)
That's exactly how Final Fantasy 3 works.In order to have vastly different NMIs through my game, I have the NMI vector point to RAM. I reserved 3 bytes of RAM for it, and usually I have a "JMP $XXXX" instruction there, and I change the address depending on what section of the game is being executed. Sometimes I place an RTI in place of the JMP, to "disable" NMIs without touching the PPU registers.
In order to avoid the catastrophic case of an NMI firing while the address is partially updated (something that would crash the program), I first change the JMP to an RTI, and then I change the address and restore the JMP.
Celius, your idea of a jump table is great, but could be optimised.
You could place the NMI in RAM so that the jump table would be a jsr table, and you just have to replace the argument of the jsr code to change the routine called. You could also call a routine that stores the content of A,X and Y into variables (instead of pushing them) so that even that step can be done from RAM (and is fully optionnal), and you can change the initial jsr to a rti to disable NMIs if needed. That way the routines called in the NMI can simply rts and don't have to do a jmp Return. Also, the exact same routine can be called from the main code if needed.
Alternatively, if having the jump table in RAM bothers you because you have to interleave the addresses with "rts" you could do the following thing :
Code: Select all
JumpToNMISubroutine
lda $20
inc $20
asl A
tax
lda $00,X
pha
lda $01,X
pha
rts
Useless, lumbering half-wits don't scare us.