Assembly 101: Macros and subroutines

Back in 2022, I promised I’d get back to you on the topic of macros versus subroutines. Sorry for the delay, but here it is! This posts recaps the last time I wrote about macros, then shows you how to use subroutines as an alternative.

If you’re new to NESMaker, or if you’ve made a game or two without really digging into the assembly code, you probably have come across and used a macro every now and then. But did you know that some macros can really eat up precious bytes, and that sometimes a subroutine would be more efficient? Let me try to explain the difference between the two, and their pros and cons.

(Estimated reading time: 5 minutes)

What’s a macro exactly?

A macro actually is a bit like a blueprint. It contains the base of code that can be applied in the game at any desirable time. For example, the ReturnBank macro has a few lines of code that return the game’s dynamic bank back to the previous bank number. Instead of having to type out these lines of code every time you want to return, you can just type ReturnBank and have the assembler put the correct code in place for you.

A macro can also take arguments, which are values that can be changed before they are applied to the code. For example, the SwitchBank macro takes one argument that tells the macro which bank number to switch in. So SwitchBank #$00 would switch to bank $00, for example. Macros can take more than one argument; AddValue, for example, takes four.

How many space does a macro take in my game?

That entirely depends on what the macro does under the hood, and how many times the macro is applied in your game. In fact, if you add a macro to your game, but never use it, it takes up exactly zero bytes. The application of a macro is kind of like a stamp: whenever you use the macro, the “macro-line of code” gets replaced with the contents of the macro when the game is being assembled. That means, for example, if the macro contains 64 bytes of byte-code, it will use 64 bytes of ROM every time you use the macro in your game.

Macros and subroutines; pros and cons

Instead of a macro, you could also use a subroutine. In fact, quite a few NESMaker macros use subroutines under the hood. Take a look at ChangeActionStep, for example, which sets the arguments and then uses the subroutine to do the actual logic. The main difference between a macro and a subroutine is that a macro “copies and pastes” its contents every time it is applied, as where a subroutine is added to your game only once and then only referred to in your code. Both a macro and a subroutine can improve the readability of your assembly code as it makes pretty clear what’s happening – this is true for macros maybe even moreso than for subroutines. The macro may also improve development time, as you don’t have to type out entire bits of code everytime you want to do something similar. Another advantage of a macro is that it’s (potentially) a tad bit faster than a subroutine, because jumping to a subroutine, then returning to where the game jumped from, will take an additional 12 cycles, which aren’t necessary per say if a macro is applied. The biggest downside of the macro when compared to a subroutine is used ROM space. If a macro is applied more than once or twice, it can add to the used bank space pretty quickly.

So, should I use a macro or a subroutine then?

Once again, that depends. Do you have bank space to spare and do you need every clock cycle to reduce lag as much as possible? By all means, apply all the macros you need then. If your game runs smooth enough but your banks are filling up fast though, perhaps it’s time to investigate which macros could be changed into (or make use of) subroutines.

How can I change a macro into a subroutine?

Let’s take a look at the beefy, meaty ChangeTileAtCollision arg0, arg1 macro which, when assembled, takes up about* 182 bytes (which is more than 1% of a ROM bank). We’re gonna change this, step by step, to make it a subroutine and potentially save a lot of bytes.

First, cut everything except the first MACRO ... line and the last ENDM line, and paste it in a new file we call doChangeTileAtCollision.asm. Add doChangeTileAtCollision: at the very start, and     RTS at the very end of this file. Also, change every instance of arg1 into arg1_hold, and every instance of arg0 into arg0_hold.

Now go back to our near-empty macro file, and between the two lines left here, we add this piece of code:

    LDA arg0
    STA arg0_hold
    LDA arg1
    STA arg1_hold
    JSR doChangeTileAtCollision

Finally, you need to add the new subroutine file to our project. It makes the most sense to do this inside of LoadAllSubroutines.asm, but any place in the static bank will do.

The new macro now takes 11 to 13 bytes and the subroutine is about the same size as the macro (it may be either one byte larger or two bytes smaller). If you use the macro only once in your game, this would not save any bytes – in fact it makes your game source even bigger – but every time you use the macro again, it will save you about 170 bytes, which is significant if you use it a lot!

*the assembled size depends on the arguments used for the macro; an immediate or zero-page argument takes up one byte less than an absolute address, for example.

Conclusion

I hope this somewhat clears the smoke around macros, what they do and why it can be either a good or not-so-good idea to apply them. I personally like to have macros referencing subroutines, which makes for a readable codebase and manageable ROM bank space. But as this is different on a per-use basis, it won’t hurt to assess each use individually.

Still not sure about macros and subroutines? Feel free to drop me any question you have!