Commodore 64: restoration and 3D graphics in BASIC, C and assembly

Restored Commodore 64 connected to a laptop through a USB frame grabber

Disclaimer: the projects described here are the result of my own independent work, done without the use of AI. This article itself was written with the help of AI.

In June 2018 I got a Commodore 64 in its original box. The c64 repository is what came out of it: a restoration photo log and three implementations of the same goal, a rotating 3D wireframe model, in the three languages the machine can realistically be programmed in.

Restoration #

The computer was complete but dirty and yellowed. I took it apart, cleaned the case and every key separately, and put it back together.

Commodore 64 in its original box before restoration

Rear ports of the C64 case during cleaning

Keyboard with cleaned keycaps put back on the frame

There is no monitor with an RF input in my house any more. The most comfortable setup turned out to be the S-Video output connected to a cheap USB frame grabber, with VirtualDub showing the picture on a laptop:

Restored Commodore 64 connected to a laptop through a USB frame grabber

For day-to-day development I used the VICE emulator and moved to real hardware only to check the result.

One renderer, three languages #

The reference was generic, my JavaScript 3D engine from 2013. Its math (rotation matrices, camera, perspective division) is simple enough to port to almost anything. The question was how far a 1 MHz 6510 with 64 KB of RAM can take it.

BASIC 2.0 #

The first version is plain Commodore BASIC (main.bas): rotation matrices in DIM arrays and the perspective projection of each vertex. It does not draw yet. Instead it prints the projected coordinate and how many ticks the calculation took, plus FRE(1)+65536 to see how much memory is left. That was the point: to measure how expensive the math alone is before writing any graphics code.

Typing the BASIC program into VICE

Projected coordinates and timing per vertex printed by the BASIC version

A separate program, bitmap_mode.bas, enters bitmap mode with two POKEs to the VIC-II and fills the screen with colour, cycling through colours forever:

basic
5 base=2*4096
9 poke 53272,peek(53272)or8
10 poke 53265,peek(53265)or32
15 co = 3
20 for i = base to base+7999: poke i,0: next
30 for i = 1024 to 2023: poke i, co: next
35 co = co + 1
40 goto 20

The easiest way to run either listing is to paste it into VICE after NEW and type RUN.

C with cc65 #

cc65 is a C compiler, assembler and linker for the 6502 family. One command produces a program with a BASIC header, so it starts with a normal autostart in VICE:

Bash
cl65 -r -O main.c -t c64

-r enables register variables (cc65 allows up to 6 per function, kept in zero page). The model is a cube defined as vertices and faces, and the renderer redraws only when a key press changes the camera.

Wireframe cube rendered by the cc65 version in VICE

Replacing TGI

cc65 ships with TGI, a portable graphics library. It is convenient but big and slow, so I replaced it with direct VIC-II and CIA register access. This switches to bitmap mode and selects the VIC-II bank:

C
void video_init() {
  // DATA DIRECTION
  TMP1 = *(byte*) 0xDD02;
  TMP1 = TMP1 | 3;
  *(byte*) 0xDD02 = TMP1;
  // VIC-II BANK
  TMP1 = *(byte*) 0xDD00;
  TMP1 = TMP1 & 252;
  *(byte*) 0xDD00 = TMP1;
  // BITMAP LOCATION, RELATIVE TO VIC-II BANK
  *(byte*)0xD018 = 0x48;
  // BITMAP MODE (POKE53265,PEEK(53265)OR32)
  TMP1 = *(byte*) 0xD011;
  TMP1 = TMP1 | 32;
  *(byte*) 0xD011 = TMP1;
  video_clear();
}

The second optimisation was to stop clearing the whole bitmap each frame. The program keeps the previous frame's projected vertices and overdraws only those lines in the background colour.

Memory map

Knowing where things live is half the work on this machine. The layout I ended up with for a cc65 program:

Address What
$0400–$07FF text screen
$0800– program code (BASIC header + compiled C)
up to $CFFF free RAM; heap grows up, C stack grows down from $CFFF
$D000–$D3FF VIC-II registers
$D400–$D7FF SID
$DC00–$DDFF CIA 1 and CIA 2
$E000–$FFFF KERNAL ROM

Keyboard state is read from zero page at $00CB instead of going through the KERNAL.

Models from Blender

Typing vertex lists by hand gets old fast. A small Ruby script reads a Wavefront OBJ file exported from Blender and prints C arrays ready to paste into main.c:

Bash
cd models
ruby 0_parser.rb cube.obj

The number of vertices matters a lot: on the C64 the matrix math takes far longer than drawing the lines.

6502 assembly with DASM #

The assembly part is the beginning of the third port. It uses DASM macros wrapping KERNAL and BASIC ROM routines (print string, set cursor, clear screen, print number) and is started from BASIC with SYS 4096, because the code is placed at $1000:

Text
   processor 6502
   org    $1000
R_PRINT_STRING = $AB1E
R_SET_CURSOR   = $E50C
R_CLEAR_SCREEN = $E544
  mac puts
    lda #<{1}
    ldy #>{1}
    jsr R_PRINT_STRING
  endm

Output of the DASM assembly program in VICE

What I took from it #

  • The same algorithm in BASIC, C and assembly is the clearest lesson in "cost of abstraction" I know. cc65 code is much bigger than hand-written assembly, and interpreted BASIC is far slower than both.
  • On 8-bit hardware drawing is cheap and math is expensive. Integer-only arithmetic, precomputed values and redrawing only what changed matter more than anything else.
  • The 3D projection from this project went on to the x86 boot sector, the ESP8266 and later to OpenCL in Simple High Performance Computing.

Source: github.com/michalasobczak/c64. See also the overview of my hands-on projects.

View as Markdown

All the content on this website is licensed under Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0). Cite as: Michał Sobczak, “Commodore 64: restoration and 3D graphics in BASIC, C and assembly”, 25 October 2026, https://michalasobczak.pl/posts/commodore-64-restoration-and-3d-in-basic-c-and-assembly.html