| Commodore VIC-20 | Commodore 64 |
|---|---|
![]() |
![]() |
![]() |
![]() |
-
Wire-frame 3D rendering
Eight-vertex, twelve-edge unit cube. Yaw + pitch rotation, world-space three-axis translation, perspective projection with per-vertex pre-computed 1/depth lookup, and post-projection zoom. -
Pseudo-pixel grid
Synthesizes a low-resolution pseudo-pixel canvas from the standard character matrix using quadrant-block PETSCII graphics. The eight quadrant-block PETSCII characters and their reverse-video forms cover all sixteen possible 2 x 2 quadrant masks per character cell. This gives an effective pseudo-pixel resolution of 44 x 46 on the VIC-20's 22 x 23 character grid. And an effective pseudo-pixel resolution of 80 x 50 on the C64's 40 x 25 character grid. -
Double-buffered output
Every frame is composed in a 506-byte (VIC-20) or 1000-byte (C64) back buffer and copied to screen RAM during vertical retrace to prevent screen tear. -
Auto-rotate or keyboard-control
SPACEtoggles between continuous auto-rotation (default) and interactive keyboard-control mode. Both modes share state, so the cube continues from wherever it was whenSPACEwas pressed. -
Four-state UI
Home, Help, Main Loop (animation), and Main Loop + Help (animation paused with help overlaid). F1 / F2 / F3 navigate between states. -
Tunable aspect correction
Per-axis Q1.7 aspect factors compensate for the host's pixel-aspect ratio. Single-line.constedits at the top of the source file change the cube's rendered proportions without modifying the projection code. -
Fixed-point math
Signed 8-bit by 8-bit multiply (multiply_signed_8), assembly-time-generated sine and inverse-depth tables, and per-row screen-address tables that replace runtime multiplies.
The original VIC-20 version was hand assembled and developed in the late 1980s using the Code Probe software monitor. Then ported to the C64 in the early 1990s. In 2026, as an experiment to see if it could be done, I used Claude Code to disassemble the original PRG files and generate nice pretty Kick Assembler listings for both versions.
Given the convenience of working with an assembly listing, I took the liberty of fixing a few bugs and improving one or two things, like taking advantage of Kick Assemblers script language to generate lookup tables for example. The original look up tables were generated with BASIC programs, and saved to SEQ files, that I then loaded into RAM using Code Probe.
I haven't exploreed the full potential of Kick Assembler. So, I look forward to seeing what others do to improve these programs.
| Machine | Source | Load address | Entry | Pseudo-pixel Resolution |
|---|---|---|---|---|
| VIC-20 | src/cube-vic20.asm |
$1001 |
SYS 4110 |
44 x 46 |
| C64 | src/cube-c64.asm |
$0801 |
SYS 2062 |
80 x 50 |
The two versions share the same:
- 3D pipeline
- Fixed-point math
- Sine and inverse-depth tables
- Bresenham line drawing
- Table-driven motion-key dispatch
They differ in:
- Screen-RAM and colour-RAM addresses
- VIC chip register layout
- BASIC stub address
- Aspect-correction stage (one Q1.7 X-only factor on the VIC-20, two independent Q1.7 factors on the C64)
- Home / help screen layouts (sized for 22 columns on the VIC-20, 40 columns on the C64).
3D Cube is released on two media images: a tape image carrying the VIC-20 version of the 3D cube demo, and a disk image carrying both the VIC-20 and the C64 versions of the 3D cube demo.
| File | Machine | Description |
|---|---|---|
CUBE-VIC20 |
VIC-20 | VIC-20 version of the 3D cube demo. |
| File | Machine | Description |
|---|---|---|
CUBE-VIC20 |
VIC-20 | VIC-20 version of the 3D cube demo. |
CUBE-C64 |
C64 | C64 version of the 3D cube demo. |
CLS |
C64 | Utility program to clear the screen and set the colors to green text on black background. |
| Media | Commodore VIC-20 | Commodore 64 |
|---|---|---|
| Tape | LOAD "CUBE-VIC20", 1, 1RUN or SYS 4110 |
N/A |
| Disk | LOAD "CUBE-VIC20", 8, 1RUN or SYS 4110 |
LOAD "CUBE-C64", 8, 1RUN or SYS 2062 |
The Commodore LOAD command takes three positional parameters — LOAD "filename", device, secondary — and each one matters here:
| Parameter | Meaning |
|---|---|
filename |
Program name on tape or disk. |
device |
The peripheral's primary address on the IEC serial bus. 1 selects the datasette (tape). 8 selects the first disk drive (additional drives, if present, are 9, 10, 11). |
secondary |
The secondary address — a sub-command sent to the device that selects how the load is performed. 0 ignores the PRG file header's load address and relocates the program to the start of BASIC's program area. 1 honours the load address stored in the PRG file header and places the program at exactly that address in memory. |
The cube demo's PRG file is laid out as a tokenised BASIC stub immediately followed by the machine-language body, and the PRG file header's load address is set to the host's standard BASIC start — $1001 on the VIC-20, and $0801 on the C64.
After the load completes, RUN invokes the BASIC stub, which auto-SYSes to the machine-language entry at $100E (SYS 4110) on the VIC-20 or $080E (SYS 2062) on the C64. The explicit SYS form jumps to the entry directly without going through BASIC.
Note:
Although the programs are loaded at $1001 for the VIC-20, and $0801 for the C64, the SYS command needs to target the first machine language instruction after the BASIC stub, at location $100E for the VIC-20, and $080E for the C64.
To launch xvic with build/cube-vic20.prg autostarted:
xvic -autostart build/cube-vic20.prgTo launch x64sc with build/cube-c64.prg autostarted:
x64sc -autostart build/cube-c64.prgThe VICE binary must be on PATH, or substitute the full path to your VICE install (e.g. C:\Programs\GTK3VICE-3.10-win64\bin\xvic.exe / x64sc.exe on Windows, /usr/bin/xvic / /usr/bin/x64sc on most Linux distributions).
The supplied run-cube-vic20.bat and run-cube-c64.bat (Windows) wrap the same launches with sensible default install paths.
- The screen border and background are set to black via the host's VIC chip registers.
- The KERNAL text colour is set to white.
- The screen is cleared via
CHROUT $93. - The screen-code-to-mask reverse-lookup table is initialised.
- The 3D state is initialised:
yaw_angle = 0,pitch_angle = 0,control_mode = 0(auto-rotate), translations zeroed,zoom_factor = $40(unity). - The Home screen is rendered.
- The main loop begins in
STATE_HOME, awaiting F1 (help) or F2 (start).
3D CUBE
VERSION 1.0
F1 HELP
F2 START
BY ROHIN GOSLING
The function keys F1, F2, and F3 are active in every state and navigate the four-state UI. The motion keys are active only in the main loop state, STATE_MAIN, with control_mode = 1 (keyboard-control mode); SPACE toggles control_mode.
| Key | Effect |
|---|---|
F1 |
Toggle help — Home <-> Help, or pause animation with help overlay in Main. |
F2 |
Start the animation from the Home screen. |
F3 |
Return to the Home screen from any other state. |
SPACE |
Toggle auto-rotate / keyboard-control mode (Main only). |
+ / - |
Zoom in / out. |
W / R |
Yaw left / right. |
E / D |
Pitch up / down. |
S / F |
Translate X left / right. |
T / G |
Translate Y up / down. |
Q / A |
Translate Z closer / further. |
In auto-rotate mode the motion keys are inert; only SPACE and the function keys produce visible effects. In keyboard-control mode the auto-rotation increments are suspended and motion keys take effect on every keypress. Holding a key produces typematic repeats at the host's normal repeat cadence, which gives smooth continuous motion without any per-frame timing logic.
3D Cube is a pair of single-file assembly projects built with Kick Assembler. Java is required.
Assemble the VIC-20 build:
java -jar KickAss.jar src/cube-vic20.asm -odir buildAssemble the C64 build:
java -jar KickAss.jar src/cube-c64.asm -odir buildor, on Windows, run the supplied drivers:
build-cube-vic20.bat
build-cube-c64.bat
The builds produce build/cube-vic20.prg (loads at $1001) and build/cube-c64.prg (loads at $0801). Each PRG runs on the corresponding physical Commodore machine and on its VICE emulator (xvic or x64sc).
Hardware required:
- A Commodore VIC-20 (unexpanded, PAL or NTSC) for the VIC-20 build, or a Commodore 64 (PAL or NTSC) for the C64 build.
- A means of transferring the assembled PRG from the build host to the target machine — for example a 1541 / 1541-II disk drive with a PRG-to-D64 toolchain, an SD-card drive emulator (SD2IEC, Pi1541, Ultimate II), or a serial cable to a real disk drive.
With the PRG on disk, load with LOAD "CUBE", <device>, 1 and start with RUN, as shown in the Loading and Starting section above.
xvic -autostart build/cube-vic20.prg
x64sc -autostart build/cube-c64.prgRun from the project root so the relative path to build/ resolves. The corresponding VICE binary must be on PATH, or substitute the full path to your VICE install.
The supplied run-cube-vic20.bat and run-cube-c64.bat (Windows) wrap the same launches with sensible default install paths.
3D Cube renders an eight-vertex, twelve-edge wire-frame cube in real time on a 1MHz 6502/6510. Every frame projects all eight vertices through the same five-stage pipeline; yaw rotation, pitch rotation, world-space translation, perspective projection with aspect correction and zoom, and screen-space mapping, then rasterises the twelve edges with a Bresenham line drawer. The 6502 has no hardware multiplier, no divider, and no floating-point unit, so every numerical step is implemented in fixed-point arithmetic with assembly-time-generated lookup tables for the transcendentals.
Throughout this section:
-
$\theta$ denotes the yaw angle,$\varphi$ denotes the pitch angle. - Angles are encoded as 8-bit unsigned integers
$a \in \{0, 1, \ldots, 255\}$ with one full revolution = 256 steps, so$\theta_{\text{rad}} = 2\pi a / 256$ . -
$\mathbf{v} = (x, y, z)^{T}$ denotes a vertex in object space;$\mathbf{v}'$ denotes the same vertex after the current pipeline stage. -
$Qm.n$ denotes a fixed-point format with$m$ integer bits and$n$ fraction bits; for example$Q2.6$ stores values in$[-2.0, +2.0)$ at a resolution of$2^{-6} = 1/64$ . -
$\gg$ denotes an arithmetic right shift;$\ll$ denotes a left shift. -
$\lfloor \cdot \rfloor$ denotes integer floor;$\bmod$ denotes truncated integer modulus.
The pipeline uses three Q-formats. All are packed into a single 8-bit byte; the multiplier produces a 16-bit intermediate that is shifted back to 8-bit before storage.
| Format | Sign | Range | Resolution | Used for |
|---|---|---|---|---|
| Q2.6 | signed | vertex coordinates, translation offsets, rotation state | ||
| Q1.6 | signed | sine table, zoom factor | ||
| Q1.7 | unsigned | aspect factors, inverse-depth scale |
The numerical interpretation of an 8-bit byte
with two's-complement decoding for the signed formats. Cube vertices are stored as
The product of two Q-formats has fraction bits equal to the sum of the operand fraction bits. The two product types in the pipeline are:
To return to asl multiply_result_lo / rol multiply_result_hi) followed by reading the high byte: an arithmetic right shift through the carry flag. So
The 6502 has no hardware multiplier. The cube relies on multiply_signed_8, which computes a signed 16-bit product
where
- Record
$\mathrm{sgn}(p) = \mathrm{sgn}(a) \oplus \mathrm{sgn}(b)$ as the XOR of bit 7 of each operand. - Replace
$a$ and$b$ with their absolute values viaEOR #$FF / CLC / ADC #$01(two's-complement negation). - Iterate eight times: shift the multiplier right one bit (consume bit 0 into the carry flag), conditionally add the multiplicand to the result high byte on carry, then rotate the 16-bit accumulator right by one. After eight iterations the eight partial products have been summed and the accumulator holds
$|a| \cdot |b|$ . - If the recorded sign is negative, two's-complement-negate the 16-bit accumulator.
The C64 version adds a second variant, multiply_signed_unsigned_8, used by the aspect-correction stage. The multiplier is consumed as an unsigned
The C64 needs this variant because the default
The 3D cube demo uses a 256-entry sine table for trigonometry, indexed by an 8-bit unsigned angle. The sine table used in the original 1989 version of the 3D cube demo was generated with a BASIC program, and the values poked into RAM where they could be saved to tape with the rest of the cube demo's machine language code and data while it was being developed. For the Kick Assembler reconstruction, we use Kick Assembler's script language to generate the sine table at assembly time.
sin_table:
.fill 256, round ( 64 * sin( i * 2 * PI / 256 ) )
Equivalently:
The factor of
A separate cosine table is unnecessary because of the standard identity:
In the 256-step encoding, $40 hex). So:
The modulo-256 reduction is free on a 6502 because adding two 8-bit bytes naturally wraps mod 256. The yaw and pitch rotation routines perform this with clc / adc #$40 on the angle byte, then re-index the sine table for the cosine value.
Yaw rotation (around the Y axis):
Pitch rotation (around the X axis):
The composed rotation applied per frame is yaw-then-pitch:
In assembly, neither matrix is materialised. The pipeline applies
with results written into rotated_vertices. It then applies
Each of the four multiplies per axis is a multiply_signed_8 call on
A subtle gotcha in the pitch stage: because pitch_y / pitch_z before any write to rotated_vertices, otherwise the second pair of multiplies would read the already-overwritten cube_vertices (a separate buffer) and writes into rotated_vertices.
When the yaw and pitch angle increments differ, the cube traces a Lissajous figure on the unit sphere whose period equals the LCM of the two angle increments. This is the source of the continuous tumbling motion in auto-rotate mode.
A simple component-wise add of the world-space offset
All three offsets are clc / adc loop with no multiplies.
The cube uses a classical pinhole projection with a fixed viewer placed on the
where
| Constant | VIC-20 | C64 |
|---|---|---|
VIEWER_DISTANCE) |
||
PROJECTION_FOCAL) |
With cube corners at inv_depth_focal[d] directly indexable without offset arithmetic.
The 6502 has no divide. The ratio
stored as an unsigned
In assembly: multiply_signed_8 on the signed
The pseudo-pixel canvas is built from
where
| Target |
ASPECT_FACTOR_X) |
ASPECT_FACTOR_Y) |
Multiply primitive |
|---|---|---|---|
| VIC-20 | implicit unity (no Y stage) | multiply_signed_8 |
|
| C64 | multiply_signed_unsigned_8 |
The VIC-20 squashes X by
The C64's stage uses multiply_signed_unsigned_8 because
After aspect correction, both axes are scaled by a shared
where the default zoom_factor is $40 (i.e.
Final mapping to integer pixel coordinates centres the X axis and flips the Y axis (mathematical
where
The Y flip uses the two's-complement identity
The 6502 implements this in two instructions: eor #$FF followed by clc / adc #(SCREEN_CENTER_Y + 1). No subtraction primitive is needed.
Each of the twelve edges draw_line, an integer Bresenham algorithm with a major-axis split. Define:
There are two cases:
X-major (
Y-major (
The error accumulator is a signed 8-bit byte. For the line_dx2 and line_dy2 respectively, eliminating the doubling work from the inner loop.
Each
The cell at byte offset
and stored back as a screen code via the bidirectional lookup pair mask_to_screen_code / screen_code_to_mask. The row stride
The 6502 has no multiply, so
generated at assembly time as .fill SCREEN_ROWS, < / > ( back_buffer + i * SCREEN_COLUMNS ). The low-byte and high-byte halves of each row's start address are stored in two parallel tables (row_start_lo, row_start_hi) so a single lda pair can resolve the 16-bit row pointer. This is the dominant trick that makes per-pixel plotting fast enough to redraw twelve lines plus a clear_pixel_screen per frame within the host's vblank-to-vblank budget.
The 3D pipeline is numerically identical on both targets, with five points of divergence:
| Axis | VIC-20 | C64 |
|---|---|---|
| Viewer distance |
||
| Aspect correction | One |
Independent X and Y factors, |
| Aspect multiply primitive |
multiply_signed_8 (factor |
multiply_signed_unsigned_8 (factor |
| Pseudo-pixel canvas |
|
|
| Row stride |
All other math — fixed-point formats, signed multiply core, sine table, inverse-depth table, rotation matrices, perspective projection ratio, zoom, Y flip, Bresenham line drawer, quadrant-mask plotting — is bit-for-bit identical between the two sources.
3D Cube is built with several, community-maintained tools. The author thanks their maintainers — without them a Kick Assembler reconstruction of a 1989 / 1990 hand-poked machine-language program would not have been practical.
| Tool | Author / Maintainer | Role in this project |
|---|---|---|
| Kick Assembler | Mads Nielsen | 6502 cross-assembler. Builds cube-vic20.prg and cube-c64.prg from the two src/*.asm sources. |
| VICE | The VICE Team | Commodore emulator suite. xvic and x64sc are the development and testing platforms. |
| cbmconvert | Marko Mäkelä | Conversion between Commodore archive formats. Builds the dist/ disk and tape release artefacts. |
| Claude Code | Anthropic | AI coding assistant. Constructed the Kick Assembler listings from the original 1989/1990 PRG binaries. |
3D Cube is distributed under the MIT License — a permissive, free-software licence that allows use, modification, and redistribution (including commercial use), provided the copyright notice and licence text are preserved.



