Yet Another Function Manager

JMP $FCE2
MarcWalters
Member
Member
Posts: 45
Joined: Wed Jun 11, 2014 2:33 pm
Location: Lake Macquarie, NSW, Australia

Yet Another Function Manager

Post by MarcWalters » Mon Oct 20, 2014 11:33 am

This is a lightweight but potentially useful function management system that makes it easier to write complex and large programs. It reduces the constraints imposed on programs by the limited CPU registers, and significantly eases the access to speed/size trade-offs. It also provides a simple method for adding code support for the REU, SCPU and expanded memory.

During development the key drive was to ensure it was balanced. The call overhead is about 350 cycles and so each feature added had to be justified.

The program was developed using CBM Prg Studio.

Native Features
  • Direct call to function address
    Up to 127 bytes of parameters
    Parameters accessed either inline (following the JSR) or a vector
    Default Parameters ("fill in the blanks"!)
    Just-In-Time code vector cache
    CPU registers are automatically preserved
    Internal function vectors are externally accessible
Not Native Features
These were in a previous version, but were relegated to a supporting role
  • List processing - but it can be hacked by jumping directly into the function manager code
    Stack frame storage - can be done ad-hoc within functions
    Data-types and expression evaluation engine - cool, but limited use
About The Code Vector Cache
Functions can include alternative code for expansion hardware.
A global variable, CpuMem$, indicates what is available: 0 is a standard C64, 8 is REU, 4 is SCPU, 2 is SCPU+REU and 1 is SCPU+SuperRAM. The list is ordered by priority, with SCPU+SR highest. Each managed function has a header of information, and the first byte of that header describes what alternate code is available, if any. On the very first use of that function the Function Manager will match the descriptor value with that of CpuMem$ and sets the function code address to be used on all subsequent uses.

Define A Managed Function
It's simple. Add the header and decide where any parameters will be stored (zero page, within the function's code, some other address, it doesn't matter) and what the default parameters will be.

Example 1: The two bytes of parameters that are passed in from the caller are redirected to $00FB ready and waiting for the program to add them together.

Code: Select all

; -------------------------------
; Sample Function (19 bytes). Without codevec management and encapsulated parameter area
DoSum    jsr FnMan
         byte 0                     ; Function Descriptor
         byte 2                     ; ParamLen
         word $FB                   ; Param Vector
         word $FB                   ; Default Param Vector
         word @Code                 ; Code Vector Default

@Code    clc
         lda $FB
         adc $FC
         sta zResult$
         rts


Example 2: This one is more verbose but does the same thing. Note that the simple declarations of addresses in the header allow a significant amount of memory to be pushed around the system without any effort on the part of the programmer. A few simple tricks are available too, such as pointing the Default Parameter address to the Parameter address to reuse values, or even pointing it at zA$ (CPU Accumulator storage) and having direct access to .A,.X.Y and .S at the time of the call . The code vector can be preset (High-byte anything other than 0) to avoid the initial code base determination, but it's not really necessary. Only if alternate code is available is there a need to add addresses to the code vector list.

Code: Select all

; -------------------------------
; Sample Function (32 bytes)
DoSum2    jsr FnMan
         byte %00001000             ; Function Descriptor
         byte 2                     ; ParamLen
         word @Params               ; Param Vector
         word @DefParams            ; Default Param Vector
         word 0                     ; Cached Code Vector
         word @Code                 ; Code Vectors
         word @ReuCode              

@Code    clc
         lda @Params
         adc @Params + 1
         sta zResult$
         rts

@ReuCode jmp @Code
          
@Params  byte 0,0
@DefParams byte 1,2
Call A Function
Call a managed function by using a JSR <function address>, followed by a control byte that has Bit 8 clear if the parameters follow, or set if the parameters are remote. The lower 7 bits of the control byte indicate how many bytes to pass into the function. And the call does not need to pass in the exact amount expected by the function because functions have default parameters to fill in the missing ones (this feature can be very useful).

Code: Select all

         jsr DoSum
         byte 2, 20, 22               ; Inline parameters comprising two bytes to be added together
         lda zResult$
         sta $0400
         rts
The Function Manager Code
Below is the manager code. It's still a work-in-progress, but fairly complete.

Code: Select all

; ------------------------------
; Function Manager
; ------------------------------

