Skip to content
q4z1Public

About

Commodore Plus4 programs in C

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

189 Commits

Folders and files

Repository files navigation

Plus/4 Playground

A workbench for writing C — with a little 6502 assembly where it matters — for the Commodore Plus/4, built with cc65 and run in VICE.

This is a playground, not a library: each subfolder is a self-contained program, from a three-line "hello world" to a full arcade game. The point is to find out how far plain C gets you on a 1984 machine with 64 KB, no sprites, and a 1.76 MHz 7501.

How this was made

Ideas, design and direction come from decades of programming experience; the implementation made intensive use of AI (Claude by Anthropic). That is part of the experiment: how far a 1984 machine can be pushed when an experienced programmer puts an AI to work on it.

What's in here

Folder What it is
demonattack/ A conversion of the Atari 2600 Demon Attack that behaves like the original frame for frame, checked automatically against it. A colour on every line through a raster interrupt, collisions worked out pixel by pixel as the 2600's video chip sees them, and a page at the end of each game for hall of fame screenshots. Also as a 32 KB cartridge that runs on the C16.
main/ The starting point — clrscr(), printf(), cgetc(). Useful as a template and as a sanity check that the toolchain is wired up correctly.
paradroid/ Paradroid (Andrew Braybrook, 1985) after the C64 original, with its decks, droids, waypoints and lifts taken out of its memory: an eight-way scrolling deck under a fixed status panel, shooting, the transfer game, lifts with the side view of the ship, the deck plan at consoles, the original's briefing. One .prg: nothing loads after the start.
pacman/ A complete Pac-Man: full-screen 40×24 maze, four ghosts with distinct AI, power pills, levels, lives, TED sound. ~1500 lines of C plus two assembly routines.
phoenix/ A rebuild of the Atari 2600 Phoenix: all five waves, the force field, the mothership, two-voice sound with the arcade melodies, and a starfield that scrolls pixel by pixel.
stardew/ Stardew Pond, a small farming game modelled on Stardew Valley by ConcernedApe: a farm, a village with a store and a smith, three villagers, a mine with thirty floors. Runs from a .d64 and loads its rooms and tile sets as it goes; figures walk see-through over any background; the game is saved to the disk.
pokerth/ Work in progress: a PokerTH client. TLS, protobuf and authentication are handled by a proxy on a PC, which hands the Plus/4 a protocol small enough to parse in 6502. Logging in and watching a lobby works; the Plus/4 end is next.

The games

Phoenix Pac-Man
Phoenix — all five waves of the 2600 game, the force field, the mothership, a starfield scrolling a pixel at a time. One 2600 pixel is two Plus/4 pixels, so 160×192 lands exactly on 40×24 cells. The hardware fine scroll was tried first and had to go; the README says why. Pac-Man — the whole maze on one screen, four ghosts with their own ways of hunting, pixel-by-pixel movement.
Demon Attack Stardew Pond
Demon Attack — frame for frame the 2600 game, compared automatically with the original running in an emulator. What the Phoenix clone could leave out, this one cannot: a colour on every line, pixel-exact collisions, the original's exact timing. The README is about how. Stardew Pond — after Stardew Valley by ConcernedApe: farming, a village and a mine, one screen per room, each room a file on the disk that is loaded when the farmer walks in. Figures with a mask over whatever is behind them, a toolbar with a character set of its own, and a save file.
Paradroid Paradroid: transfer
Paradroid — the C64 classic, deck by deck from the original's data. The TED cannot change its vertical scroll under a status panel the way the C64 does; setting back two of its internal counters in the middle of the picture does it instead. The droids switch the cells they cover to multicolour over the hires deck. The transfer game: twelve wires a side, pulses, a column of lights to win.

The Plus/4 has no sprites at all, so all the games build their figures out of characters that are rewritten as the figures move — and each does it its own way, which is the interesting part. Pac-Man works the cells out on every frame. Phoenix cannot afford that with a dozen figures on screen, so it works out all 32 ways a figure can sit inside its cells when a wave starts and then only copies finished blocks. That is the difference between 4000 cycles per figure and a few hundred; a frame has 17784. Demon Attack keeps a pool of characters that are handed out afresh for every picture and taken back before the next, draws into a second, hidden screen, and changes two colour registers on every line while the picture is shown.

Each folder has a README of its own with the details.

Toolchain

Piece Role
cc65 C compiler, assembler and linker for 6502 targets (cl65 -t plus4)
VICE (xplus4) Plus/4 emulator, also used for source-level debugging
VS64 VS Code extension that drives cc65 and VICE and provides breakpoints
plus4emu Another Plus/4 emulator to check against, see below

Both cc65 and VICE are expected under ~/.local/share/cc65-vs64/bin. On this machine VS Code runs as a Flatpak while cc65 and VICE are host packages, so that directory holds small shim scripts that reach the host tools (flatpak-spawn --host, CC65_HOME, …) and fall back to the plain binaries when there is no sandbox. VS64 also expects the classic cc65 layout (<dir>/bin, <dir>/include), which the shim directory provides. If you clone this elsewhere, point vs64.cc65InstallDir / vs64.viceExecutable in the .vscode/settings.json files at your own installation, or just call cl65 directly (see below).

