Skip to content

Repository files navigation

Made with Claude

cc64-web

A JavaScript reimplementation of cc64 — Philip Zembrod's small-C compiler for the Commodore 64 — running natively in the browser and producing standard C64 .prg files, with one-click handoff to Web64 (Mika Jussila's browser VICE port).

Live: https://rpi6.memention.net/cc64-web/ — write C, press Compile, press Run in Web64.

The original cc64 is 6502 machine code (written in VolksForth) that runs on the C64, so there was nothing to compile to WASM. This project rebuilds the whole pipeline — scanner, preprocessor, parser, code generator, minilinker — in dependency-free ESM JavaScript, reusing cc64's release runtime modules (assets/rt/) as binary inputs.

The prime directive: byte-identity

Output is byte-identical to real cc64. The reference implementation is cc64 itself running headlessly in VICE (tools/oracle/); the golden PRGs it produces are the test targets. Four fixtures — helloworld, sieve, printf/libc, and a torture test (switch, function pointers, prototypes, ?:, &&/||, pointer arithmetic) — compile to the same bytes as the real thing (npm test).

Everything from the Forth source is ported faithfully, including the odd corners: >> is an arithmetic shift, / and % floor toward −∞, string literals are PETSCII, small #defines are char-typed, static init data is emitted hi-byte-first into a reversed stream. docs/PLAN.md has the full Forth→JS port map and fidelity notes.

Extensions (clearly fenced off)

Three additions real cc64 rejects, documented as deliberate divergences — golden output is untouched unless a source opts in:

  • __zeropage storage class — file-scope variables allocated in $57–$70 (the BASIC FP work area, free while only the KERNAL is called), addressed with zero-page opcodes: one cycle and one byte less per access.
  • __asm { ... } inline assembly (src/asmblock.js) — a full line-oriented 6502 assembler inside function bodies: local labels, symbol+offset operands (self-modifying code works), #</#>, .byte/.word, automatic zp/abs selection, forward #<label/#>label byte-selects, and identifiers that resolve to C globals and #define constants through the compiler's symbol table.
  • __sprite data blocks (src/sprite.js) — C64 sprites drawn as pixel art directly in the source: 21 raw rows compile to a 64-byte char array, hires (24 ./x pixels per row) or multicolor (12 ./-/o/x pairs), whitespace as visual grouping.

The raytracer

raytracer

