C64 READY.

Docs / Memory (src/memory.js) — Architecture Overview

Memory (src/memory.js) — Architecture Overview

A high-level map of the C64's bank-switched memory subsystem: the per-page dispatch tables that make reads/writes fast, the 6510 CPU port and PLA banking, the four access functions (read/write/peek/peekForCpu), the $D000-$DFFF I/O routing, color RAM, the shared open-bus latch, cartridges, and the reset model.

This document describes the implementation and points at the real method and field names so it can be used as a guide into memory.js. Memory is the address-space router: it owns C64 RAM and color RAM, caches the active mapping supplied by an attached cartridge device, and decides — for every address — whether an access hits RAM, a ROM, an I/O chip, color RAM, a cartridge, or the open bus. The I/O chips and cartridge-specific state machines live elsewhere; Memory just dispatches to them. See the machine orchestrator for how Memory is wired into the system.


1. Big picture

Every CPU memory access goes through read(addr) / write(addr, val). The C64's banking (which of RAM / KERNAL / BASIC / CHAR-ROM / I/O / cartridge is visible at a given address) changes at runtime via the 6510 CPU port ($01) and the cartridge EXROM/GAME lines. Rather than re-decode the banking on every access, Memory precomputes a 256-entry per-page dispatch table and rebuilds it only when banking state changes.

   CPU / VIC
      │ read(addr) / write(addr)
      ▼
   page = addr >> 8
      │
      ├─ _readPageArr[page] != null ?
      │     yes → typed-array load:  arr[addr - _readPageOffset[page]]   (FAST)
      │     no  → _readSlow(addr) → _readIO(addr)  → VIC/SID/CIA/colorRAM/cart   (SLOW)
      │
      └─ every access updates externalDataBus8  (the shared open-bus latch)

The common case — RAM, KERNAL, BASIC, CHAR-ROM, cartridge ROM, ultimax open-bus — resolves to a single typed-array indexed load. Only I/O ($D000-$DFFF when gated on) and the CPU port ($00/$01) take the slow path.


2. The per-page dispatch table

Four parallel arrays, indexed by page (addr >> 8, 0-255):

Array Meaning
_readPageArr[page] the Uint8Array to read from, or null ⇒ slow path
_readPageOffset[page] value subtracted from addr to index that array
_writePageArr[page] / _writePageOffset[page] same, for writes (null ⇒ slow / dropped)

So read is arr[addr - offset] and write is arr[addr - offset] = val. The offset trick lets a page point into RAM (offset 0), KERNAL (offset $E000), char-ROM (offset $D000), etc., all through one indexed load.

A reusable _openBus scratch buffer (256 bytes of $FF) serves ultimax open-bus pages: each such page sets offset = page << 8 so arr[addr - offset] always lands at _openBus[addr & 0xFF].

_rebuildMemoryMap() fills these tables from the current CPU port + cartridge state. It is cheap (256 iterations) and fires only on state changes — a CPU port write, a cartridge swap, or a ROM assignment (the kernal/basic/charRom property setters call it) — never per CPU cycle.


3. The 6510 CPU port ($00 / $01) & banking

Addresses $00/$01 are the 6510's on-chip I/O port, not RAM, so they are special-cased at the top of read/write before the table dispatch:

Banking (_rebuildMemoryMap) reads the port pins ((cpuPort & cpuDDR) | (0x07 & ~cpuDDR)) to extract LORAM/HIRAM/CHAREN, then maps each page per the PLA truth table:

Region Visible when
KERNAL ROM ($E000-$FFFF) HIRAM
BASIC ROM ($A000-$BFFF) LORAM && HIRAM
CHAR ROM ($D000-$DFFF) !CHAREN (and CHAREN ⇒ I/O instead)
I/O ($D000-$DFFF) (LORAM || HIRAM) && CHAREN
RAM otherwise (ROM regions still take writes — ROM is a read shadow)

Cartridge modes (8k/16k/ultimax) override this — see §6.


4. The four access functions

