ca65 goodies

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 569
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Re: ca65 goodies

Post by Jarhmander »

In the spirit of the last post, here's some "goodies" of mine. They are pseudo-ops that just reduce the number of lines of code, however one should always be cautious because they can possibly emit alot of instructions, so they can create a false impression of program compactness. Also, one may call it heresy that those pseudo-ops are more than 3 characters, and that it may look like another cpu instruction set: no, my goal isn't to implement another processor in a 6502, it is just to save a bit of typing while having some fun with ca65's macro system.

push/pop:

Code: Select all

.macro push a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15
    .ifnblank a0
        .if .xmatch({a0},p) .or .xmatch({a0},P)
            php
        .else
            .if .match({a0},x)
                txa
            .elseif .match({a0},y)
                tya
            .elseif .match(.left(1,{a0}),=)
                lda #>(.right(.tcount({a0})-1,{a0}))
                pha
                lda #<(.right(.tcount({a0})-1,{a0}))
            .elseif !(.match({a0},a))
                lda a0
            .endif
            pha
        .endif
        push a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15
    .endif
.endmacro

.macro pop a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15
    .ifnblank a0
        pop a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15
        .if .xmatch({a0},p) .or .xmatch({a0},P)
            plp
        .else
            pla
            .if .match({a0},x)
                tax
            .elseif .match({a0},y)
                tay
            .elseif !(.match({a0},a))
                sta a0
            .endif
        .endif
    .endif
.endmacro
The idea here is that you specify what you want to push/pull to/from the stack; A, X, Y and P can be specified along with memory locations. push pushes all the args in the order specified and pop pops them in reverse order, so the following:

Code: Select all

    push A,X,Y,somevar
    pop  A,X,Y,somevar
...will save and restore correctly the specified registers and the memory location. Of course, A should be first (or second, if P is first) in the list if one want to save A, or else A won't be correctly saved/restored without a warning.

Also, push accepts regular immediate (8-bit) arguments but also 'absolute' arguments with =: one can push absolute addresses, and those addresses are pushed in the same order that jsr and brk does. So the following is a slow and convoluted way to jump at somelabel:

Code: Select all

    ; using rts
    push =somelabel-1
    rts

    ; using rti
    push =somelabel, P
    rti
mov/movw:

Code: Select all

.macro mov dest, src
    lda src
    sta dest
.endmacro