; ----------
; GLOBALS
; The CPU-Memory layout
; 0 = C64
; Bit 1 = SCPU + SuperRAM
; Bit 2 = SCPU + REU
; Bit 3 = SCPU
; Bit 4 = REU
CpuMem$  byte 0

zResFnMan$ = $80

; ------------------------------
; Function Manager
; ------------------------------

; ----------
; GLOBALS
zResult$ = zResult

; Reg Storage
zA = zResFnMan$            ; 1 byte
zX = zA + 1                ; 1 byte
zY = zX + 1                ; 1 byte
zp = zX + 1

; Results
zResVec = zP + 1           ; 2 bytes
zResult = zResVec + 2      ; 5 bytes

; Vectors
zCodeVec    = zResult + 5           ; 2 bytes
zCallParVec = zCodeVec + 2          ; 2 bytes
zCallVec    = zCallParVec + 2       ; 2 bytes
zFnVec      = zCallVec + 2          ; 2 bytes
zDefParVec  = zFnVec + 2            ; 2 bytes
zParVec     = zDefParVec + 2        ; 2 bytes
zReturnVec  = zParVec + 2           ; 2 bytes

; Storage
zParLen   = zReturnVec + 2          ; 1 byte
zParCnt   = zParLen + 1             ; 1 byte
zFnDesc   = zParCnt + 1             ; 1 byte
zCallCtrl = zFnDesc + 1             ; 1 byte
zTB1$     = zCallCtrl + 1           ; 1 byte

; Initialise Function Manager
InitFnMan jsr InitResV
         rts

; Initialise the Result Vector
InitResV lda zResult
         sta zResVec
         lda zResult + 1
         sta zResVec + 1
         rts         

FnMan    ; Store registers
         sta zA
         stx zX
         sty zY
         php
         pla
         sta zP

         ; Get Function Rtn Addr
         pla
         sta zFnVec
         pla
         sta zFnVec + 1 

         ; Get Function Descriptor
         ldy #1
         lda (zFnVec),Y    
         sta zFnDesc
       
         ; Get Function Param Length
         iny
         lda (zFnVec),Y    
         sta zParLen

         ; Get Function Param Vector
         iny
         lda (zFnVec),Y    
         sta zParVec
         iny
         lda (zFnVec),Y    
         sta zParVec + 1

         ; Get Function Default Param Vector
         iny
         lda (zFnVec),Y    
         sta zDefParVec
         iny
         lda (zFnVec),Y    
         sta zDefParVec + 1
         
         ; Get Function Code Vector
         iny
         lda (zFnVec),Y    
         sta zCodeVec
         iny
         lda (zFnVec),Y    
         sta zCodeVec + 1  ; if 0 then not initialised
         bne @SetVec

         ; CodeVec is clear so a JIT determination is required
         iny               ; Bump to point to Default Code Vector
         
         ; Get CPU-Mem support
         lda zFnDesc       ; Get Function Descriptor
         beq @SetCodeVec   ; No support for anything other than C64 Standard
         sta zTB1$
         lda CpuMem$
         beq @SetCodeVec   ; C64 Standard so fetch first code vector

         ; Select a code vector from a prioritised list
         ldx #4            ; 4 iterations
@L1      lsr zTB1$
         bcc @C1
         iny               ; bump to next
         iny
         lsr
         bcs @SetCodeVec   ; Supported!
         bcc @C2
@C1      lsr               ; Unsupported so lsr to the next bit position
@C2      dex
         bne @L1
         
         ; No matching alternate code, so Reset to default code pointer
         ldy #9

         ; Set new Function Code Vector
@SetCodeVec lda (zFnVec),Y    
         sta zCodeVec      ; Store the Vec in case future FNMan versions require it, eg list processing
         pha
         iny
         lda (zFnVec),Y
         sta zCodeVec + 1
         ldy #8
         sta (zFnVec),Y    ; The location of the Function's @CodeVec
         dey
         pla
         sta (zFnVec),Y

         ; Set new Function vector
@SetVec  lda zCodeVec    
         sta JsrVec + 1
         lda zCodeVec + 1
         sta JsrVec + 2

         ; Get Caller Vector
