This document is under development!

The c64lib is a set of Kick Assembler macros grouped in several libraries. They are the building blocks that can be reused in retro projects. The intention behind c64lib is to provide retro community with modern development tools such as build support, CI assistance, dependency management, artifact versioning and comprehensive documentation.

The c64lib is hosted on GitHub: https://github.com/c64lib/.

1. Overview

The c64lib project consists of multiple libraries. Each library is hosted as separate GitHub repository and is plugged into CircleCI build system for Continuous Integration.

There are following core libraries:

common

The one that hosts some commonly used functions, macros and subroutines; no other c64lib library can work without it. This code is intended to be platform-independent, it complies to typical MOS 6502 assembler syntax (except it is written in KickAss, which is C64 specific at the moment).

chipset

The one that hosts code that is platform specific and is dedicated to handle typical C64 chipset such as VIC-II, SID, CIA as well as typical C64 memory layout.

text

The one that handles displaying text in text mode of Commodore 64 / VIC-II.

copper64

Utility library that makes raster interrupt programming on VIC-II pleasant and easy.

The diagram below describes dependencies between internal libraries of c64lib project. There is also an excellent 64spec testing library used and shown as external reference.

c64lib deps.excalidraw

2. Installation

The c64lib is a set of libraries that can be used in assembly programs written with Kick Assembler. Each library consists of one or more source files that shall be then imported in Kick Assembler program. There are several methods of downloading and "installation" of libraries, some of them will be presented there starting from the most convenient one.

2.1. Using Gradle as a build tool

The easiest way to use c64lib is to add the libraries as dependencies to the gradle build. It is an easy task due to Retro Build Tool.

The complete manual for Retro Build Tool is available at: https://c64lib.github.io/gradle-retro-assembler-plugin/.

2.1.1. When Gradle is already used

Well, when you are a happy user of Retro Assembler plugin, then you are even happier, because it’s extremely easy to add C64Lib to your project. The c64lib is a GitHub project, so it can be added as GitHub dependency to your gradle.build file. Put the following lines into your retroProject section of the build file:

libFromGitHub "c64lib/common", "0.4.0"
libFromGitHub "c64lib/chipset", "0.4.0"
libFromGitHub "c64lib/text", "0.4.0"
libFromGitHub "c64lib/copper64", "0.4.0"

Then you will be able to use all four libraries from c64lib. Of course not all four are mandatory. You can use any subset of them as long as you obey dependency graph as shown in Overview.

Retro Assembler plugin downloads all dependencies into default location:

.ga/deps

All libraries from c64lib will be downloaded under c64lib subfolder therefore the following location:

.ga/deps/c64lib

must be added to the lib dir so that Kick Assembler will see them when interpreting #import directive. The following must be also a part of your gradle.build file:

libDirs = [".ra/deps/c64lib"]

The complete gradle.build will be following:

plugins {
    id "com.github.c64lib.retro-assembler" version "1.6.0"
}

retroProject {
    dialect = "KickAssembler"
    dialectVersion = "5.25"
    libDirs = [".ra/deps/c64lib"]

    libFromGitHub "c64lib/common", "0.4.0"
    libFromGitHub "c64lib/chipset", "0.4.0"
    libFromGitHub "c64lib/text", "0.4.0"
    libFromGitHub "c64lib/copper64", "0.4.0"
}

In order to build your executable you just need to execute gradle:

./gradlew

or

./gradlew build

In result C64 executables (a prg files) will be created.

2.1.2. When Gradle is not used

but you really want to start using it, you have to enable it first. Following steps are requires as preconditions:

  • Download and install JDK 8 or higher.

  • Download and install Gradle.

Once it is done, you have to restart your console/terminal application and go to your project location. In your location you run Gradle to install a wrapper:

gradle wrapper

then you add gradle.build file using following content (or similar):

plugins {
    id "com.github.c64lib.retro-assembler" version "1.6.0"
}

retroProject {
    dialect = "KickAssembler"
    dialectVersion = "5.25"
    libDirs = [".ra/deps/c64lib"]

    libFromGitHub "c64lib/common", "0.4.0"
    libFromGitHub "c64lib/chipset", "0.4.0"
    libFromGitHub "c64lib/text", "0.4.0"
    libFromGitHub "c64lib/copper64", "0.4.0"
}

Of course, the set of used libraries (with libFromGitHub element) may vary as well as the version of Kick Assembler.

And that’s it: from now on you are able to build your project using simple gradlew or gradlew build commands. It’s not even necessary to have Gradle installed. All you need is Java 8 or higher.

2.2. Manual clone from GitHub

If you don’t want to or cannot use Retro Assembler plugin, you can use your git client and clone libraries manually and then just point the location with -libdir parameter of the KickAss.

Lets assume your project has following directory layout on the disk:

work
  |--libs
  +--project
       |--SomeFile.asm
       +--SomeOtherFile.asm

Then you go to the libs directory (cd work/libs), and then clone as many libraries from c64lib as you need:

git clone https://github.com/c64lib/common.git
git clone https://github.com/c64lib/chipset.git
git clone https://github.com/c64lib/text.git
git clone https://github.com/c64lib/copper64.git

This will checkout latest released version of the library (actually a top of the master branch, which usually means the same). In result, you will get something like this:

