---
title: "Commodore 64: restoration and 3D graphics in BASIC, C and assembly"
url: https://michalasobczak.pl/posts/commodore-64-restoration-and-3d-in-basic-c-and-assembly.html
author: "Michał Sobczak"
description: "I restored a boxed Commodore 64 and wrote the same 3D wireframe renderer for it three times: in BASIC 2.0, in C with cc65, and in 6502 assembly with DASM. Notes on the memory map, entering bitmap mode without TGI, and importing Blender models."
published: 2026-10-25T09:30:00.000Z
updated: 2026-10-25T09:30:00.000Z
language: en
categories: ["Hobby"]
tags: ["Vintage Computing", "3D", "Programming"]
license: CC BY-NC-ND 4.0 (https://creativecommons.org/licenses/by-nc-nd/4.0/)
---
# Commodore 64: restoration and 3D graphics in BASIC, C and assembly

> **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](https://github.com/michalasobczak/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](https://michalasobczak.pl/media/projects/c64-restoration-IMG_20180612_114706.jpg)

![Rear ports of the C64 case during cleaning](https://michalasobczak.pl/media/projects/c64-restoration-IMG_20180612_132114.jpg)

![Keyboard with cleaned keycaps put back on the frame](https://michalasobczak.pl/media/projects/c64-restoration-IMG_20180612_153355.jpg)

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](https://michalasobczak.pl/media/projects/c64-restoration-IMG_20180616_204750.jpg)

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](https://michalasobczak.pl/posts/generic-3d-engine-in-javascript-ported-to-esp8266.html), 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](https://michalasobczak.pl/media/projects/c64-basic-typing.png)

![Projected coordinates and timing per vertex printed by the BASIC version](https://michalasobczak.pl/media/projects/c64-basic-3d.png)

A separate program, `bitmap_mode.bas`, enters bitmap mode with two `POKE`s 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](https://michalasobczak.pl/media/projects/c64-cc65-3d.png)

#### 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](https://michalasobczak.pl/media/projects/c64-asm.png)

## 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](https://michalasobczak.pl/posts/x86-bootloader-2d-and-3d-graphics-in-nasm.html), the [ESP8266](https://michalasobczak.pl/posts/generic-3d-engine-in-javascript-ported-to-esp8266.html) and later to OpenCL in [Simple High Performance Computing](https://michalasobczak.pl/posts/simple-high-performance-computing.html).

Source: [github.com/michalasobczak/c64](https://github.com/michalasobczak/c64). See also the [overview of my hands-on projects](https://michalasobczak.pl/posts/hands-on-projects-on-github.html).