examples/raytracer/ is the proving ground: a mirror sphere over a checkered floor (a C port of an assembly original), 320×200 hires, blue-noise dithered, all math in 8.8 fixed point. The optimization log in its README walks from a naive ~2 hours per frame down to 3.8 minutes on a stock PAL C64 — 42% faster than the hand-written assembly original — using the extensions above (quarter-square multiply with self-modifying table lookups in __asm, long-division fdiv, the zeropage pool filled to the last byte) plus operands passed straight through the zeropage cells, reflection algebra that never materializes the surface normal, a squares-only fsq(), a leading-zero skip in fdiv, and an incremental shadow term. Every step verified on the cycle-exact 6502 harness — byte-identical renders, except the reflection algebra (212 dither pixels) and the shadow term (34, at the shadow's dithered edge).

Boing

boing

examples/boing/ is the __sprite showcase: a red/white checkered ball (hello, Amiga) bouncing around the screen with gravity, drawn as six multicolor __sprite frames — the same ball with its checker pattern shifted one column each, covering the pattern's full 6-column period; the sprite pointer is picked from the x position so the checkers stay locked to the screen and the ball rolls over them. Along the way it demonstrates the raster-compare frame loop, the 9th sprite-x bit, 1/8-pixel fixed-point movement, and the char-typed-#define gotcha (0 + XMIN) that once froze the ball against the left wall.

Ghosts in the border

ghosts

examples/ghosts/ is the harder demo: eight ghosts bob in the lower border, below where the 25-row screen ends — the classic VIC-II open-border trick. A raster interrupt switches the VIC to 24-row mode between the two border compare lines so the flip-flop is never set and the VIC keeps drawing sprites into the border; a second interrupt near the top restores 25-row mode each frame. The two handlers are installed by hand in an __asm block (CIA masking, $0314/$0315 vector, $ea81 exit), and the ghost is a hires __sprite. It needs real VIC-II timing, so run it in Web64 — the pure-CPU harness has no raster.

Sprites in the side borders

sideborders

examples/sideborders/ goes one harder: two ghosts in the left and right borders. Opening a side border means switching 40/38 columns ($d016) at an exact cycle on every raster line — a ~1-cycle window — so it needs a cycle-stable raster (a double interrupt whose txs cancels the entry jitter, then a loop that is exactly 63 cycles per line), no badlines (it runs inside the opened lower border), and Y-expanded sprites so the sprite DMA that steals CPU cycles is uniform across every opened line. All in cc64 C with an __asm handler.

Sprites beside the frame

sideborders2

examples/sideborders2/ moves the same side-border trick up into the display window — a ghost in the left and right border level with the middle of the screen, beside the text frame. That's harder than the lower-border version for two reasons: the badlines that only exist inside the window would steal ~40 cycles and wreck the per-line timing, dodged here with a per-line FLD $d011 write so no line is ever a badline; and the ~1-cycle window is unforgiving enough that a naive single-IRQ sync only opens part of the band, so it takes the double-interrupt stabiliser (the per-line $d016 flip itself stays the plain naive loop). Verified stable and full-height in VICE.

Tooling

  • tools/run6502.mjs — minimal NMOS 6502 interpreter with cycle-exact timing (page-cross and branch penalties), used for semantic probing and benchmarks: make bench PRG=file.prg.
  • tools/profile6502.mjs — per-function profiler: compiles a file or a whole project dir, runs it, attributes every instruction and cycle by address, resolves the runtime's $mult/$divmod/$shl/$shr helpers by name from its jump table, and follows cc64's prototype jmp stubs when counting calls: make profile SRC=examples/raytracer.
  • tools/oracle/ — the VICE pipeline that builds golden PRGs from real cc64 (make golden SRC=... CC64_REPO=<cc64 checkout>).
  • src/d64.js / src/petscii.js — 1541 disk images (validated against c1541) and ASCII↔PETSCII, supporting the oracle.

The browser IDE

npm run web    # -> http://localhost:8064/web/

web/index.html is a zero-dependency IDE: multiple named projects in localStorage, a files rail with the bundled cc64 headers, syntax highlighting driven by the compiler's real keyword list, a brace-depth formatter, and unity builds (src/amalgamate.js concatenates the project's .c files with hoisted, deduped includes). Projects export and import as .cc64proj.json — each example's mkproject.mjs builds one, and the deployed IDE seeds the raytracer and boing projects on first visit.

Handoff to Web64

The deployed instance keeps compiled PRGs in memory for 5 minutes (server/cc64web_server.py, ~180 lines of Python stdlib) and hands Web64 a fetchable URL via its ?file= autostart, warp during load toggleable via a checkbox:

compile → POST api/prg → open web64.nofs.ai/?file=<prg-url>&autorun=true&warp=true

Locally, the program row is draggable straight onto a Web64 tab (Chromium's DownloadURL drag becomes a real file drop), or download the .prg. Deployment bits live in deploy/.

Commands

make             # menu of everything below
npm test         # full suite, incl. the byte-identity differential test
npm run web      # dev server for the IDE
make prg SRC=f.c            # compile a file from the CLI
make bench PRG=f.prg        # cycle-exact benchmark
make profile SRC=<f.c|dir>  # per-function cycle profile
make golden SRC=f.c         # build a golden via real cc64 in VICE

Credits

  • Philip Zembrod — cc64, the real thing.
  • Mika Jussila — Web64, the browser C64.
  • The unported remainder: link-lib (library modules for sources without main()) and cc64's editor/shell/X16 variants — see docs/PLAN.md.

About

cc64 small-C compiler for the C64, reimplemented in JS — byte-identical output, browser IDE, Web64 handoff

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages