Page 1 of 1
Best way to pass arguments
Posted: Sun Oct 22, 2017 10:50 pm
by za909
I've recently found that passing arguments can prove to be somewhat troublesome depending on what you have in mind. Obviously, one might go for a route where each CPU register holds one of the arguments assuming there are up to 3 arguments. Some situations might call for a method that only uses 'A' to pass arguments, especially whenever it's important to be able to use instructions with indexed addressing. I see two ways it might be done, and I'd be interested to know which of these works better, or if there's a better way of doing it.
For example, this is an ASM6 macro I have in my game that can be used in any entity's AI to accelerate the entity over time:
Code: Select all
.macro ACCELERATE Xhi,Xlo,Yhi,Ylo
; accelerate the current object
; pass arguments via the stack
lda #>(accelreturn-1) ; push return address in advance
pha
lda #<(accelreturn-1) ; push return address in advance
pha
; recognize and avoid unnecessary lda-s
lda #Yhi
pha
.if Yhi != Ylo
lda #Ylo
.endif
pha
.if Ylo != Xhi
lda #Xhi
.endif
pha
.if Xhi != Xlo
lda #Xlo
.endif
jmp AccelerateObject
accelreturn:
.endm
I have no choice but to push the return address in advance (
AccelerateObject ends with an RTS), otherwise the arguments get buried under it and messing with 'S' seems to be a lot less optimal.
The other method I've used before was to pass arguments using zero page temporaries:
Code: Select all
.macro LOADPALETTE target,palID,amount
; Load palettes and raise the update flag
load=PaletteSet+(palID*4)
lda #<load
sta temp+14
lda #>load
sta temp+15 ; set up indirect vector
lda #amount ; number of extra palettes to load
sta temp+1
lda #target ; target in the buffer
jsr LoadThePalette
.endm
Now both of these are sort of "hybrids", mixing the the 'registers-method' with the other one respectively, but of course this avoids a pointless PHA-PLA pair in the end.
All in all, is there a generally acceptable "best way" of doing this, or is it always situational?
Re: Best way to pass arguments
Posted: Sun Oct 22, 2017 10:58 pm
by rainwarrior
I think what you've just described is the best way. In registers is best, then over the ZP if that's not possible. Go to the stack in the rare case you need re-entrant/recursion and have exhausted the registers.
Macros are great for easy call setups, too.
One other useful place for arguments and return values is the flags (especially carry or V). Sort of like having some extra 1 bit registers.
Re: Best way to pass arguments
Posted: Sun Oct 22, 2017 11:27 pm
by tokumaru
Yeah, this is one of the common problems of 6502 development. While there are a few decent generic solutions out there (usually involving manipulation of the hardware stack, or the use of a software stack), they're often pretty slow. If you want speed, registers, flags and ZP are the way to go. Unfortunately, the ZP way doesn't allow for recursion (thankfully we hardly ever need recursion in NES games), and avoiding ZP collisions as you go deeper and deeper with the subroutine calls can be a problem too.
I've created a pair of ca65 macros (StartScratchpad and EndScratchpad) to help me with declaring function arguments and local variables in the scratchpad area of ZP. The arguments to this macro are an offset into the scratchpad area, and the names of other functions that run alongside the one being defined. These macros keep track of the start and size of the scratchpad variables of every function that uses them, and they'll warn me about any collisions so I can adjust the offsets until there are no more collisions.
I also implemented another pair of macros to manage named memory counters. If I start declaring a block of variables using a memory counter, the counter is updated accordingly when the block ends. This means I can have all functions that are in the same "call tree" share a memory counter, so that there are no scratchpad overlaps between them. This is not optimal though, since functions A, B and C being in the same call tree doesn't necessarily mean that they'll all use scratchpad RAM at the same time, maybe A runs alongside B and B alongside C, but A and C are never used together, so they could in fact share scratchpad space just fine.
But yeah, I just end up mixing all the different ways depending on what's best for each specific part of the program, but I usually go with registers, flags, and ZP. A generic solution is too costly to be used all throughout a program, specially with the smaller subroutines. I'll personally only consider using the stack for arguments if I ever need recursion.
Re: Best way to pass arguments
Posted: Mon Oct 23, 2017 1:35 am
by Garth
See my 6502 stacks treatise at
http://wilsonminesco.com/stacks/, especially chapters 4, 5, and 6. Chapter 6 is titled "Parameter-passing methods."
Unfortunately, the ZP way doesn't allow for recursion
Actually, it does, although you'll use X for the pointer. The method is also shown in the stacks treatise above, being introduced in chapter 4, "Virtual stacks and various ways to implement them."
Re: Best way to pass arguments
Posted: Mon Oct 23, 2017 6:36 am
by Sumez
While using registers is obviously the most effective way to go, I almost always end up swapping out my X/Y register arguments with zero page addresses somewhere along the road, so in retrospect I might have been better off just starting out with those. Using ZP variables also allows me to make use of some better naming, instead of having to remember what X or Y does, and whether I have to be careful about preserving it. The Y register especially can be extremely useful to preserve through an entire volley of subroutines when you're working with object arrays.
What I tend to do, is assigning short names for each number from 0 to F and use them as makeshift registers - by always using the same ones for similar purposes, there's a much lower risk of accidentally interfering with eachother. The 6502 is super fast at working with ZP, and loading a value into the accumulator only takes one more cycle than transferring from X or Y.
I'm currently working on porting a Z80 game to NES, and the Z80 has a crapton of registers, with most of them being able to do 16bit operations. I was afraid I'd end up wasting a ton of CPU cycles trying to maintain exact functionality, but the 6502 works with the zero page faster than the Z80 works with any of its registers, and my version of the game actually runs most of the logic much faster than the original code! It's really a lovely CPU once you learn how it prefers to be handled.
Re: Best way to pass arguments
Posted: Mon Oct 23, 2017 1:38 pm
by gauauu
tokumaru wrote:
But yeah, I just end up mixing all the different ways depending on what's best for each specific part of the program, but I usually go with registers, flags, and ZP. A generic solution is too costly to be used all throughout a program, specially with the smaller subroutines. I'll personally only consider using the stack for arguments if I ever need recursion.
This. It all depends on the specifics of the routine and when it's called. I never use the stack, but will miss and match between zero page and registers depending on the situation. (Particularly, I have a handful of routines that take 2 16-bit parameters. At that point, you can't fit them all in registers anyway)
Re: Best way to pass arguments
Posted: Mon Oct 23, 2017 3:38 pm
by tokumaru
And even when the arguments do fit in the registers and flags you have available, you have to consider the work the subroutine has to do, which might end up needing RAM anyway, so in this case it may be better to pass the arguments directly through RAM, as someone mentioned.
Sometimes you can be a bit creative and avoid using RAM. For example, the other day I was coding a simple subroutine that can be called from many places, so using RAM in it would mean dodging the local variables of many other subroutines, so I decided I'd code it to not use any RAM. It takes an argument that's a value to subtract from a global variable, so the obvious thing to do would be this:
Code: Select all
sta Temp ;3
sec ;2
lda Global ;3
sbc Temp ;3
sta Global ;3 = 14 cycles
But this requires a temporary variable in RAM, so I figured I could negate the value and add it to the global instead:
Code: Select all
eor #$ff ;2
sec ;2
adc Global ;3
sta Global ;3 = 10 cycles
In addition to not using any scratchpad RAM, this is actually faster than the obvious solution.
Re: Best way to pass arguments
Posted: Mon Oct 23, 2017 4:33 pm
by keldon
I've a few virtual registers in zero page with a few contracts when functions are called and I need more than just A/X/Y.
Argument registers (a0-a7) are preserved by the called function. Use to pass parameters to methods, or to store variables if you've run out of work registers
Temporary registers (t0-t3) are not preserved by the called function. Use these to carry out calculations without the need to preserve its value on the stack.
Return registers (r0-r3) are not preserved by the called function
Work registers (w0-w7) are preseved by the called function. Use these to store variables needed throughout your function.
Argument and work registers are preserved on the stack (allowing recursion) while temp registers can be used for throwaway values.