work
  |--libs
  |    +--common
  |         +--lib
  |              |--common.asm
  |              |--invoke.asm
  |              |--invoke-global.asm
  |              |--math.asm
  |              |--math-global.asm
  |              |--mem.asm
  |              +--mem-global.asm
  |    +--chipset
  |         |--...
  |    +--text
  |         |--...
  |    +--copper64
  |         |--...
  +--project
       |--SomeFile.asm
       +--SomeOtherFile.asm

If you then specify -libdir parameter to the KickAss appropriately, you’ll be able to use the libs (asm files in lib directory) with simple #import directive, i.e.:

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

As mentioned earlier, checkout from master branch ensures that last released version of library is used. If you want to change it and use concrete version from the past, after git clone you have to enter the cloned directory (i.e. cd common) and checkout desired version:

git checkout 0.4.0

(for version 0.4.0).

Assembling is then possible with manual invocation of Kick Assembler:

java -jar c:\ka\KickAss.jar -libdir ../libs SomeFile.asm
java -jar c:\ka\KickAss.jar -libdir ../libs SomeOtherFile.asm

2.3. Manual copy

Least desired method of installation of c64lib is to download source code of given version and unzipping it into target directory. It is not a very convenient method, but it does not require Gradle nor Git to be installed on your computer.

For every library module you have to visit GitHub and open Releases tab:

https://github.com/c64lib/common/releases/tag/0.1.0

Under assets, you will see zipped content of the library. Download it and unzip into desired location, i.e. into libs directory. In result, you end up with a similar layout as with "Git clone" method (see above).

You use exactly the same method to use library in your source code, i.e.:

#import "common/lib/invoke_global.asm"

and you invoke Kick Assembler using the same syntax:

java -jar c:\ka\KickAss.jar -libdir ../libs SomeFile.asm

assuming, that your libs directory exists on the same level as your project directory.

3. Building blocks

3.1. The c64lib namespace

Each element mentioned in this section are declared inside c64lib namespace. This is to avoid potential name clashes with other libraries and/or with client assembly programs. Kick Assembler allows to access namespaced objects via c64lib. prefix, but this, unfortunately, works for labels only. All other elements such as functions and macros can be only accessed from within c64lib namespace.

3.2. Global imports

If accessing elements via c64lib namespace is, by any reason, not possible nor convenient, there are also so-called "global" declarations, where certain elements of the library are exposed with global names (thus accessible in global namespace). All such global declarations have names prefixed with c64lib_ prefix, to avoid name clashes.

3.3. Labels

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

3.4. Functions

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 1. Function that negates (inverts all bits) of its argument.
.function neg(value) {
    .return value ^ $FF
}
Example 2. 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.

3.5. Macros

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.

3.6. Subroutines

Subroutine is a consistent set of assembly instructions that performs concrete operation. The major difference between plain macro and subroutine is that macro is used to substitute commonly used patterns of intructions (at cost of growing machine code size) and subroutine is used to save on machine code size actually.

3.6.1. Regular subroutines

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.

3.6.2. Macro-hosted subroutines

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 3. 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).

4. Functional guidelines

4.1. Subroutines

4.2. Passing parameters to subroutines

In most cases a subroutine requires one or more input parameters. Number of parameters is basically unlimited. The size of each parameters can also vary (however usually it is 1 or 2 bytes). There are three methods of passing such parameters to the subroutine:

  • Using CPU registers.

  • Using fixed memory locations.

  • Using call stack.

The "CPU regs" method is the simplest and the most efficient, however, due to limited number and size of MOS 6502 registers (A, X, Y, each 1 byte in size) its application is very limited.

The "Fixed memory locations" method does not have this size limitation and also, when using zero page, can be quite performant. This method can be rather tricky in application, because memory locations must be carefully choosen not to clash between different, possibly cooperating subs. The most pragmatic approach to this method is to use Macro-hosted subroutines, where these fixed memory locations can be configured via macro attributes.

The "call stack" method uses CPU stack located at page 1 to pass parameters. This way it is possible to pass more data than via CPU regs method and does not require fixed memory locations to be used. The stack size is of course very limited (256 bytes max, but each jsr consumes at least 3 bytes).

It is possible and quite often useful to combine "CPU regs" and "Call stack" method: some arguments are passed via registers and some via stack. We’ll call this method a hybrid invocation.

4.2.1. Support for call stack parameters passing

The common library contains helper macros for parameter passing via the stack. Recursive calls to the subroutines are not supported.

Related sources
  • c64lib/common/lib/invoke.asm.

  • c64lib/common/lin/invoke-global.asm.

Parameters should be pushed onto the stack just before calling subroutine (which is done via jsr instruction). Within the subroutine, parameters must be fetched from the stack in reverse order and stored elsewhere. Before this is done, a return pointer must be preserved in order to enable return from the subroutine (a rts instruction).

There are four macros for pushing parameters to the subroutine:

pushParamB

Pushes single byte as a parameter. The byte is provided via macro parameter.

pushParamW

Pushes two bytes as a parameter. The word is provided via macro parameter.

pushParamBInd

Pushes single byte as a parameter. The address of this byte is provided via macro parameter.

pushParamWInd

Pushes two bytes as a parameter. The starting address of this two-byte value is provided via macro parameter.