Building and running

There are two ways to work, and which one you get depends on which folder you open in VS Code.

1. Open the repository root — build whatever file is in the editor

F5 (or the Plus/4: Build & Run (aktive Datei) task) builds the .c file that is currently active and starts it in VICE. Ctrl+Shift+B builds it without launching. No breakpoints, but you can hop between programs freely.

This runs .vscode/run-current.sh, which you can also call from a shell:

.vscode/run-current.sh pacman/pacman.c

2. Open a single program folder — full debugger

Open main/ or pacman/ directly and F5 uses VS64's own VICE debugger with source-level breakpoints. It always loads the program named in that folder's project-config.json, which is exactly the one program in there.

The split exists because VS64 supports one project-config.json per workspace: its debugger always resolves the first workspace folder and the program field in launch.json is ignored. One folder per program is the way around that.

3. No editor at all

BIN=~/.local/share/cc65-vs64/bin
mkdir -p pacman/build
$BIN/cl65 -t plus4 -O -g -c -o pacman/build/pacman.o pacman/pacman.c
$BIN/cl65 -t plus4    -o pacman/build/pacman.prg pacman/build/pacman.o
$BIN/xplus4 -autostartprgmode 1 pacman/build/pacman.prg

Two steps on purpose: cl65 otherwise drops the object file next to the source.

Other emulators

VICE is not the last word on the TED, so a program can be checked in plus4emu too (a PAL Plus/4 with 64 KB). There is no package for it; the prebuilt Linux release goes to ~/.cache/plus4emu:

mkdir -p ~/.cache/plus4emu && cd ~/.cache/plus4emu
curl -LO https://github.com/istvan-v/plus4emu/releases/download/1.2.11-beta_20190320/plus4emu-1.2.11-beta_20190320-x86_64.tar.xz
tar xf plus4emu-*.tar.xz
mkdir -p ~/.plus4emu/roms && cp plus4emu-*/roms/*.rom ~/.plus4emu/roms/
plus4emu-*/p4makecfg ~/.plus4emu

Then, for any .prg:

~/.cache/plus4emu/plus4emu-1.2.11-beta_20190320/plus4emu \
    -cfg ~/.plus4emu/config/P4_64k_PAL.cfg paradroid/build/paradroid.prg

or, for Paradroid, paradroid/run-plus4emu.sh, which also works from VS Code's Flatpak. The joystick is the keypad (8/2/4/6, 0 fires) or a gamepad.

Adding a program

  1. mkdir foo
  2. Copy main/.vscode/ into it — those three files are deliberately program-agnostic (they use ${workspaceFolderBasename}), so they work as a template unchanged.
  3. Copy project-config.json and set name and main to the new name.
  4. Write foo/foo.c.

If a program needs more than an emulator - the PokerTH client needs a proxy running beside it - put a run.sh in its folder. F5 builds as usual, notices it, and hands the finished .prg over to it instead of starting VICE.

If it needs its own compiler switches, put them in a cflags file next to the source. Phoenix uses that for -Cl: cc65 otherwise keeps local variables on its software stack, which costs a sixth of the running time in a game that is busy every frame.

Convention: the folder, the source file and the output all share one name. foo/foo.c builds to foo/build/foo.prg. Build output never goes into git.

Plus4/
├── .vscode/            F5 = build & run the active .c file
├── main/
│   ├── main.c
│   ├── project-config.json
│   ├── .vscode/        F5 = VS64 build & run with debugger
│   └── build/          main.prg (ignored)
└── pacman/
    └── …same shape

Things this machine taught us