.macro movw dest, src
    .local sepd, seps
    sepd .set 0
    seps .set 0

    .if .match({.right(2,{dest})},{,x}) .or .match({.right(2,{dest})},{,y})
        sepd .set 2
    .endif
    .if .match({.right(2,{src})},{,x}) .or .match({.right(2,{src})},{,y})
        seps .set 2
    .endif
    
    .if .match(.left(1, {src}),#)
        mov {.left(.tcount({dest})-sepd,dest)+0 .right(sepd,dest)}, #<(.right(.tcount({src})-1,{src}))
        mov {.left(.tcount({dest})-sepd,dest)+1 .right(sepd,dest)}, #>(.right(.tcount({src})-1,{src}))
    .else
        mov {.left(.tcount({dest})-sepd,dest)+0 .right(sepd,dest)}, {.left(.tcount({src})-seps,src)+0 .right(seps,src)}
        mov {.left(.tcount({dest})-sepd,dest)+1 .right(sepd,dest)}, {.left(.tcount({src})-seps,src)+1 .right(seps,src)}
    .endif
.endmacro
mov is trivial so won't explain, but is included here nevertheless because movw uses it. The latter moves a word (which can be immediate) into some address; it assumes words are stored into consecutive byte addresses. dest and src can even use absolute or zeropage indexed addressing mode if you use curly braces:

Code: Select all

    movw {$4000,X}, {APU_virtualregs,X}
Limitations: it can only use A for transfers, it does not fit well with the use of the struct-of-array "idiom", and if you try to use indexed indirect addressing, it will silently break because it will be converted into absolute indexed (oh nevermind, it gives errors fortunately, but one should be cautious when using parens, it should not begin with an open parens, that how ca65 tells if it's absolute or indirect addressing).
((λ (x) (x x)) (λ (x) (x x)))
User avatar
Movax12
Posts: 541
Joined: Sun Jan 02, 2011 11:50 am

Re: ca65 goodies

Post by Movax12 »

Was working on the aforementioned stuff and came up with an idea to help with .define style macros.
These can be very helpful in some cases: they can be used anywhere, as compared to regular macros which are required to be at the beginning of a line. As well, they are the only "variable" in ca65 that can be assigned a string. They are constants, but they can be undefined and redefined, so they can be treated as variables. The problem is that they can get really tricky, for example:

Code: Select all

.define FOO BAR

.ifdef FOO
    .out "YES"
.else
    .out "NO"
.endif
This will output NO, since the .define macros always resolve to their value, which is BAR, and BAR is not defined. They only way they don't resolve to their value is when using .undefine

To make things simpler I use this:

Code: Select all

.define inlineIsDefined(i)  ( .defined(::.ident(.sprintf("_ISDEFINED_%s", i) )) .and ::.ident(.sprintf("_ISDEFINED_%s", i) ) )

.macro define i, v
    ; if defined and if defined as 1
    .if inlineIsDefined i
        .undefine .ident(i)
    .endif
    
    .define .ident(i) v
    ::.ident(.sprintf("_ISDEFINED_%s", i)) .set 1
    
.endmacro

.macro undefine i
    .if inlineIsDefined i
        .undefine .ident(i)
    .endif
    
    ::.ident(.sprintf("_ISDEFINED_%s", i)) .set 0
.endmacro
With these macros you can:

Code: Select all

define "FOO", BAR

; output is YES
.if inlineIsDefined "FOO"
    .out "YES"
.else
    .out "NO"
.endif

; redefine:
define "FOO", "This is a string"
This also has the advantage that you can use define or undefine at anytime, whereaas with the original .define and .undefine, ca65 would output an error if the identifier was not defined/undefined properly first.
User avatar
Movax12
Posts: 541
Joined: Sun Jan 02, 2011 11:50 am

Re: ca65 goodies

Post by Movax12 »

How to evaluate unknown amount of parameters:

You could probably do this with just a repeat as well, but I find recursion the easiest way. The simplest way is to just list a bunch of parameters and hope you have enough and recurse on them, but more flexible is something like:

Code: Select all

.macro myMacro param

    ; quickly find next comma:
    .local COMMA_POSITION
    COMMA_POSITION .set 0
    
    .repeat .tcount({param}), I
        .if .not COMMA_POSITION
            .if .xmatch( {.mid(I, 1, {param}) } ,  {,}   )
                COMMA_POSITION .set I
            .endif
        .endif
    .endrepeat

    .if COMMA_POSITION
        .define PARAM  .mid(0, COMMA_POSITION, {param})
    .else
        .define PARAM param
    .endif
    
    ; -----    
    ; Do some stuff with PARAM
    ; -----
   
    .undefine PARAM
    ; do again with next param if there was a comma:
    .if COMMA_POSITION
        myMacro { .mid ( COMMA_POSITION + 1 , .tcount({param}) - COMMA_POSITION - 1, {param} ) }
    .endif

.endmacro
Then one would call the macro with as many parameters as they want, but the parameters must be in in curly braces "{}".
This isn't a big deal but it allows for some flexibility where you may want other parameters passed that are not a part of the variable amount of parameters:

Code: Select all

call myFunctionName, {x: #34, y: foo, a: bar, p1: baz}, banked
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: ca65 goodies

Post by thefox »

Here's a fairly convenient way to create custom character mappings based on a tileset. "#" is used to notate unused characters.

EDIT: Note that line continuation has to be enabled with ".linecont +" for this to work as is.

Code: Select all

.define CHARSET .concat( "################", \
                         "################", \
                         " ###############", \
                         "################", \
                         "#ABCDEFGHIJKLMNO", \
                         "PQRSTUVWXYZ#####", \
                         "0123456789######", \
                         "#()###########-=", \
                         "#abcdefghijklmno", \
                         "pqrstuvwxyz#####", \
                         "################", \
                         "##,.##`'##,.:;!?" )

.repeat .strlen( CHARSET ), i
    .if .strat( CHARSET, i ) <> '#'
        .charmap .strat( CHARSET, i ), i
    .endif
.endrepeat

.undefine CHARSET
Last edited by thefox on Thu May 28, 2015 2:01 pm, edited 1 time in total.
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
Movax12
Posts: 541
Joined: Sun Jan 02, 2011 11:50 am

Re: ca65 goodies

Post by Movax12 »

That's pretty nice. Just note to be careful that redefining the charset doesn't cause things like "NES" in the iNES header to be changed.
User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 569
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Re: ca65 goodies

Post by Jarhmander »

Yep, that was the first pitfall I encountered when using the cc65 suite. cl65 uses the c64 charmap by default (because the default target is c64), and that "corrupted" the iNES header.
((λ (x) (x x)) (λ (x) (x x)))
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: ca65 goodies

Post by thefox »

On the same note, here are a couple of macros I've used to save/restore the charmap before modifying it. They save the original character map, and then set a "direct" mapping 1..255 => 1..255. Note that in this implementation __saveCharMap opens a new scope, so it may not be usable under all circumstances (but was fine for what I was doing).

Code: Select all

.macro __saveCharMap
    ; In scope so that CHARMAP_xxxs are local.
    .scope
        ; Find out character mappings.
        .repeat 255, i
            ; Change mapping only if it's not already direct to avoid flooding
            ; the debug information with unnecessary symbols.
            .if i + 1 <> .strat( .sprintf( "%c", i + 1 ), 0 )
                ; Save the current mapping.
                .ident( .sprintf ("__CHARMAP_%d", i + 1 ) ) .set .strat( .sprintf( "%c", i + 1 ), 0 )
                ; Set mapping to direct (1..255 -> 1..255).
                .charmap i + 1, i + 1
            .endif
        .endrepeat
.endmacro

.macro __restoreCharMap
        ; Now restore the original mapping.
        .repeat 255, i
           ; Restore only those char mappings which were modified.
            .ifdef .ident( .sprintf ("__CHARMAP_%d", i + 1 ) )
                .charmap i + 1, .ident( .sprintf ("__CHARMAP_%d", i + 1 ) )
            .endif
        .endrepeat
    .endscope
.endmacro
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
zzo38
Posts: 1096
Joined: Mon Feb 07, 2011 12:46 pm

Re: ca65 goodies

Post by zzo38 »

Is it possible in ca65 to tell some string literals to use ASCII and some to use custom character sets? My own assembler has this feature, and if you need it in ca65 too then perhaps you should add such a thing.
(Free Hero Mesh - FOSS puzzle game engine)
User avatar
Jarhmander
Formerly ~J-@D!~
Posts: 569
Joined: Sun Mar 12, 2006 12:36 am
Location: Rive nord de Montréal

Re: ca65 goodies

Post by Jarhmander »

ca65 can change the charmap "dynamically", so yes it is possible (or thefox' last macro wouldn't work/would have no practical use). It would even be possible to do it less verbosely using macros—which would look much like the last macro.
((λ (x) (x x)) (λ (x) (x x)))
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: ca65 goodies

Post by thefox »

Wrote a little macro to parse fixed point numbers (to be used as constants in code) from strings. It has some limitations, main one being that only a limited number of digits can be used (otherwise overflows in the 32-bit arithmetic). Improvements are welcome.

Example of usage:

Code: Select all

parseFixedPoint myNumber1, "-5.4321", 8
; myNumber1 = -1391 (-1391/256 = -5.43359375)

parseFixedPoint myNumber2, "18.95", 8
; myNumber2 = 4851 (4851/256 = 18.94921875)

Code: Select all

.macro parseFixedPoint targetSymbol, number, fractBits
    .local numberInt
    numberInt .set 0
    .local foundDecimalPoint
    foundDecimalPoint .set 0
    .local fractMultiplier
    fractMultiplier .set 1
    .local sign
    sign .set 1
    .repeat .strlen( number ), i
        .if .strat( number, i ) = '.'
            .if foundDecimalPoint
                .error "multiple decimal points found"
            .endif
            foundDecimalPoint .set 1
        .elseif .strat( number, i ) = '-' .or .strat( number, i ) = '+'
            .if i <> 0
                .error "sign can only appear in the beginning"
            .endif
            sign .set -1*( .strat( number, i ) = '-' )
        .else
            ; Check for overflow in case of too many digits.
            .if numberInt >= ( ( 1 << 31 ) - 1 ) / 10
                .error "can't fit digits in an integer, use less digits"
            .endif
            numberInt .set 10*numberInt + .strat( number, i ) - '0'
            .if foundDecimalPoint
                fractMultiplier .set 10*fractMultiplier
            .endif
        .endif
    .endrepeat
    ; \todo Warn if result not exact?
    ; \todo Other rounding options?
    ; \todo Is there a better way to check for overflow?
    .if numberInt * ( 1 << fractBits ) / ( 1 << fractBits ) <> numberInt
        .error "overflow in multiplication, use less digits"
    .endif
    ; \todo Break down the calculations so that more digits can be handled
    ;       without overflow.
    targetSymbol = ( numberInt * ( 1 << fractBits ) + fractMultiplier/2 ) / fractMultiplier * sign
.endmacro
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: ca65 goodies

Post by tokumaru »

Here's a simple one I came up with a while ago to deal with the problem of referencing structures of arrays generated by external tools:

Code: Select all

.macro StartStructureOfArrays _Label0, _Label1, _Label2, _Label3, _Label4, _Label5, _Label6, _Label7, _Label8, _Label9, _Label10, _Label11, _Label12, _Label13, _Label14, _Label15
	CreateLabelsForArraysInStructure *, (:+ - *) / .paramcount, _Label0, _Label1, _Label2, _Label3, _Label4, _Label5, _Label6, _Label7, _Label8, _Label9, _Label10, _Label11, _Label12, _Label13, _Label14, _Label15
.endmacro

.macro CreateLabelsForArraysInStructure _Address, _ArrayLength, _Label0, _Label1, _Label2, _Label3, _Label4, _Label5, _Label6, _Label7, _Label8, _Label9, _Label10, _Label11, _Label12, _Label13, _Label14, _Label15
	.ifnblank _Label0
		_Label0 := _Address
		CreateLabelsForArraysInStructure _Address + _ArrayLength, _ArrayLength, _Label1, _Label2, _Label3, _Label4, _Label5, _Label6, _Label7, _Label8, _Label9, _Label10, _Label11, _Label12, _Label13, _Label14, _Label15
	.endif
.endmacro

.macro EndStructureOfArrays
	:
.endmacro
Example:

Code: Select all

StartStructureOfArrays Tile0, Tile1, Tile2, Tile3, Palette, Type
	.incbin "metatiles.bin"
EndStructureOfArrays
It just measures the size of the included file and divides it by the number of labels (which is the number of arrays) to find the length of the arrays and distribute the labels accordingly.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: ca65 goodies

Post by tokumaru »

Today I found myself needing to trim strings from the left or from the right, so I came up with these:

Code: Select all

.macro ExtractLeft _Source, _Target, _Length
	.define _Target .sprintf(.sprintf("%%.%ds", _Length * (_Length > 0)), _Source)
.endmacro

.macro ExtractRight _Source, _Target, _Length, _String, _Position
	.ifblank _String _Position
		ExtractRight _Source, _Target, _Length, "", .strlen(_Source) - 1
	.else
		.if .strlen(_String) < _Length .and _Position > -1
			ExtractRight _Source, _Target, _Length, {.sprintf("%c%s", .strat(_Source, _Position), _String)}, _Position - 1
		.else
			.define _Target _String
		.endif
	.endif
.endmacro
Usage:

Code: Select all

ExtractLeft "SomeString", Output, 4
.out Output
.undefine Output

ExtractRight "SomeString", Output, 6
.out Output
.undefine Output
Getting characters from the left is easy, .sprintf alone can do it, but getting them from the right was much trickier. The function calls itself recursively to build the final string character by character. Does anyone know of a better way to do this?
calima
Posts: 1745
Joined: Tue Oct 06, 2015 10:16 am

Re: ca65 goodies

Post by calima »

Not sure if it'd work in ca65, but in C sprintf you'd do

Code: Select all

sprintf(foo, "%.6s", "SomeString" + 4);
Dynamic length, etc left as an exercise to the viewer.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: ca65 goodies

Post by tokumaru »

calima wrote:Not sure if it'd work in ca65, but in C sprintf you'd do

Code: Select all

sprintf(foo, "%.6s", "SomeString" + 4);
Doesn't work in ca65, strings are not pointers there.
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Re: ca65 goodies

Post by thefox »

Strings are bit of second class citizens in ca65, so it's not uncommon to end up with hacks like that. Maybe new builtin functions (e.g., .strleft, .strright and .strmid) should be added to complement .left, .right and .mid. The macro you gave might expand to quite inefficient code (not exactly sure when ca65 evaluates certain parts of it).
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi
Post Reply