All these macros are destroying A register, that should be considered when using hybrid invocation (first push params via macros, last set the A parameter value).

Example 4. copyLargeMemForward invocation.

The copyLargeMemForward subroutine copies up to 64K of data from one memory location to another memory location. These memory locations can overlap as long as we copy from lower address to higher address. This subroutine uses call stack invocation method and requires 6 bytes of input parameters:

  • Source data address.

  • Destination address.

  • Data size.

Let’s assume we want to copy 1024 bytes from $3000 to $4000. Example of the subroutine invocation is following:

pushParamW($3000)
pushParamW($4000)
pushParamW(1024)
jsr copyLargeMemForward

The subroutine, once being called, must do the following:

  1. Pull return address from the stack and store it in temporary place.

  2. Pull all parameters from the stack in reverse order and store them in some internal placeholders.

  3. Push return address back to the stack.

Preserving and restoring of return address may be done via invokeStackBegin and invokeStackEnd macros. Each of these macros takes single argument that denotes temporary address of 2-byte large placeholder where return address can be preserved.

The invokeStackBegin should be called before any parameter is pulled from the stack. The invokeStackEnd should be called before a rts instruction is called.

There are three macros for pulling parameters from the stack (remember to call them in reverse order, this is how the stack works):

pullParamB

Pulls a single byte from the stack and stores it under address given as a macro parameter.

pullParamW

Pulls two bytes from the stack and stores them under address given as a macro parameter.

pullParamWList

Pulls two bytes from the stack and stores them under multiple addresses provided as an input parameter list.

Example 5. rotateMemRight subroutine implementation.

The rotateMemRight subroutine rotates up to 256 bytes of the memory to the right. It uses hybrid invocation: a memory address (2 bytes) should be passed via stack, and size of the memory window should be set into X register.

It uses self modifying code technique and copies input address directly into four address location in its own code.

  rotateMemRight: {

  invokeStackBegin(returnPtr)
  pullParamWList(List().add(loadFirst, loadNext, staNext, staLast))

  lda loadFirst:$ffff, x
  sta preserve
  loop:
    dex
    lda loadNext:$ffff, x
    inx
    sta staNext:$ffff, x
    dex
  bne loop
  lda preserve
  sta staLast:$ffff

  invokeStackEnd(returnPtr)
  rts
  // local vars
  returnPtr:      .word 0
  preserve:       .byte 0
  }

It is noteworthy, that semi-local variables are declared as additional 3 bytes at the end of the subroutine (guarded by preceding rts instruction). These variables are used to preserve return address and additional single byte for rotation.

4.3. Memory operations

The common library helper macros helps with common memory related operations.

Related sources
  • c64lib/common/lib/mem.asm.

  • c64lib/common/lib/mem-global.asm.

4.3.1. Setting the memory

There are a bunch of set* pseudocommands that can be used to quickly set given memory cells to some value. The set8 works with 1-byte values, the set16 works with 2-byte values (words).

The following sets content of memory address $B000 to 0:

set8 #0 : $B000

The following sets content of memory address $C000 to a value stored under address $0F:

set8 $0F : $B000

It is also possible to set two consecutive bytes to a given 2-byte value, but one needs to use the following macro for that (sets $B000 to $02 and $B001 to $01):

set16($0102, $B000)

There is also a macro for setting one byte value which can be used as long as immediate addressing is needed:

set8($01, $B000)

In order to fill specified memory block with given value a fillMem subroutine can be used.

Example 6. Filling memory with a value.

In order to use this subroutine import it and place a label before it so it can be called later.

fillMem: #import "common/lib/sub/fill-mem.asm"

This subroutine uses hybrid invocation, push start address of the memory block into the stack and set value in A and block size in X. As you see, this subroutine is limited to 255 as maximum size of the block.

The following code can be used to fill 200 bytes of memory starting from $B000 address with value 0.

pushParamW($B000)
lda #0
ldx #200
jsr fillMem

4.3.2. Memory transfer

You can use copy8 pseudocommand to copy one byte from one place to another using A:

copy8 $B000:$C000

You can use copy16 pseudocommand to copy two consecuive bytes from one place to another:

copy16 $B000:$C000

For fast, unrolled copying of data block use copyFast macro. To copy 20 bytes from $B000 to $C000 use the following:

copyFast($B000, $C000, 20)

Remember, that copyFast macro will consume a lot of space if count parameter (the last one) will be big.

There is a "slow" copy subroutine that is handy for "unpacking" a PRG file and moving arbitrary sized blocks of data to the target location. This subroutine can be used to move SID data (music) into target location as well as to move VIC-II data (charsets, sprites) into the VIC-II addressable bank. Target and source spaces can overlap as long as target address is bigger than source address.

Example 7. Copying large blocks of data

In order to use this subroutine import it and place a label before, so that it can be called later.

copyLargeMemForward: #import "common/lib/sub/copy-large-mem-forward.asm"

This subroutine requires three WORD parameters to be placed on stack: * source address * target address * data size (can be more than 256)

pushParamW($A000)
pushParamW($E000)
pushParamW(1024)
jsr copyLargeMemForward

4.4. 16-bit math

The common library helper macros helps with common math related operations. They "extend" a basic set of math opcodes used to do arithmetics with macros and pseudocommands that operates on two-byte sized values (words).