Function Side effects? Drives bus latch? Used by
read(addr) yes (device reads clear flags / advance state) yes normal CPU reads
write(addr, val) yes yes (except $00/$01) normal CPU writes
peek(addr) no — never pokes a device; returns the bus latch for I/O pages no VIC sampling the byte a BA-stalled CPU drives (§3.14.6 invalid c-reads)
peekForCpu(addr) no read side effects, but returns the value an I/O register read would place on the bus (_peekIO) no CPU opcode predecode (peek(pc)), so decoding doesn't ack a CIA ICR etc.

The split matters: the CPU decodes the next opcode with peekForCpu so the predecode doesn't trigger a real I/O side effect, then the cycle-1 micro-op does the actual read().


5. I/O region routing ($D000-$DFFF)

When the page table maps a $Dx page to the slow path (null), _readSlow / _writeSlow route to _readIO / _writeIO:

Range Device
$D000-$D3FF VIC-II (addr & 0x3F)
$D400-$D7FF SID (addr & 0x1F)
$D800-$DBFF Color RAM (low nibble; see §7)
$DC00-$DCFF CIA1 (addr & 0x0F) — joystick ports ANDed in (§8)
$DD00-$DDFF CIA2 (addr & 0x0F)
$DE00-$DFFF attached cartridge device's ioRead / ioWrite / ioPeek, or open bus

A missing device returns _openBusRead(). _peekIO mirrors _readIO without side effects.


6. Cartridges

cartridges/registry.js maps CRT hardware-type numbers to device classes. C64Machine.loadCartridge() parses the container, creates the device, and calls Memory.installCartridge(device). Tests and older callers may use setCartridge(cfg), which constructs the same devices through a compatibility factory.

Every device owns its ROM/RAM arrays, registers, I/O decoding, serialization, and reset/freeze lifecycle. It publishes only a small mapping cache: mode, active romLo/romHi, optional writable ROML target, and an optional exceptional ROML read hook. applyMapping() copies these stable values into Memory and rebuilds the page table. Normal reads therefore retain the direct typed-array hot path; only I/O and exceptional electrical behavior call the device.

Registered devices:

ROM regions are normally read shadows: underlying C64 RAM still receives writes. Ultimax ROM pages normally drop writes; devices can explicitly publish a writable ROML target for cartridge RAM.


7. Color RAM

Color RAM ($D800-$DBFF, 1 KB) is only 4 bits wide — the upper nibble is open bus. On read, _readIO composes (externalDataBus8 & 0xF0) | (colorRam & 0x0F) (the upper nibble is typically the byte the VIC fetched in phi1 of this cycle), and — if colorRamReadDrivesComposedByte — re-drives the latch with that composed value. Writes store only the low nibble. This is byte-faithful to how demos read color RAM during open side borders.


8. Open bus & the shared data-bus latch

externalDataBus8 models the C64 data bus (D0-D7) when no device is actively driving it. It is updated by every CPU read, every CPU write (in read/ write), and every VIC chip-bus fetch (the VIC holds a mem back-reference so its g-/c-/sprite-accesses re-drive the latch). An open-bus read$DE00-$DFFF with no cart device, or the high nibble of color RAM — samples this latch instead of returning a fixed value. _openBusRead has three modes (openBusMode):

The CIA1 joystick integration also lives here: _readCIA1 ANDs joyPort2 into $DC00 and joyPort1 into $DC01 (both ports also carry the keyboard matrix, so joystick bits AND in).


9. Reset model

Two layers, matching the hardware:

loadROM(data, baseAddr) is a simple bulk copy into RAM (used for raw ROM loads).


10. Profile flags (bisection knobs)

Defaults preserve current demo behaviour; each can be toggled at runtime to A/B a regression:

Flag Default Effect
openBusMode 'vice-compatible' open-bus read source (latch / $FF / random)
colorRamReadDrivesComposedByte true a color-RAM read re-drives the latch with the composed byte
openBusWritesToZeroOneEnabled false the 6510 RAM-under-port quirk (VIC phi1 byte lands in RAM[$00/$01])

11. Key invariants & gotchas (quick reference)