Assembly 101: Dynamic subroutines

Earlier, I wrote something about subroutines, how they work, what they do and why you need them. This time, I’ll explore the somewhat more advanced concept of dynamic subroutines, i.e. calling one of a few possible subroutines based on an external value.

This method combines the concepts of subroutines and lookup tables, so if you want, you can read up on those first.

Case study: a monster takes action based on its state

Imagine you have a variable which holds the current state of a monster. This variable determines the action a monster should take. For example, if the current monster’s state is 0, the monster should jump; if it’s in state 1 the monster will shoot a projectile, and in state 2 the monster will teleport. Also, you’ve got the subroutines for these actions ready. You could chain these together using a bunch of CMP’s, BEQ’s, JSR’s and JMP’s… but that might get messy real quick. Using dynamic subroutines (or trampolines, as NESmaker call them) might be the better way to go.

The idea: we load the address of the subroutine in a temporary variable and then do an indirect jump to that address. Unfortunately, 6502 ASM does not support indirect JSR, which means you can’t JSR to a variable value. That’s what the trampoline method is for: instead of a JSR to a variable address, we JSR to a static subroutine, which in turn JMP’s to the actual subroutine’s address, because JMP does support indirect addressing. Since that subroutine ends in a RTS, the script will return to the last time a JSR operation was executed. This allows us to simulate a dynamic subroutine call.

Here I’ll give you the complete assembly concept, which I will break down after.

do_dynamic_subroutine:
    LDY current_monster_state

    LDA tbl_monster_state_action_hi, y
    STA temp16

    LDA tbl_monster_state_action_lo, y
    STA temp16+1

    JSR +do_temp16_subroutine
    JMP +done_temp16_subroutine

+do_temp16_subroutine:
    JMP (temp16)

+done_temp16_subroutine:
    RTS

sub_monster_jump:
    ;; Jump
    RTS

sub_monster_shoot:
    ;; Shoot projectile
    RTS

sub_monster_teleport:
    ;; Teleport
    RTS

tbl_monster_state_action_hi:
    .db #>sub_monster_jump, #>sub_monster_shoot, #>sub_monster_teleport

tbl_monster_state_action_lo:
    .db #<sub_monster_jump, #<sub_monster_shoot, #<sub_monster_teleport

So, what happens here?

First, we load the current monster state in the Y-register. The current monster state should hold a value of 0 (jump), 1 (attack) or 2 (teleport). Then, we load the address of the subroutine we need to call based on this state value. We use the variable temp16 and temp16+1 to store this address. Because a memory address is a 16-bit value, and the NES is an 8-bit system, we need to use two variables – one for the high byte, and one for the low byte. This is reflected in the lookup tables at the bottom of the script: the first table, tbl_monster_state_action_hi, holds the high byte values of the address, and the second table, tbl_monster_state_action_lo, holds the low byte values. This is what the greater-than and less-than characters do. STA temp16+1 here means “store the value of the accumulator at the address that’s one byte to the right of temp16”.

Now temp16 holds the high byte, and temp16+1 holds the low byte of the subroutine we want to call. But, as stated before, JSR (temp16) is not a valid operation in 6502 assembly. So instead, we JSR to the static address +doTemp16Subroutine, which in turn jumps to the memory address held by temp16 and temp16+1. This is one of the three defined subroutines, based on the monster’s state, which is exactly what we wanted! When the subroutine has finished, it RTS’s to where the subroutine was called. The next line JMPs to +done_temp16_subroutine, which then returns to wherever you did a JSR do_dynamic_subroutine.

Complex stuff? Maybe somewhat – NESmaker even calls this an “advanced concept” in its source code – but I hope this little post clarified a bit what a trampoline is, what it does, how it works and how to apply it to your project if needed.