Related sources
  • c64lib/common/lib/math.asm.

  • c64lib/common/lib/math-global.asm.

4.4.1. Increments, decrements

You can easily increment and decrement 16-bit counters using following macros:

inc16($2000) // increments counter located in $2000,$2001
dec16($3000) // decrements counter located in $3000,$3001

4.4.2. Adding, subtraction

You can add / subtract value to / from a value stored in memory via add16 and sub16 macros.

In example to add 315 to the value stored under $B000:

add16(315, $B000)

Alternatively you can use add16 and sub16 pseudocommands:

add16 $B000 : $C000

You can add / subtract value stored in one memory address to / from a value stored in another memory address via addMem16 and subMem16, respectively.

In example to add value from address $B000:$B001 to the value from address $C000:$C001:

addMem16($B000, $C000)

4.5. Far conditional jumps

Well known limitation of MOS 6502 machine code is that conditional jump instructions use 1 byte for relative jump shift. This means that if you want to jump too far, you have to use conditional / absolute jump combination, which is cumbersome.

Related sources
  • c64lib/common/lib/common.asm.

  • c64lib/common/lib/common-global.asm.

The c64lib offers few macros that can simplify this task:

fbne(label)

It calculates the jump length and if it is too far it replaces simple bne with beq/jmp combination.

fbmi(label)

It calculates the jump length and if it is too far it replaces simple bmi with bpl/beq`jmp` combination.

4.6. Handling of RLE compression

The c64lib supports very basic yet useful compression method called RLE. It is particularly useful for data consisting of repetitive patterns of bytes such as simple graphics, game maps and so on.

Related sources
  • c64lib/common/lib/compress.asm

  • c64lib/common/lib/compress-global.asm

One can call compressRLE(data) macro that crunches provided data during compilation of program. Such data must be loaded with LoadBinary(filename) function.

Compressed data can be decompressed (and relocated) with decompressRLE subroutine.

decompressRLE: #import "common/lib/sub/decompress_rle.asm"

This subroutine requires three WORD parameters to be placed on stack: * source address * target address

.var data = LoadBinary("data-to-be-compressed.bin")

sourceAddress: c64lib_compressRLE(data)

// ...

pushParamW(sourceAddress)
pushParamW(targetAddress)
jsr decompressRLE

// ...

targetAddress:

4.7. Commodore 64 memory layout management

The Commodore 64 is known to have extremely flexible memory layout configuration. The c64lib library provides simple macro to change memory configuration in single line of code.

Related sources
  • c64lib/chipset/lib/mos6510.asm.

  • c64lib/chipset/lib/mos6510-global.asm.

To change memory configuration one can run configureMemory(config) macro, where config can have one of the following values:

  • RAM_RAM_RAM

  • RAM_CHAR_RAM

  • RAM_CHAR_KERNAL

  • BASIC_CHAR_KERNAL

  • RAM_IO_RAM

  • RAM_IO_KERNAL

  • BASIC_IO_KERNAL

The BASIC_IO_KERNAL is an initial value after C64 is being powered on. Names of the labels above are self explanatory: each section represents one of switchable region: RAM or BASIC, RAM/IO/CHARSET and RAM or KERNAL.

Example 8. Switching to the most useful memory layout

You usually don’t need BASIC and KERNAL but still want IO region to be banked in.

You have to ensure that interrupts are disabled during configuration process. You would like to disable default interrupt sources (as KERNAL is banked out the interrupt vectors are too).

sei
c64lib_disableNMI()
c64lib_configureMemory(c64lib.RAM_IO_RAM)
c64lib_disableCIAInterrupts()
cli

4.8. VIC-II memory management

4.8.1. Configuring VIC-II memory bank

Configuration of which one out of four C64 memory banks is addressable by VIC-II chip can be done via reprogramming the CIA-2 chip.

Related sources
  • c64lib/chipset/lib/cia.asm.

  • c64lib/chipset/lib/cia-global.asm.

The change can be done via setVICBank`c64lib_setVICBank` macro.

The following code:

c64lib_setVICBank(c64lib.BANK3)

sets up the last memory bank ($C000-$FFFF) for VIC-II graphic chip.

4.8.2. VIC-II memory layout management

There are two distinct memory areas that have to be fconfigured for VIC-II. Their meaning is different depending on the screen mode. For text modes it would be screen memory and charset memory. For bitmap modes it would be screen memory and bitmap memory.

4.9. VIC-II NTSC detection

4.10. VIC-II IRQ handling

4.11. Text outputting

The text library offers macros and subroutines to output texts and numbers.

Related sources
  • c64lib/text/lib/text.asm.

  • c64lib/text/lib/text-global.asm.

  • c64lib/text/lib/sub/out-hex-nibble.asm.

  • c64lib/text/lib/sub/out-hex.asm.

The outTextXYC outputs text terminated with $FF that is no longer than 255 characters. As it is a macro-hosted subroutine, it must be instantiated with screen and color RAM addresses first. Such subroutine can be then used to output different texts at different screen position and with different color.

myOutTextXYC: c64lib_outTextXYC($1000, $D800)

text: .text "Commodore 64"; .byte $ff

