C64 READY.

Docs / Performance

Performance

This is a cycle-accurate C64: every master cycle it clocks the VIC-II, the 6510, two CIAs, the SID shadow voices and (when attached) the 1541's 6502. That per-cycle fidelity — not demo content — is what sets the cost, so the performance story is mostly about keeping the always-on per-cycle machinery cheap and allocation-free. Runtime performance switches live in src/switches.js; the rendering pipeline is summarised in the master overview ("Rendering pipeline & performance switches") and detailed in the VIC-II §14.

1. Cost model

One PAL frame is 19 656 master cycles and must finish inside ~20 ms (50 Hz) to hold realtime. Where that budget goes, structurally:

2. Standing optimizations

All of these are in place and on by default. The two with runtime switches live in src/switches.js and can be flipped for A/B without a rebuild — append ?NAME=0 to the URL in the browser, or set NAME=0 as an env var for node harnesses.

src/switches.js also carries two hardware-accuracy switches (driveTrueClockRatio, iecEdgeLatency, both default on). These tune drive/IEC timing fidelity, not performance — see the 1541 drive §3 and the machine orchestrator §3.

3. Throughput & footprint

The figures below are approximate and machine-dependent — treat them as orders of magnitude, not benchmarks.

4. Platform & mobile

Desktop V8 has generous heaps and an optimiser that erases most short-lived allocation, so garbage collection is a non-issue there. Phones are less forgiving:

5. Requirements

The SID audio ring is a SharedArrayBuffer, so the page must be cross-origin isolated. That requires two response headers on the document (and its assets):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them SharedArrayBuffer is unavailable and the machine constructor throws. The Vite dev and preview servers set these headers automatically; a production host must send the same. See the machine orchestrator §7.

6. Measurement Harnesses

Never trust a performance comparison across a different thermal window. On an M1-class laptop, a heavy build, test run, or screenshot pass can change the next number enough to make a neutral change look like a large regression. Use a same-thermal A/B: measure the candidate, stash only the files under test, measure the baseline, then pop and measure the candidate again back-to-back.

The desktop V8 harness is tools/perf-workloads.mjs:

C64_ASSETS=<your C64 disk dir> node --expose-gc tools/perf-workloads.mjs <workload> <mode> [frames]

Workloads are idle, orbit, rastertime, comaload, and comarun; modes are time, prof, alloc, allocsites, mem, and all. time is useful for throughput, but V8 can escape-analyze short-lived allocations away. Use allocsites to see allocation a non-EA engine, such as JavaScriptCore on iOS, would still pay for.

The Safari/JSC harness is tools/jsc-perf.mjs plus tools/jsc-perf-harness.html:

npx playwright install webkit          # one-time
node tools/jsc-perf.mjs "label"

It starts a throwaway static server with the COOP/COEP headers required by the SID SharedArrayBuffer, launches Playwright WebKit, boots to READY, and times 9×300-frame idle batches through the real unbundled modules. Treat desktop WebKit time as only part of the story: allocation reductions can be neutral on a desktop core with an ample nursery and still remove visible jank on a phone.