@GetCrVc pla
         sta zCallVec
         pla
         sta zCallVec + 1 

         ; Get Caller Control Mode (Inline or Vector)
         ldy #1 
         lda (zCallVec),Y
         tax
         asl
         lda #0
         rol
         sta zCallCtrl

         ; Get Caller Param Count
         txa
         and #127
         sta zParCnt

         ; Set up parameter vector and return address
FixCall  iny               ; Point to first Data
         lda zCallCtrl
         bne @Vector
         
         ; Inline Parameters
@Inline  tya
         clc
         adc zCallVec
         sta zCallParVec
         lda #0
         adc zCallVec + 1
         sta zCallParVec + 1
         
         lda #2            ; Set Call Return Vec to ( Caller + 2 + ParameterCount )
         clc
         adc zParCnt
         bne @C1           ; Always

         ; Address of Parameters
@Vector  lda (zCallVec),Y
         sta zCallParVec
         iny
         lda (zCallVec),Y
         sta zCallParVec + 1

         tya               ; Set Call Return Vec to ( Caller + 4). .Y is correct address offset
@C1      clc
         adc zCallVec
         sta zReturnVec
         lda #0
         adc zCallVec + 1
         sta zReturnVec + 1

         ; Write Caller params
WrtCall  ldy zParCnt
         dey
         sty zTB1$
         bmi @C1 
@L1      lda (zCallParVec),Y
         sta (zParVec),Y
         dey
         bpl @L1

         ; Fill remainder of Params using the Function's Default Params
@C1      ldy zParLen
         cpy zParCnt
         beq SetRet                 ; No defaults to be written
         dey
         bmi SetRet
@L2      lda (zDefParVec),Y         ; Write defaults
         sta (zParVec),Y
         dey
         cpy zTB1$
         bne @L2

         ; Setup Return Vector
SetRet   lda zReturnVec + 1
         pha
         lda zReturnVec
         pha

         ; Store Registers
         lda zA
         pha
         ldy zX
         pha
         lda zY
         pha
         lda zP
         pha
         
JsrVec   jsr $0000                  ; Call function

         ; Restore registers
         plp
         pla
         tay
         pla
         tax
         pla
         rts

; -------------------------------
; Sample Caller
;
; jsr Ex_Fn
;   byte 2            ; Call Control + Param Count
;   byte 5,5          ; Params
;

; -------------------------------
; Sample Function
;
; Fn jsr FnMan
;   byte %00001000             ; Function Descriptor
;   byte 2                     ; ParamLen
;   word @Params               ; Param Vector
;   word @DefParams            ; Default Param Vector
;   word @CodeVec              ; Cached code vector
;   word @Code, @ReuCode       ; Code Vectors



satpro

Re: Yet Another Function Manager

Post by satpro » Tue Oct 21, 2014 1:08 pm

Nice, clean programming style, Marc. How does the call overhead time "feel" during a call? Do you notice any significant lag time making calls through this function manager?

MarcWalters
Member
Member
Posts: 45
Joined: Wed Jun 11, 2014 2:33 pm
Location: Lake Macquarie, NSW, Australia

Re: Yet Another Function Manager

Post by MarcWalters » Tue Oct 21, 2014 3:01 pm

The cycle overhead is stable and predictable. It really ends up a question of whether the reduced program development time, size and complexity is worth the cycle overhead. That overhead is further diminished if the bonus features are used to shunt memory around and pipeline some tasks.

One issue is that without re-entrancy, functions compete (and lose) against tabular or stack programming techniques. And when hand-assembled code begins to look and feel like it's been run through a compiler, it's time to put the assembler aside and program in C instead.

An earlier version did ensure that managed functions were re-entrant - it allocated a section of zero-page as local variable storage for functions and storing this to the CPU stack if another function was called which modified the local variable area. But the cycle overhead was an extra 300 cycles and could not be justified given how little it was needed in practice. The use of relocatable parameter and result addresses alleviates this problem to some extent, and I've devised another solution (not mentioned in the article) that can be applied ad-hoc to functions that require protected variable space.

As a long-time assembly programmer, I dislike "wasting" 350 cycles on a subroutine in a flat memory architecture. However, I've written a number of large assembly programs in the past which, thinking back, would most likely have benefited from such a system.

Post Reply Previous topicNext topic

Who is online

Users browsing this forum: No registered users and 1 guest