c64lib_pushParamW(text)
ldx #5
ldy #12
lda #LIGHT_GREEN
jsr myOutTextXYC

The outNumberXYC outputs two digit number (a byte). As this is a macro-hosted subroutine, it must be instantiated with screen and color RAM addresses first. Such subroutine can be then used to output numbers at different screen position and with different color. This subroutine displays bytes in hexadecimal form unless number is BCD packed.

myOutNumberXYC: c64lib_outNumberXYC($1000, $D800)

number: .byte 3*16 + 6

c64lib_pushParamW(number)
ldx #5
ldy #12
lda #LIGHT_GREEN
jsr myOutNumberXYC // displays 36 at (5,12) using light green color

4.12. Text scrolling

coming soon

4.13. 2x2 scrollable background

coming soon

5. Copper 64

The Coppper 64 library is a tool that eases complex raster-related effects that can be achieved by Commodore 64.

Raster-related effects are all effects that are triggered at certain raster line. The Commodore 64 and its VIC-II chip can be programmed in a way so that CPU is interrupted once certain raster line of the screen is drawn by CRT (or LCD). If this interrupt then performs certain actions, sometimes even by reprogramming of VIC-II itself, a wide variety of interesting effects can be achieved:

  • splitting the screen into two or more pieces each using different screen mode or addressing,

  • extending amount of visible colours by changing global colour registers of VIC-II,

  • extending amount of visible sprites by reusing eight available sprite slots in different regions of the screen (so-called sprite multiplexing),

  • displaying colorful raster bars or achieving "rainbow fonts",

  • performing certain video-RAM operations at given moments to achieve smooth scrolling,

  • doing any "background task" that requires regular update such as playing music or incrementing timers,

  • and many more…​

Usually you need to perform various raster-relation actions at once, on single screen. This means that you need to perform several different handling code for several different raster times. Unfortunately, the VIC-II chip and its interrupt system allows just to specify single raster value at a time - that is, you can only trigger an interrupt at single line.

This is a limitation that can be overcame: all you need to do is to reprogram VIC-II raster register at the end of interrupt handling method, maybe also reprogram IRQ vector of MOS 6502 so that the VIC-II will execute second IRQ handler at second position. Of course, you have to reprogram raster register at the end of the second IRQ handler, the same needs to be done for IRQ vector. This way you have two distinct IRQ handlers fired at two distinct raster positions.

This approach can be generalized to N handlers and raster positions. Of course at each time you have to ensure that:

  1. Raster position grows except the very last position which should reset to the lowest raster at the end.

  2. You have to ensure that IRQ handler have enough time to execute itself (the next raster position must be big enough so that we have enough cycles to execute the whole IRQ handler).

Not conforming to any of the rules above results in effect called "frame-skips", that is the VIC-II will display the whole frame before next handler will be run.

Futhermore, the MOS 6502 are unstable by default, that is, it is usually hard to predict when exactly the code execution by the CPU will be interrupted. Some visual effects are affected by this problem, prominently screen splits and raster bars. Special programming techniques including code cycling and double interrupts are used to mitigate this problem.

The Copper 64 library solves most of these problems. It is configured via special table allowing to specify various effects being triggered at raster lines. The design of this library has been inspired by Amiga’s Copper chip (and so-called Copper list) or 8-bit Atari’s display list.

Related sources
  • c64lib/copper64/lib/copper64.asm.

  • c64lib/copper64/lin/copper64-global.asm.

5.1. Defining Copper table

Copper table is a block of memory containing copper entries, 4 bytes each.

Table 1. Copper 64 entry definition
Byte offset Name Description

0

Control

Control byte serving different purposes (see below).

1

Raster

Lower 8 bits of raster line address.

2

Data 1

First byte of data.

3

Data 2

Second byte of data.

The control byte has multiple purposes depending on its value:

Table 2. Control value
Control value Description

$00

Stop execution of the copper list.

$FF

End of the copper list (start from the beginning - loop).

$01 - $FE

Execute function (see below).

For values from range $01 - $FE the bits of the control byte have following meanings.

Table 3. Function selector structure
Bit Description

7

Nineth bit of the raster line address.

6

Reserved, should be 0.

5

Reserved, should be 0.

4..0

IRQ Function - 1..31.

There are two macros that can be used to simplify copper table definition:

copperEntry

Creates single effect line of copper list. This macro takes following arguments: raster - raster counter (9 bits), function - IRQ handler code, data1 - first byte of function data, data2 - second byte of function data.

copperLoop

Is used to finish copper list - the copper 64 wraps to the first line once loop line is detected. This macro does not take any arguments.

5.2. Copper 64 main subroutine

The main Copper 64 subroutine must be preconfigured using hosted subroutine approach.

Copper 64 requires three bytes from zero page: two subsequent bytes to store copper list address and one byte for copper list pointer.

Install main subroutine with following macro call:

.label COPPER_LIST_ADDR = $10
.label COPPER_LIST_PTR = $12

startCopper(
    COPPER_LIST_ADDR,
    COPPER_LIST_PTR,
    List().add(c64lib.IRQH_JSR, c64lib.IRQH_BG_RASTER_BAR, c64lib.IRQH_BG_COL_0).lock())

Note that all three location on the zero page can be freely configured via first and second parameter of the macro call.

