C64jasm - a new take on C64 macro assembling

Introduction

For the past couple of months I’ve worked on the C64 VISIO 2018 invitation intro (YouTube capture at the bottom of this post). I upped the difficulty level a bit by first designing and implementing a new 6502 macro assembler called c64jasm, and then using that to develop my demo. While I haven’t had the time to polish the assembler to, say, KickAssembler level, my tool turned out to be a real joy to work with. This post highlights some of its features that I believe make it somewhat unique in its small niche.

These were my initial design choices for c64jasm:

  • Written in TypeScript & running on Node. Easy multi-platform environment with easy to install dependencies.
  • Ability to extend the assembler’s functionality by calling into user-defined JavaScript modules directly from the .asm source.
  • A sufficiently powerful pseudo language (macros, conditionals, for-loops, etc) but build on the JavaScript extension mechanism for more complex pseudo ops like LUT creation and file format parsing.
  • Node-style “watch” support: automatically recompile if any source files change. Design it for efficiency.
  • VSCode integration (for on-the-fly compilation and error reporting within the IDE, launch output binary in VICE.)

The choice of TypeScript was very deliberate. I’ve been meaning to learn the language for quite some time, and developing a macro assembler in it was a good way to learn & evaluate TypeScript. TypeScript turned out to be a great language: supports functional programming style well (and so well-suited for developing compilers) and has a simple but powerful static typing system.

The assembler extension mechanism is the main feature that differentiates c64jasm from traditional 6502 macro assemblers and I’ll spend the rest of this blog talking about this mechanism.

This blog post assumes that you’re at least somewhat familiar with macro assemblers and that you know a bit of JavaScript.

Why do I need to extend my assembler?

So why does one need to extend the assembler in the first place? I have my assembly, macros and other pseudo directives, isn’t this enough? Well, you need all of these too. But some things like loading graphics files, generating lookup tables, etc. can be better expressed in a real programming language. Quite commonly a demo project is built using a build script that processes LUTs, graphics, and other assets into hex listings to be included in the main assembly source.

Extending functionality

Traditionally, extending the functionality of an application or a tool means that you’d first need to learn the app’s extension API, figure out how to build your extension (say .DLLs or a Java .jar file), install it (and maybe debug your PATH or CLASSPATH when it fails to load), and once all done, the feature becomes available in your tool and you use it to develop your content.

The approach I took in c64jasm is that assembler extensions are just a bunch of source files in your project’s source directory. Extensions shouldn’t require a separate build or installation step and the overhead of creating a new one for any specific purpose should be minimal. Ideally the extensions should be runnable stand-alone without the assembler and that you should be able to debug and test them using standard debugging tools.

Before we get deeper into c64jasm assembler plugins, let’s review some of the c64jasm pseudo directives.

C64jasm pseudo ops

If you’ve used something like KickAssembler, ACME or 64tass, these should look pretty familiar.

Declaring pseudo variables: You can declare pseudo variables using !let. This is basically the same as KickAssembler’s .const/.var (many other assemblers commonly have this as EQU.)

; declaring variables
!let after_bounce_frames = 64
!let sprite_writer_path_tablen = 128

; assigning a new value to a previously declared variable
after_bounce_frames = 33

; using the variables in immediates
    sec
    sbc #sprite_writer_path_tablen
    cmp #after_bounce_frames
    bcs do_wrap
    jmp no_wrap
do_wrap:
    jmp wrap

Macros:

some_mem_loc: !byte 0

