This is an important decision in any SNESdev project and can be difficult to change in large projects without modifying a lot of code and risking crashes. I have not seen many discussions or documents about SNES assembly code calling conventions.
I want to write a document about 65c816 calling conventions after the 2025 SNESdev game jam and I am curious as to what calling conventions you are using.
By calling convention I'm talking about:
- Register sizes (Accumulator and Index)
- DB Data Bank register values
- DP Direct Page register values
- What do you do for subroutines and macros with a different register-size, DB or DP requirements?
- What register do you typically use to pass entity/actor/object/sprite (whatever you call it) values on? X, Y or DP?
- What do you do when you run out of registers for function arguments?
- Are any registers or arguments callee saved?
I currently have 4 active SNES projects, all with slightly different calling conventions.
untech-engine:
Inconsistent call convention. Expected register sizes/values are documented in the subroutine/macro code. It's been a while since I delved into this engine, I honestly don't know if this is accurate:
Mainloop:
- Accumulator varies. Usually 16-bit, occasionally 8-bit
- Typically 16-bit Index
- DP = 0 or entity pointer
- DB = 0x7e to access a lot of Work-RAM. Long addressing or direct-page is used to access the MMIO/PPU registers.
- 8 bit Accumulator, 16 bit Index
- DB = $7e, DP = 0
- DB = $80, DP = 0
- DB = $7e, DP = $2100 (subroutine name suffixed with "_dp2100")
- 16 bit Accumulator, 8 bit Index
- DB = $80, DP = 0
- DB = $80, DP = $4300 (subroutine name suffixed with "_dp4200")
- DB = $7e, DP = $2100 (subroutine name suffixed with "_dp2100")
Registers are not callee saved. Sometimes subroutines will keep X or Y unchanged for optimisation purposes. Those are tagged with `KEEP` in the function comments.
snes test roms:
Typically: 8 bit A, 16 bit Index, DB = 0x80, DP = 0
A few tests use: 8 bit A, 16 bit Index, DB = 0x7e, DP = 0
Unnamed SNES Game: documented
Contexts:
- Main Loop: 8 bit A, 8 bit Index, DB = 0x7e, DP = 0
- Entity Loop: 8 bit A, 8 bit Index, DB = 0x7e, DP = 0, Y = entityId
- MetaSprite rendering: 8 bit A, 16 bit Index, DB = 0x7e, DP = 0
- Setup (force-blank): 8 bit A, 16 bit Index, DB = 0x80, DP = 0
- VBlank: 8 bit A, 16 bit Index, DB = 0x80, DP = 0
Y is callee saved if it is the entityId.
Extra arguments are stored in zeropage temporary variables.
Some functions do not honor my calling convention and the function name must be private (with a double-undersocre prefix) and/or suffixed appropriately (ie, "__clobbers_y", "__keep_y", "__mem16_idx8", etc).
Terrific Audio Driver ca65/64tass API: Also documented
- 65816 native mode
- Decimal mode off
- 8 bit Memory
- 16 bit Index, unless the subroutine is tagged "I unknown" (the queue and query functions)
- D Direct Page = 0
- DB Data Bank Register accesses low-RAM ($7e or $00..=$3f or $80..=$bf). With one exception - the LoadAudioData callback.