As the third argument you have to pass a locked list of all IRQ handlers you are going to use in copper list. That is, even though Copper 64 supports more than dozen of different effects, only few of them can be used at once (this limitation is caused by the fact, that Copper 64 is cycled to achieve stable effects, therefore all handlers must fit into single page of memory).

Once hosted subroutine is installed and configured, you can make call to it, ensuring that firstly you set up copper list address into appropriate zero page location. This way you can easily reuse the same main subroutine for different copper list, this works well as long as you use the same set of IRQ handlers in all of these lists. If by any reason you cannot use the same list of IRQ handlers (because i.e. they do not fit into 256 bytes of memory), you have to configure and install main subroutine twice.

5.3. Stop Copper 64 subroutine

Basically you have to stop IRQ to have Copper 64 deactivated, or reprogram IRQ vector, or both. You can use the following macro to just turn VIC-II initiated IRQ off:

stopCopper()

5.4. Copper 64 effects

5.4.1. Set border color

Changes border color.

  • Handler label: IRQH_BORDER_COL

  • Handler code: 1

  • Argument 1: desired border color; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BORDER_COL, <color>, 0)

5.4.2. Set background color 0

Changes background color 0.

  • Handler label: IRQH_BG_COL_0

  • Handler code: 2

  • Argument 1: desired background 0 color; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BG_COL_0, <color>, 0)

5.4.3. Set background color 1

Changes background color 1.

  • Handler label: IRQH_BG_COL_1

  • Handler code: 3

  • Argument 1: desired background 1 color; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BG_COL_1, <color>, 0)

5.4.4. Set background color 2

Changes background color 2.

  • Handler label: IRQH_BG_COL_2

  • Handler code: 4

  • Argument 1: desired background 2 color; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BG_COL_2, <color>, 0)

5.4.5. Set background color 3

Changes background color 3.

  • Handler label: IRQH_BG_COL_3

  • Handler code: 5

  • Argument 1: desired background 3 color; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BG_COL_3, <color>, 0)

5.4.6. Set border and background 0 color uniformly

Changes background color 0 and border color to the same color.

  • Handler label: IRQH_BORDER_BG_0_COL

  • Handler code: 6

  • Argument 1: desired color for border and background 0; 0..15

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BORDER_BG_0_COL, <color>, 0)

5.4.7. Set border and background 0 color separately

Changes background color 0 and border color to another values in single step, the colors are specified as arguments.

  • Handler label: IRQH_BORDER_BG_0_DIFF

  • Handler code: 7

  • Argument 1: desired color for border; 0..15

  • Argument 2: desired color for background 0; 0..15

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BORDER_BG_0_DIFF, <border color>, <background color>)

5.4.8. Set VIC memory register and VIC memory bank

Changes VIC memory control and VIC memory bank in one step.

  • Handler label: IRQH_MEM_BANK

  • Handler code: 8

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MEM_BANK, <memory control>, <vic bank number>)

5.4.9. Set VIC mode and memory settings

Changes VIC display mode and memory settings in one step. VIC bank cannot be changed.

  • Handler label: IRQH_MODE_MEM

  • Handler code: 9

  • Argument 1: mode of vic2; for performance reasons the values for two control registers are packed in one byte: %00010000 for Multicolor, %01100000 for ECM or Bitmap

  • Argument 2: value for MEMORY_CONTROL register

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_MEM, <vic mode>, <memory control>)

5.4.10. Jump to custom subroutine

Jumps to custom subroutine that can do whatever you want, i.e. play music. Subroutine must end with rts.

  • Handler label: IRQH_JSR

  • Handler code: 10

  • Argument 1: Low byte of subroutine address

  • Argument 2: High byte of subroutine address

  • Cycled: no

Usage:

copperEntry(<raster>, c64lib.IRQH_JSR, <address, >address)

5.4.11. Set hires bitmap mode

Sets up hires bitmap mode using given memory layout and VIC bank. Useful for screen splits using totally different memory locations for VIC chip.

  • Handler label: IRQH_MODE_HIRES_BITMAP

  • Handler code: 11

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_HIRES_BITMAP, <memory control>, <vic bank number>)

5.4.12. Set multicolor mode

Sets up multicolor bitmap mode using given memory layout and VIC bank. Useful for screen splits using totally different memory locations for VIC chip.

  • Handler label: IRQH_MODE_MULTIC_BITMAP

  • Handler code: 12

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_MULTIC_BITMAP, <memory control>, <vic bank number>)

5.4.13. Set hires text mode

Sets up hires text mode using given memory layout and VIC bank. Useful for screen splits using totally different memory locations for VIC chip.

  • Handler label: IRQH_MODE_HIRES_TEXT

  • Handler code: 13

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_HIRES_TEXT, <memory control>, <vic bank number>)

5.4.14. Set multicolor text mode

Sets up multicolor text mode using given memory layout and VIC bank. Useful for screen splits using totally different memory locations for VIC chip.

  • Handler label: IRQH_MODE_MULTIC_TEXT

  • Handler code: 14

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_MULTIC_TEXT, <memory control>, <vic bank number>)

5.4.15. Set extended background mode

