It is currently Mon Nov 20, 2017 5:57 pm

All times are UTC - 7 hours





Post new topic Reply to topic  [ 8 posts ] 
Author Message
PostPosted: Sun Oct 22, 2017 10:50 pm 
Offline
User avatar

Joined: Fri Jan 24, 2014 9:05 am
Posts: 138
Location: Hungary
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:
.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:
.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?


Top
 Profile  
 
PostPosted: Sun Oct 22, 2017 10:58 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 5824
Location: Canada
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.


Top
 Profile  
 
PostPosted: Sun Oct 22, 2017 11:27 pm 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10116
Location: Rio de Janeiro - Brazil
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.


Top
 Profile  
 
PostPosted: Mon Oct 23, 2017 1:35 am 
Offline

Joined: Wed Nov 30, 2016 4:45 pm
Posts: 93
Location: Southern California
See my 6502 stacks treatise at http://wilsonminesco.com/stacks/, especially chapters 4, 5, and 6. Chapter 6 is titled "Parameter-passing methods."

Quote:
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


Top
 Profile  
 
PostPosted: Mon Oct 23, 2017 6:36 am 
Offline
User avatar

Joined: Thu Sep 15, 2016 6:29 am
Posts: 382
Location: Denmark (PAL)
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.


Top
 Profile  
 
PostPosted: Mon Oct 23, 2017 1:38 pm 
Offline
User avatar

Joined: Sat Jan 09, 2016 9:21 pm
Posts: 245
Location: Central Illinois, USA
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)

_________________
My games: http://www.bitethechili.com


Top
 Profile  
 
PostPosted: Mon Oct 23, 2017 3:38 pm 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10116
Location: Rio de Janeiro - Brazil
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:
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:
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.


Top
 Profile  
 
PostPosted: Mon Oct 23, 2017 4:33 pm 
Offline

Joined: Wed Jun 07, 2017 7:55 am
Posts: 8
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.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 8 posts ] 

All times are UTC - 7 hours


Who is online

Users browsing this forum: Bing [Bot] and 15 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group