Collected here because every one of them compiles cleanly and only shows up in the emulator.

  • The ROM is banked out. cc65 hides it to use RAM up to $FD00, so copying the character generator from $D000 yields garbage. Page it in via $FF3E / $FF3F with interrupts off — and touch no C stack in that window (it lives at $F500–$FCFF, underneath the ROM): globals only, no locals, no calls. Check with cc65 -S that no sp appears between the switches.

  • Character literals are translated to PETSCII. 'A' is 193, not 65; 'a' is 65. So case 'w' matches unshifted W and case 'W' the shifted one — handle both. When converting to screen codes, compute relative to 'A'/'a' instead of subtracting 64/96, or everything lands 128 too high and the TED renders all text inverted, which looks deliberate and is easy to miss.

  • VS64 2.6.2 always links c64.lib, even for -t plus4 — it is hardcoded and the libraries field in project-config.json is read but never used. The build succeeds and the binary is wrong (conio writes to $0400 instead of $0C00). The ld65 shim rewrites it to <target>.lib; verify with a map file (ld65 -m map.txt) that the modules come from plus4.lib.

  • Colors are brightness * 16 + hue. High brightness washes out to white on the Plus/4; the strong colors live at brightness 3–6.

  • Avoid % and * in per-frame code. cc65 calls a full division routine for a modulo; a single conditional subtraction or a small lookup table is far cheaper.

  • Comparing a byte against a plain constant costs a 16-bit compare. if (zeile >= ZEILEN) with ZEILEN an int constant makes cc65 widen both sides and push them through its software stack. Casting the constant to unsigned char turns four such comparisons in the drawing routine back into byte compares — in Phoenix that alone was worth more than the assembly around it.

  • -Cl is worth a sixth of the running time in a program that is busy every frame: cc65 otherwise keeps every local on its software stack, and each access is an indexed load through a zero-page pointer. There is no recursion in a game loop, so static locals cost nothing.

  • Either everything on screen is hardware-scrolled, or nothing is. The TED's fine scroll moves the whole picture in the instant the register is written, but a figure built out of characters only follows on its next redraw — and a redraw takes longer than a frame. Every step leaves the figures a pixel behind, and when the register wraps they all jump seven pixels and crawl back one at a time. It cannot be synchronised; it has to be avoided. Moving a sparse background by hand is cheaper than compensating for a scroll anyway, because the compensation touches everything on screen.

  • An absolute array is cheaper than a pointer, and a loop iteration is expensive. Keeping ready-made bytes in a table can be slower than recomputing them, because cc65 reads TAB[x] in one instruction and p[x] in several; and eight rounds of two bytes beat sixteen rounds of one. Both were measured, both are the opposite of what the source suggests.

  • Measure with an autopilot and an immortality switch, and hold the load still. A game left alone dies in seconds, and then the numbers describe BASIC sitting at its prompt rather than the game. An early Phoenix measurement said 0.8 passes per second for exactly that reason; the truth was twenty times better.

  • Music must not hang off the game loop. A pass is however long the drawing takes, so a tune advanced once per pass changes tempo with the number of objects on screen — obvious the moment somebody with an ear for it listens. Sound belongs on an interrupt: save the vector at $FFFE (cc65 leaves the ROM banked out and has its own handler there), chain back to it, and save cc65's zero page around the call because the interrupted C code is in the middle of using it. About five per cent of the drawing, and the tempo is then exact.

  • Hang on to the interrupt the machine already runs — do not acknowledge it. Setting up a raster interrupt of one's own and clearing $FF09 before handing on looks right and is not: the KERNAL hangs off the same interrupt, finds the cause already cleared, decides there is nothing to do and stops counting its clock. Everything timed off that clock then runs at a fraction of its speed while the music sounds perfect — which is a confusing thing to debug, because the symptom is nowhere near the cause.

  • The joystick hangs on the keyboard's lines, and $FF08 has to be read twice. The row goes to $FD30, and writing a value with bit 2 low to $FF08 puts joystick 1 on those lines instead. The write leaves its own value on the data bus and the TED samples a cycle later, so a read in the next instruction hands back what was just written — which looks exactly like a closed contact. It costs an evening if the test program happens to have a few instructions in between and therefore works. Both games read it the same way; phoenix/README.md has the details.

  • VICE looks for joysticks once, at startup, and only in /dev/input/by-id. A wireless pad that is asleep when VICE starts is not picked up, and a Bluetooth pad has no by-id entry at all unless a udev rule makes one — which is why a perfectly working gamepad can look like a broken game.

Testing without looking at the screen

VICE can be driven remotely, which makes Plus/4 programs verifiable in an automated way:

# Screenshot after a fixed number of cycles (boot + autostart alone cost ~28M)
xplus4 -default -warp -autostart-warp -autostartprgmode 1 \
       -limitcycles 45000000 -exitscreenshot "$HOME/shot.png" \
       -autostart "$HOME/path/to/program.prg"

# Or the binary monitor, which is far more powerful
xplus4 -remotemonitor -remotemonitoraddress ip4://127.0.0.1:6510 …

Over the monitor socket, m 0c00 0fe7 dumps screen memory — reconstructing the display as text is more precise than diffing screenshots — and writing to $0527 (keyboard buffer) plus $00EF (count) injects keystrokes, so a game can be played remotely. x resumes emulation. Note that -keybuf does not work together with -autostart: autostart consumes the buffer itself.

Controls

Each program lists its own; see the README in its folder.

Demon Attack joystick in port 1 or 2, or cursor keys and space; fire, space or F1 starts, F2 picks one of the ten games, F3 / Help the difficulty
Pac-Man joystick in port 1, or W A S D, or the cursor keys; Q quits, fire or space starts
Paradroid joystick in either port, or cursor keys; fire (or space, CTRL, C=) with a direction shoots, held without one transfers, held on a lift or before a console opens it; Run/Stop pauses
Phoenix joystick in port 1, or cursor keys and space; stick down raises the force field
Stardew Pond joystick in either port, or cursor keys; fire or space uses what is in your hand; , . pick an item; I the backpack

About

Commodore Plus4 programs in C

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages