This documentation is obsolete. For actual documentation go https://c64lib.github.io/.

Building blocks

There are plenty of elements that can be declared within library and then reused. In most cases a dedicated Kick Assembler syntactic elements are used. These elements are declared source files (asm) which can be used by your programs by using #import directive.

Most of the library source files have #importonce flag set - that is given library file can be imported in many places (in direct or indirect way) and only first import is effective. That also means that libraries can import themselves (and in fact they do!).

This document describes all kinds of elements that are declared by c64lib.

Namespace

All elements of c64lib are defined within c64lib namespace, that is, they are visible only inside this namespace. This approach prevents possible name clashes between possible other libraries that could be used together with c64lib in a project.

Label

A label is a value tagged with name. Labels are used to give symbolic names to values denoting addresses, registers or constants. Labels are useful to define memory locations and are widely used in chipset library:

.label VIC2                 = $D000 
.label SPRITE_0_X           = VIC2 + $00 
.label SPRITE_0_Y           = VIC2 + $01 
.label SPRITE_1_X           = VIC2 + $02 
.label SPRITE_1_Y           = VIC2 + $03 
.label SPRITE_2_X           = VIC2 + $04 
.label SPRITE_2_Y           = VIC2 + $05 

Labels are always declared inside c64lib namespace. Labels can be reached outside c64lib namespace by prefixing their names with namespace name (labels are the only elements that can be accessed that way due to Kick Assembler limitations). For example: at any time it is legal to write:

lda #100
sta c64lib.SPRITE_0_X

Function

A function is an element of Kick Assembler that is declared using .function keyword. Functions does not assemble into any machine code - they are used by assembler to evaluate values that can be then used in other functions, macros or to assembly a code.

Example of function that negates (inverts all bits) of its argument:

.function neg(value) {
  .return value ^ $FF
}

Example of function that calculates packed value of memory register based on its arguments:

/*
 * Params:
 * video: location of video ram: 0..15
 * bitmap: location of bitmap definition: 0..1
 */
.function getBitmapMemory(video, bitmap) {
  .return bitmap<<3 | video<<4
}

Functions are always declared inside c64lib namespace. Functions are declared in files .asm files located under lib directory.

Macro

A macro is an element of Kick Assembler that is declared using .macro directive. Once used, macro is replaced with assembly code defining it. Macros can be parametrised (that is, they can take an argument, or even more arguments) - this parametrisation can affect the code being generated.

It is noteworthy that macro is not an equivalent of subroutine, even though it looks similar to one from syntactic point of view. When macro is “called”:

someFancyMacro(parameter1, parameter2)

it does not mean, that your code will jump into place where macro someFancyMacro is declared, pass both parameters and return from that place once execution is finished. Instead, an assembler will paste code declared inside macro substituting parameters with values provided as parameter1 and parameter2.

Under some circumstances you can use macros as subroutines. This may be fast, because there is no subroutine calling overhead. This will however consume a lot of memory (in sense of generated machine code) if not used wisely.

Macros are declared in .asm files located under lib directory.

Subroutine

As subroutine we understand a piece of ML code that can be used by jumping there with jsr operation. A subroutine always ends with rts which means that at the end of execution program counter will be restored to the position right after original jsr operation and code execution will continue. In this sense a subroutine is an equivalent of procedure, function or method in high level programming languages.

Kick Assembler as such does not provide any special means to create subroutines as it is just a macro assembler. With c64lib we basically share subroutine code just by writing piece of asm code and place it in separate source files.

Subroutines are declared in .asm files located under lib/dir subdirectory. Each subroutine consists of appropriate rts operation so that it should always be accessed with corresponding jsr operation. If soubroutine consumes input parameters, they should be set accordingly before jsr is executed. Depending on the parameter passing method it should be either register setup (that is A, X or Y), memory location setup or pushing to the stack. For stack method there is a convenience library invoke available.

A subroutine code should be imported in place where it needs to be located - we don’t do it at the top of the source file but rather we use #import directive exactly in place where we want to have our subroutine.

Lets consider copyLargeMemForward subroutine as an example. We have to label a place in memory where the subroutine will start and then import the subroutine itself:

copyMemFwd:
    #import "common/lib/sub/copyLargeMemForward.asm"

The subroutine takes three parameters using stack passing method:

Stack WORD - source address
Stack WORD - target address
Stack WORD - size

So, before calling subroutine, you have to push 6 bytes to the stack. The easiest way to do it is to use invoke library:

#import "common/lib/invoke-global.asm"

and then:

c64lib_pushParamW(sourceAddress)
c64lib_pushParamW(destinationAddress)
c64lib_pushParamW(amountOfBytesToCopy)
jsr copyMemFwd

In result a subroutine will be called and amountOfBytesToCopy bytes will be copied from sourceAddress location to the destinationAddress location.

Macro-hosted subroutine

Some subroutines use this convenient method of distribution. Instead of being declared in separate source file, they are declared where macros and functions are declared - in library source files itself.

Macro-hosted subroutines are used when further parametrisation is needed before subroutine is ready to use. Usually there are some variants that can be turned on or off (in such case such macro can be called multiple times thus generating multiple versions of subroutine). Sometimes subroutine requires some zero-page addresses that we don’t want to hardcode in the library - it would be then up to the user to parametrise subroutine with addresses of choice.

Example - a scroll subroutine

Let’s consider scroll1x1:

This subroutine requires three parameters being passed via stack but also needs two consecutive bytes on zero page for functioning (indirect addressing is used). Let’s assume we will use address 4 and 5 for this purpose.

#import "text/lib/scroll1x1.asm"
#import "common/lib/invoke.asm"

...

.namespace c64lib {
    pushParamW(screenAddress)
    pushParamW(textAddress)
    pushParamWInd(scrollPtr)
}
jsr scroll

...

scroll: .namespace c64lib { scroll1x1(4) }

So, the scroll subroutine is configured for address 4 (and 5), and installed under address denoted by scroll label. It can be then normally called with jsr scroll. Before calling input parameters need to be pushed to the stack. It is done via pushParamW macros (for address values) and pushParamWInd (to extract value from memory location pointed by parameter).

Global importables

Macros and functions from c64lib are declared within c64lib namespace. As already mentioned this is to avoid name clashes when using libraries from different sources. Due to KickAssembler limitations it is not possible to access macro or function from within namespace using “dot” notation. In order to allow accessing elements from outside c64lib namespace, each library file has _global counterpart that can be alternatively imported. In such case all elements such as functions and macros will be available straight from root namespace by using c64lib_ name prefix.

Example - you can use elements from invoke in traditional way:

#import "common/lib/invoke.asm"

.namespace c64lib {
    pushParamW(sourceAddress)
    pushParamW(destinationAddress)
    pushParamW(amountOfBytesToCopy)
}

or by using global importable:

#import "common/lib/invoke-global.asm"

c64lib_pushParamW(sourceAddress)
c64lib_pushParamW(destinationAddress)
c64lib_pushParamW(amountOfBytesToCopy)