Sets up extended text mode using given memory layout and VIC bank. Useful for screen splits using totally different memory locations for VIC chip.

  • Handler label: IRQH_MODE_EXTENDED_TEXT

  • Handler code: 15

  • Argument 1: value for MEMORY_CONTROL register

  • Argument 2: value for VIC bank (goes to CIA2_DATA_PORT_A); only two least significant bits are taken, other bits of the data port are preserved

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_MODE_EXTENDED_TEXT, <memory control>, <vic bank number>)

5.4.16. Full raster bar

Generates colorful raster bar across whole screen including border. Color for each subsequent bar line is fetched from $FF terminated array of colors (values 0..15). Because procedure is cycled using busy waiting on raster, a raster time for whole bar will be consumed. Color array can be cycled or modified in any way to get interesting animation effects.

  • Handler label: IRQH_FULL_RASTER_BAR

  • Handler code: 16

  • Argument 1: Low byte of bar color definition address

  • Argument 2: High byte of bar color definition address

  • Cycled: yes (PAL, 63 cycles) - it sucks on badlines however

Usage:

copperEntry(<raster>, c64lib.IRQH_FULL_RASTER_BAR, <address, >address)

5.4.17. Background-only raster bar

Generates colorful raster bar across whole background. Color for each subsequent bar line is fetched from $FF terminated array of colors (values 0..15). Because procedure is cycled using busy waiting on raster, a raster time for whole bar will be consumed. Color array can be cycled or modified in any way to get interesting animation effects.

  • Handler label: IRQH_BG_RASTER_BAR

  • Handler code: 17

  • Argument 1: Low byte of bar color definition address

  • Argument 2: High byte of bar color definition address

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_BG_RASTER_BAR, <colorCycleDef, >colorCycleDef)
...
colorCycleDef:  .byte COLOR_3, LIGHT_RED, RED, LIGHT_RED, YELLOW, WHITE, YELLOW, YELLOW, COLOR_3, $ff

5.4.18. Horizontal scroll

Scrolls screen horizontally using specified amount of pixels.

  • Handler label: IRQH_HSCROLL

  • Handler code: 17

  • Argument 1: value for horizontal scroll register (0..7)

  • Argument 2: unused

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_HSCROLL, <scroll value>, 0)

5.4.19. Mapped horizontal scroll

Applies shallow tech-tech effect (using values 0..7) starting from given raster position. Horizontal scroll value for each corresponding raster line is taken from $FF terminated array of values, each should contain value from 0..7 range. The scroll map can be further modified (i.e. rotated) to achieve interesting animation effects.

  • Handler label: IRQH_HSCROLL_MAP

  • Handler code: 17

  • Argument 1: low byte of scroll map definition address

  • Argument 2: high value of scroll map definition address

  • Cycled: yes (PAL, 63 cycles)

Usage:

copperEntry(<raster>, c64lib.IRQH_HSCROLL_MAP, <hscrollMapDef, >hscrollMapDef)
...
hscrollMapDef:  .fill TECH_TECH_WIDTH, round(3.5 + 3.5*sin(toRadians(i*360/TECH_TECH_WIDTH))) ; .byte 0; .byte $ff

5.5. Examples

5.5.1. Level screen for a TRex-64 game

We would like to display a new level screen for a video game. This screen should play background music, display some information and handle controls to start the game when player is ready. Lets consider the following requirements:

  1. Music player should be triggered once per frame.

  2. Regular font used on this screen should use "rainbow font" effect using smooth shades of grey.

  3. Get ready text should use different shades (yellow and red like fire) which is additionally animated by cycling the rainbow.

  4. We need a counter that can be used to calculate delay - this is required to handle keyboard input properly.

copper64 trex level

The whole screen can be built using following copper table:

copperList:
    copperEntry(10, IRQH_JSR, <playMusic, >playMusic)
    copperEntry(80, IRQH_JSR, <scrollColorCycle2, >scrollColorCycle2)
    copperEntry(124, IRQH_BG_RASTER_BAR, <colorCycle1, >colorCycle1)
    copperEntry(140, IRQH_BG_RASTER_BAR, <colorCycle2, >colorCycle2)
    copperEntry(156, IRQH_BG_RASTER_BAR, <colorCycle1, >colorCycle1)
    copperEntry(245, IRQH_JSR, <dly_handleDelay, >dly_handleDelay)
    copperLoop()

For background raster bar effect we use following arrays defining color cycles:

colorCycle1: .byte GREY, GREY, GREY, LIGHT_GREY, WHITE, WHITE, LIGHT_GREY, GREY, GREY, BLACK, $ff
colorCycle2: .byte BLACK, LIGHT_RED, RED, LIGHT_RED, YELLOW, YELLOW, WHITE, YELLOW, YELLOW, BLACK, $ff

Additionally, the content of colorCycle2 array is rotated using the following code, that is triggered by second entry of the copper table:

scrollColorCycle2: {
  dec z_colorCycleDelay2
  bne !+
    lda #COLOR_CYCLE_DELAY
    sta z_colorCycleDelay2
    rotateMemRightFast(colorCycle2 + 1, 6)
  !:
  rts
}

Note that it is essential, that last byte of the colour cycle entry is the same as the screen colour (BLACK in this case) - this restores bg colour of the screen remainder.

Also note, that "rainbow" font effect requires inverted charset to be used, because rainbow is done via rasterbar effect on background colour.

