Best way to pass arguments

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
User avatar
za909
Posts: 226
Joined: Fri Jan 24, 2014 9:05 am
Location: Hungary

Best way to pass arguments

Post by za909 » Sun Oct 22, 2017 10:50 pm

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?

User avatar
rainwarrior
Posts: 8006
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Best way to pass arguments

Post by rainwarrior » Sun Oct 22, 2017 10:58 pm

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.

User avatar
tokumaru
Posts: 12002
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Best way to pass arguments

Post by tokumaru » Sun Oct 22, 2017 11:27 pm

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.

Garth
Posts: 207
Joined: Wed Nov 30, 2016 4:45 pm
Location: Southern California
Contact:

Re: Best way to pass arguments

Post by Garth » Mon Oct 23, 2017 1:35 am

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."
http://WilsonMinesCo.com/ lots of 6502 resources

User avatar
Sumez
Posts: 920
Joined: Thu Sep 15, 2016 6:29 am
Location: Denmark (PAL)

Re: Best way to pass arguments

Post by Sumez » Mon Oct 23, 2017 6:36 am

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.

User avatar
gauauu
Posts: 724
Joined: Sat Jan 09, 2016 9:21 pm
Location: Central Illinois, USA
Contact:

Re: Best way to pass arguments

Post by gauauu » Mon Oct 23, 2017 1:38 pm

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)

User avatar
tokumaru
Posts: 12002
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Best way to pass arguments

Post by tokumaru » Mon Oct 23, 2017 3:38 pm

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.

keldon
Posts: 8
Joined: Wed Jun 07, 2017 7:55 am

Re: Best way to pass arguments

Post by keldon » Mon Oct 23, 2017 4:33 pm

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.

Post Reply