; declare a macro to write an immediate value to memory
!macro mov8imm(d, imm) {
    lda #imm
    sta d+0
}
; expand the macro
+mov8imm(some_mem_loc, #$80)

; this will emit the following code:
;
;    lda #$80
;    sta some_mem_loc

Symbols declared within a macro are local to that macro. You can also declare macros within macros.

If/elif/else:

!let double_y = 1
!if (double_y) {
    ldy #yy*6+1         ; code from this path
    sta (zpdst_even),y  ; will be emitted
    sta (zpdst_odd),y
    txa
    dey
    sta (zpdst_even),y
    sta (zpdst_odd),y
} else {
    ldy #yy*3+1
    sta (zpdst_even),y
    txa
    dey
    sta (zpdst_even),y
}

For-loops:

; emit 8 nops
!for i in range(0, 8) {
    nop
}

As opposed to a C-style “declare loop variable, check condition, increment” for loops, c64jasm’s for is similar to Python’s for-statement. You use a function called range() that returns a list of integers and the for-loop just iterates over the elements of this list, assigning the current list element to the bound loop variable i. E.g., these two are conceptually the same:

; emit four NOPs
!for i in range(4) {
    nop
}

!for i in [0,1,2,3] {
    nop
}

The “emit byte” pseudo !byte supports emitting a list of bytes when passed an integer list. The following two lines are equivalent:

!byte range(4)
!byte 0, 1, 2, 3

C64jasm extensions

So with the basic c64jasm pseudo ops explained, let’s continue to c64jasm extensions. A c64jasm extension is just a JavaScript .js module. To use such a .js module in your assembly source, you bind it to a name with !use "path/to/plugin.js". This name becomes a function that you can call in your assembly source file.

Let’s illustrate this with some code to generate a sine lookup table. First the plugin code:

# sintab.js
module.exports = ({}, len, scale) => {
    const res = Array(len).fill(0).map((v,i) => Math.sin(i/len * Math.PI * 2.0) * scale);
    return res;
}

Here’s how you’d use the above in your .asm source file:

; Bind sintab.js default export to a pseudo function 'mkSintab'
!use "./sintab" as mkSintab

!let sin1scale = 26
sintab1:
!for s in mkSintab(256, sin1scale) {
    !byte s
}

As !byte and !word accept a list as an argument, the above can be written simply as:

sintab1: !byte mkSintab(256, sin1scale)

The nice thing about this approach is that I can keep the assembler implementation simple. The assembler doesn’t need a large standard library of math and other utility functions as this functionality can be easily expressed as tiny JavaScript modules.

Sure, generating a sine table would be pretty easy without an extension too (and this is totally supported by c64jasm):

sintab1:
   !for s in range(0, 256) {
       !byte sin(s/256*2*PI)*scl
   }

Where I find the JavaScript extension mechanism really shines is importing assets (sprites from .spd, .sid, etc.). Here’s how I compile SpritePad .spd files into my demo binary:

# spd.js
module.exports = ({readFileSync, resolveRelative}, filename) => {
    const buf = readFileSync(resolveRelative(filename));
    const numSprites = buf.readUInt8(4)+1;
    const data = [];
    for (let i = 0; i < numSprites; i++) {
        const offs = i*64+9;
        const bytes = [];
        for (let j = 0; j < 64; j++) {
            bytes.push(buf.readUInt8(offs + j));
        }
        data.push(bytes);
    }
    return {
        numSprites,
        enableMask: (1<<numSprites)-1,
        bg: buf.readUInt8(6),
        multicol1: buf.readUInt8(7),
        multicol2: buf.readUInt8(8),
        data
    };
}

Using the above in an .asm file:

!use "./spd" as spd

; Load 'hirmu.spd' and store the returned JavaScript object in a
; variable called 'hirmu_sprite'
!let hirmu_sprite = spd("../sprites/hirmu.spd")

At this point, no code or bytes were yet emittted. The .spd file contents are now in a pseudo variable called hirmu_sprite. This is just a JavaScript object with fields like numSprites, enableMask, data that can be accessed from .asm with the dot operator (e.g., sprite_data.numSprites.)

With this, I can expand the sprite data into the output binary. Or I can use to emit code to setup sprite registers (enable bits, individual and multi color values, etc).

Emitting actual sprite data for all the sprites contained within the .spd file:

hirmu_sprite_data:
    ; loop over all the sprites in hirmu_sprite.data
    !for sdata in hirmu_sprite.data {
        !byte sdata  ; expand 64 bytes of sprite data (one sprite)
    }

    ; or equivalently:
    ; !for sidx in range(hirmu_sprite.numSprites) {
    ;    !byte hirmu_sprite.data[sidx]  ; expand 64 bytes of sprite data (one sprite)
    ; }

I also define a macro that expands machine code to write sprite registers based on the .spd file contents:

!macro set_sprite(s, ptr, xScale, yScale) {
    !if (xScale == 2) {
        lda #s.enableMask
    } else {
        lda #0
    }
    sta $d01d  ; XSCALE register

    !if (yScale == 2) {
        lda #s.enableMask
    } else {
        lda #0
    }
    sta $d017  ; YSCALE register

    ; set sprite pointers and individual sprite colors for
    ; all sprites
    !for sprite_idx in range(0, s.numSprites) {
        ; load the individual color from sprite data
        lda #s.data[sprite_idx][63]
        ; indiv sprite color
        sta $d027 + sprite_idx
        lda #ptr/64 + sprite_idx
        sta $7f8 + sprite_idx
    }

    ; enable multicolor for the same sprites, set multicolor values
    lda #s.multicol1
    sta $d025
    lda #s.multicol2
    sta $d026

    ; enable all the sprites that were loaded from the .spd
    lda #s.enableMask
    sta $d01c       ; multicolor enable
    sta $d015       ; sprite enable
}

set_hirmu_sprite: {
    +set_sprite(hirmu_sprite, hirmu_sprite_data, 2, 2)
    rts
}

I use the same pattern for all the different asset types used in my demo. Here’s how I pull the SID tune into the binary:

# sid.js
function readWord(buf, offs) {
    return buf.readUInt8(offs) + (buf.readUInt8(offs+1) << 8);
}

// Read a big-endian word
function readWordBE(buf, offs) {
    return (buf.readUInt8(offs)<<8) + buf.readUInt8(offs+1);
}

module.exports = ({readFileSync, resolveRelative}, filename) => {
    const buf = readFileSync(resolveRelative(filename));
    const version = readWordBE(buf, 4);
    const dataOffset = readWordBE(buf, 6);
    const startAddress = readWord(buf, dataOffset);
    const init = readWordBE(buf, 0x0a);
    const play = readWordBE(buf, 0x0c);
    const numSongs = readWord(buf, 0x0e);
    const res = {
        startAddress,
        data: [...buf.slice(dataOffset+2)],
        init: startAddress,
        play: startAddress + 3
    }
    return res;
}

Using the loaded SID tune from the .asm source:

!use "./sid" as sid
!let music = sid("sid/tune.sid")

demo_start: {
    lda #0
    jsr music.init
    ...
    jsr music.play      ; usually called from an IRQ
}

* = music.startAddress  ; usually music.startAddress == $1000
music:
!byte music.data

All of these content plugins are just normal .js files that can be run from the command line with node. This enables you to use standard tools like VSCode to debug them as stand-alone JavaScript modules. I prefer this code to be part of the project source tree rather than buried somewhere within c64jasm’s standard libraries.

Epilogue

OK, that’s about it for now on c64jasm plugins. I will probably write another blog later about using c64jasm in VSCode (with quick re-compile-on-save and launch .prg in VICE.) I tweeted about it a while ago, you can check out the video if you’re curious about this.

Oh by the way, c64jasm is available on GitHub and is already quite usable. However, it’s still very much work-in-progress. If you’re into this type of stuff and feeling adventurous, feel free to give it a try. Otherwise, stick with the more established C64 assemblers. :)

VISIO 2018 invitation demo capture