ca65: Passing parameter to function with a software stack

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
ca65: Passing parameter to function with a software stack
by on (#50551)
Warning: wall of text.

In some previous threads (here and here) me, Miau and Disch have been talking about macros, how to use the stack in a better way, parameters etc.

Disch came out with the concept of a software stack in zero page instead of using the real one. After thinking a little bit about it, I decided to make one and tried to make a few macro to help the process of using it.

Here's the concept. First, you need to define a variable in zero page for the stack pointer and some constant for the location of the stack. You have to init it like the real stack with the value $FF

Code:
.segment "ZEROPAGE"
zpStackPointer:      .res 1 ; Software stack pointer

; ... Some code later

; Define for stack location
SOFTWARE_STACK   = $00      ; Software stack pointer is in zero page

.segment "CODE"
   ldx #$FF
   stx zpStackPointer       ; Set software stack pointer


The init must be done after your init your memory to zero. Then, you need to create a method that defines a few parameters. The way you define a method is this way:

Code:
; The procedure with parameter
.proc subShowAnimFrame
; Parameters on software stack:
.struct Param
   PosX        .byte   ; X location
   PosY        .byte   ; Y Location
   SpriteDir   .byte   ; Sprite direction
.endstruct

; ... some code here
.endproc


The structure defined inside the .proc is important: it defines the size of your parameters and where they are located. It will become clearer later.

Now you need a few macro to make your life easier (and make your code cleaner to read). One is for allocating memory, one is for loading parameters and the other one is for releasing memory.

Here's the macros:
Code:
;->BEGIN----------------------------------------------------------------------
; Allocate memory on the stack for parameters
;
; Parameter: 
;   paramStruct : The structure that represent the parameters that needs memory
;                 to be allocated.
;
; Notes: The stack pointer value is put by default in the X index at the end. This
;        is because we expect to store parameters after alocating memory.
;
.macro alocateMemOnStack paramStruct
   lda zpStackPointer         ; Load stack pointer
   sec               
   sbc #.sizeof(paramStruct)      ; Adjust stack pointer
   sta zpStackPointer         ; save it
   tax               ; Store stack pointer value in index
.endmacro
;-<END------------------------------------------------------------------------

;->BEGIN----------------------------------------------------------------------
; Release the memory stored for the parameters on the stack
;
; Parameter: 
;   paramStruct : The structure that represent the parameters that needs memory
;                 to be released.
;
.macro releaseMemFromStack paramStruct
   lda zpStackPointer         ; Load stack pointer
   clc               
   adc #.sizeof(paramStruct)      ; Adjust stack pointer
   sta zpStackPointer         ; save it
.endmacro
;-<END------------------------------------------------------------------------

;->BEGIN----------------------------------------------------------------------
; Store parameter on the stack
;
; Parameter: 
;   paramLocation : Where in memory relative to the stack pointer that is should
;                   be stored.
;   value         : The value we want to store
;
; Note: We should make it more inteligent and knows when it's a 16/8 value to store
;
.macro spos paramLocation, value
   lda value
   sta SOFTWARE_STACK + paramLocation, x
.endmacro
;-<END------------------------------------------------------------------------


Now that we have some macro to do the job, it's time to see how to use them. Here's the code example to call a function with parameters:

Code:
   ; Alocate ram the size of all parameters
   alocateMemOnStack subShowAnimFrame::Param


   ; Store parameters on stack
   spos subShowAnimFrame::Param::PosX, #$00
   spos subShowAnimFrame::Param::SpriteDir, zpDebugMMDir
   spos subShowAnimFrame::Param::PosY, #$64
   
   jsr subShowAnimFrame
   
   ; Release it
   releaseMemFromStack subShowAnimFrame::Param


Now without explanation it will be hard to understand. The first line allocate ram on the stack. How it work is that is the macro receive as a parameter the function parameter's structure (subShowAnimFrame::Param). The macro will check the size of the structure and will allocate the ram right away. Then it will set the X register with the current stack location.

The spos puts a parameter on the stack. The first parameter is the location on the stack (subShowAnimFrame::Param::PosX). The second one is the value you want to put. In it's current stage, you can only put 8 bits value. I want to improve it later.

One thing you will find interesting is the order of parameters is not important. Since the structure knows the location in memory of a parameter and you allocated the RAM you need before using it, you don't have to worry about parameters order. this will be the same thing when retrieving them too.

Now that we called the method, we need to know how to access those parameters inside it. Here's the code example:

Code:
   ldx zpStackPointer
   lda SOFTWARE_STACK + Param::SpriteDir, x ; Check sprite orientation.


Since you use a software stack in zero page, you must first load the X register with the stack pointer. Then you can access the location of the variable with stack location + location in RAM (SOFTWARE_STACK + Param::SpriteDir). You don't have to worry if it's parameter 1 or 2, the structure will take care of it.

The pro of this approach is to have dynamic allocation of parameters. Once you understand how to use the macro properly, it's quite simple. It removes the possibility of using by accident some temp variable defined elsewhere too. If you want local variables, you could just add a few extra parameters inside the struct. The value of those "parameters" will be initialized inside the method. This way, the memory for the parameters and local variables would be allocated before calling the function.

The cons are that there will be a small price in performance. You need to load the stack pointer in X every time you want to access a value. Compared to a direct zero page variable, you have do do a few processing before being able to access it. So I will not recommend this approach for critical method used in the vblank for example. But in some other cases, it could make the programming easier for methods that have many parameters. You don't have to worry how many temp variable you have and if someone is using them.

I have been testing it yesterday and for now it's working. I need to test it more to see if there is any issues I didn't see yet.

That's was a long message. Any comment on the subject will be appreciated.
Re: ca65: Passing parameter to function with a software stac
by on (#100456)
I wanted to do something like this with as little additional coding as required (not counting the macros themselves) and have come up with something I like. This does not use a stack, because I did not want the additional overhead in the code slowing things down and reducing flexability - I just wanted to know that my code was not doing anything it shouldn't.

So I decided to just have a kind of range checking system that warns you if you might be doing something bad and provide you with the chance to take a close look and ignore the warnings if everything is okay. Once you are completed your game/app you can turn off the checks and eliminate any slowdown from range checking.


Code:
; In a main module, define:

FUNCTION_RANGE_CHECKING_ON = 1 ;Turn off if functions are okay
FUNCTION_LOCAL_SIZE = $0A ;# bytes for local zero page shared
FUNCTION_PARAMS_SIZE = $06 ;bytes for paramaters

; these two must be the first thing reserved in zeropage
.segment "ZEROPAGE"
function_locals: .res FUNCTION_LOCAL_SIZE 
param:   .res FUNCTION_PARAMS_SIZE     


Anywhere else or in an included file define your callable functions as:
Code:
.func get_nametableaddress


.locals
 nametableaddress .byte
.endlocals

; IN:  reg x has nametable x, reg y has nametable y
; OUT : reg x has low address, reg y has high address
; LOCAL: Uses 1 byte

 tya     
 asl   
 
 asl   
 asl
 asl
 asl
 
 stx local::nametableaddress
 ora local::nametableaddress
 tax
 tya
 lsr
 lsr
 lsr
 ora #$20
 tay
 rts
 
.endfunc

If you wish to define a function as above inside a scope and export/import it:
Code:
;export:
.exportfunc get_nametableaddress

;import:
.importfunc get_nametableaddress

In code, you can use call:
Code:
  call get_nametableaddress, #10,#10

The macro will generate a small amount of code that checks the local variable usage and parameter usage. In this example no paramater memory is used because the macros justs loads reg x and y. There is no stack! Only a small amount of code will be output that checks for overuse or nested use of paramaters and local memory use and outputs a warning, so you can look closer and see if there is a problem or not. If you do pass paramaters in paramater memory space use clear_param num, where num is the number of paramaters acknolwedged as read/no longer needed:
Code:
.func some_function

; IN reg X,Y (addressIN) and a, param+0 (addressout)

.locals
 addressIN .word
 addressOUT .word
.endlocals



 stx local::addressIN
 sty local::addressIN + 1
 
 sta local::addressOUT
 lda param+0
 clear_param 1
 sta local::addressOUT + 1
 ;.....etc

If you look at the very first line of code:
Code:
FUNCTION_RANGE_CHECKING_ON = 1 ;Turn off if functions are okay

If you comment that line, no extra code will be generated and your assembled code will be the same as if you did not use any range checking code of these macros.

I made a blog post here where I posted the macro code: http://mynesdev.blogspot.ca/2012/09/tra ... usage.html

Disclaimer: I just finished this code, I do not know if it works 100%
Re: ca65: Passing parameter to function with a software stac
by on (#100476)
Personally, I wouldn't use .feature leading_dot_in_identifiers, I would leave the dotted identifiers exclusively to the assembler.

Code:
; these two must be the first thing reserved in zeropage
.segment "ZEROPAGE"
function_locals: .res FUNCTION_LOCAL_SIZE 
param:   .res FUNCTION_PARAMS_SIZE

This is not a good idea, you should put it in a separate segment. And even then, you probably should add an .assert that verifies they are where they're supposed to be: .assert function_locals = 0, error, "oh no"

About the parameter checking, I have been thinking for some time about possibly adding a feature to an emulator that could allow doing such checks without performance overhead. One possible feature would be runtime asserts, but to support something like this they'd have to be pretty complex.

And this brings me to another possible feature: Lua/Python code embedding in the ROM. What I mean is, you could embed pieces of script code within the ROM to do the debug checks without any overhead. Hell, it could even be used to easily implement placeholders for some of the features of the game until they get replaced with 6502 code.

Note that the code wouldn't actually have to be physically in the ROM, it could be output to an entirely separate file which is read by the emulator based on debug symbols, so it wouldn't even have to add practically any ROM overhead. Needless to say, this feature would only be used at development time.
Re: ca65: Passing parameter to function with a software stac
by on (#100477)
I'm not sure what is the advantage of using a software stack against a hardware one. Maybe a very slight gain of performance ?

Because you'll need the X register anyways, but acessing the hardware stack will be done with :
lda $100 + wathever,X

which is only one byte more and one cycle slower than doing it in zero page.

However the pushing and pulling of parameters can be done with pha / pla, which can't be done with the software stack approach, thus a gain of bytes and speed against a software stack.
Re: ca65: Passing parameter to function with a software stac
by on (#100480)
I'll try the assert. Personally I like the way the . reads, it's more about aesthetics though I think of it a bit like extending the assembler. Having a segment I guess is okay, but then I have to change the .cfg if I need to change the size, it seems like more effort.

I was using the hardware stack before, but I decided I didn't like it due to a few factors. If your paramaters are in zeropage you can perform any instruction with them without having to load or pull them from the stack for one. Plus I don't use a stack at all, just shared space with a count of memory used.
Re: ca65: Passing parameter to function with a software stac
by on (#100481)
Movax12 wrote:
I'll try the assert. Personally I like the way the . reads, it's more about aesthetics though I think of it a bit like extending the assembler. Having a segment I guess is okay, but then I have to change the .cfg if I need to change the size, it seems like more effort.

Yeah I understand it's for aesthetics, and certainly thought about doing it myself too, but ended up using syntax hilighting instead for a similar effect.

You don't have to change the .cfg if the segment size changes. You can have one MEMORY definition (which specifies the size), and put multiple segments in it. The order of the segments in the linker file defines the ordering in the memory area.