The Copper 64 main subroutine can be configured in following way:

startCoopper:
  startCopper(
    $03,
    $05,
    List().add(c64lib.IRQH_JSR, c64lib.IRQH_BG_RASTER_BAR).lock())

Main subroutine can be then started multiple time using the following code:

  lda #<copperList
  sta $03
  lda #>copperList
  sta $04
  jsr startCopper

As shown above, the Copper 64 subroutine can be easily reused with different copper lists as long as the same effects are used. All to be done is to set up copper list address which is in this example stored in addresses $03 and $04.

5.5.2. Title screen for a TRex-64 game

The title screen for the TRex-64 video game uses rainbow fonts, both static and animated as well as fade in/fade out effect on credits/instruction text. The fade effect is also done with background colour, so also inverted charset must be used to hide all parts of the background that shouldn’t be visible.

copper64 trex title

This screen is implemented with following copper list:

titleScreenCopperList:
        copperEntry(10, IRQH_JSR, <playMusic, >playMusic)
        copperEntry(80, IRQH_JSR, <scrollColorCycle2, >scrollColorCycle2)
    fadeEffectColor:
        copperEntry(166, IRQH_BG_COL_0, BLACK, 0)
        copperEntry(190, IRQH_JSR, <rotateColors, >rotateColors)
        copperEntry(206, IRQH_BG_COL_0, BLACK, 0)
        copperEntry(235, IRQH_BG_RASTER_BAR, <colorCycle2, >colorCycle2)
        copperEntry(250, IRQH_JSR, <dly_handleDelay, >dly_handleDelay)
        copperEntry(261, IRQH_JSR, <handleCredits, >handleCredits)
        copperLoop()

In this example we see that copper list can be modified in runtime. Here we modify the background colour at the begining of the fade in section. In another place (handleCredits) we modify this value with simple:

    sta fadeEffectColor + 2

Just each time you want to fade in or out, we have to change colours according to special fade in / fade out array of colours.

5.5.3. Supporting both PAL and NTSC

We all know that NTSC on C64 is hard, because there are far less raster lines per frame (due to the fact that VIC-II does 60Hz vs 50Hz in PAL). But what is even harder and more awkward: while PAL version has 0 raster line at the very top of the screen, the raster 0 on NTSC is near at the bottom.

For PAL we do have 312 raster lines, and v-blank starts at 300 (the very bottom of the visible screen area). for NTSC we have 263 raster lines, v-blank starts at line 13 (so raster lines 0 to 12 are visible at the very bottom of the screen). So, lets assume we would like to set up an interrupt at the line 251 (PAL). To have this interrupt to be triggered in exactly the same place on NTSC, we do need to set it up in line 1.

Therefore in order to make our copper list work equally well on PAL and NTSC we do have to:

  1. Detect which machine are we running on (PAL / NTSC).

  2. Modify copper list accordingly (change 251 to 1 if on NTSC).

Let’s consider following copper list:

ingameCopperList:
    copperEntry(DASHBOARD_Y + 20, IRQH_JSR, <upperMultiplex, >upperMultiplex) // 50 + 20 = 70
    copperEntry(77, IRQH_JSR, <playMusicIrq, >playMusicIrq)
  scrollCode:
    copperEntry(103, IRQH_JSR, <scrollBackground, >scrollBackground)
  switchPagesCode:
    copperEntry(280, IRQH_JSR, <switchPages, >switchPages)
    copperLoop()

We see that the last raster line is way beyond of the NTSC display capabilities. In fact, IRQ at 280 will never be triggered on NTSC machine. In short, the following raster table will not work there. We have to detect NTSC and modify the table accordingly.

startIngameCopper: {
  lda #<ingameCopperList
  sta z_displayListPtr
  lda #>ingameCopperList
  sta z_displayListPtr + 1

  lda z_ntsc
  bne ntsc
  jmp !+
  ntsc: {
    // for NTSC we want to change raster counter for last IRQ handler (switch pages)
    lda #8
    sta switchPagesCode + 1
    lda switchPagesCode
    and #%01111111
    sta switchPagesCode
  }
  !:
    jsr startCopper
  rts
}

Note that we have to change both byte 0 and 1 of the copper list entry - this is because we have to also clear bit 9 of the raster counter, which is stored in control byte.

To detect NTSC we can use the following code:

setNTSC: {
  lda #1
  sta z_ntsc
  rts
}

detectNTSC: {
  lda #0
  sta z_ntsc
  detectNtsc(0, setNTSC)
  rts
}

6. Contribute

6.1. Coding standards

6.2. Branching and versioning strategy

7. Library reference

This section is incomplete and will be rewritten soon.

8. Privacy disclaimer

8.1. How do we process your data

This website is powered by AsciiDoctor and is hosted on GitHub/GitHub Pages.

AsciiDoctor is not collecting or processing user data in any form.

GitHub however, as any other hosting company, can have full insight in visitor data, like connecting IP addresses, visited pages, etc. Note that Github is not known to actively profile visitors. By using a VPN you can (try to) prevent this.

8.2. Google Analytics

This website uses Google Analytics. This happens only if you accept third party cookies. I have configured Google Analytics so that IP addresses are anonymized.

Use the following option to grant or revoke collecting data and using cookies.

Enable Google Analytics