Skip to content
Techniques & Technology

Screen Memory

How the C64 displays text and graphics

Screen RAM, colour RAM, and the VIC-II—understanding how 1000 bytes create 40×25 characters of colourful text.

commodore-64screen-ramvic-iimemory-mapdirect-screen-access

The C64’s text screen is 40 characters wide by 25 lines tall—exactly 1000 characters. Each character is stored in screen RAM (which character to display) and colour RAM (what colour to draw it). Change a byte in screen RAM and the display updates instantly—no PRINT statements, no scrolling, just direct hardware control. This is how games draw fast.

Memory Layout

Screen RAM

Address: $0400-$07E7 (1024-2023 decimal)
Size: 1000 bytes (40 × 25)
Purpose: Stores screen codes for each character position

The screen is laid out sequentially in memory:

  • Bytes 1024-1063: Row 0 (top line, 40 characters)
  • Bytes 1064-1103: Row 1
  • Bytes 1104-1143: Row 2
  • Bytes 1984-2023: Row 24 (bottom line)

Colour RAM

Address: $D800-$DBE7 (55296-56295 decimal)
Size: 1000 bytes (40 × 25)
Purpose: Stores colour value (0-15) for each character

Colour RAM is its own dedicated 4-bit-wide static RAM chip on the C64 board, separate from the main 64 KB DRAM. It’s accessible at $D800-$DBE7 like normal memory but only the low 4 bits of each byte hold real data — the high 4 bits float and read as garbage. Always use AND 15 when reading colour RAM to mask off the floating bits, or accept that PEEKed values may have noise in the upper nibble.

Position Formula

To find the memory address for any screen position:

REM Calculate screen position
ROW = 10
COL = 5
SCREEN = 1024 + (ROW * 40) + COL
COLOUR = 55296 + (ROW * 40) + COL

Example: Position (0,0) = 1024. Position (0,39) = 1063. Position (1,0) = 1064. Position (24,39) = 2023.

How it works

The VIC-II chip scans screen RAM 50 times per second (PAL) or 60 times per second (NTSC), reading each byte and converting it to pixels on screen. Each screen code points to a character definition in the character ROM (or custom character set). The corresponding colour RAM byte tells the VIC-II what colour to draw that character.

The process:

  1. VIC-II reads byte from screen RAM (e.g., value 1 = letter ‘A’)
  2. VIC-II reads corresponding byte from colour RAM (e.g., value 7 = yellow)
  3. VIC-II looks up character 1 in character ROM (8 bytes define the ‘A’ shape)
  4. VIC-II draws yellow ‘A’ on screen

This happens continuously, automatically, every frame.

Screen Codes vs PETSCII

Critical difference: Screen RAM uses screen codes, not PETSCII codes.

Character PETSCII Screen Code
@ 64 0
A 65 1
B 66 2
Space 32 32
0 48 48

Rule of thumb:

  • PRINT uses PETSCII: PRINT "A" uses code 65
  • POKE uses screen codes: POKE 1024,1 displays ‘A’

See PETSCII Chart for complete conversion tables.

Practical Examples

Example 1: Write Character to Screen

10 REM Put 'H' at top-left corner
20 POKE 1024,8
30 POKE 55296,7

Screen code 8 = ‘H’. Colour 7 = yellow. Result: yellow ‘H’ at position (0,0).

Example 2: Clear Screen to Spaces

10 FOR I=1024 TO 2023
20 POKE I,32
30 NEXT I

Screen code 32 = space. Fills entire screen with spaces (much faster than PRINT).

Example 3: Write Text at Position

10 T$="HELLO"
20 R=10:C=15
30 P=1024+(R*40)+C
40 FOR I=1 TO LEN(T$)
50 A=ASC(MID$(T$,I,1))
60 IF A>=65 AND A<=90 THEN S=A-64 ELSE S=A
70 POKE P+I-1,S
80 POKE 55296+(R*40)+C+I-1,1
90 NEXT I

Displays “HELLO” in white at row 10, column 15. Converts PETSCII to screen codes on the fly.

Example 4: Fill Screen with Pattern

10 FOR I=1024 TO 2023
20 POKE I,160
30 POKE 55296+(I-1024),6
40 NEXT I

Screen code 160 = solid block. Colour 6 = blue. Result: solid blue screen.

Example 5: Inverse Video

10 REM Normal 'A' at position 0
20 POKE 1024,1
30 REM Inverse 'A' at position 1
40 POKE 1025,129

Screen codes 128-255 are the inverse (reverse video) versions of 0-127. Add 128 to any screen code for inverse display.

Character ROM Location

By default, the VIC-II reads character definitions from ROM at $D000-$DFFF. Each character is defined by 8 bytes (8×8 pixel matrix).

Character 1 (‘A’) definition in ROM:

$D008: 00011000  ($18 - top row, 2 pixels on)
$D009: 00111100  ($3C - 4 pixels)
$D00A: 01100110  ($66 - two legs)
$D00B: 01111110  ($7E - crossbar)
$D00C: 01100110  ($66 - legs)
$D00D: 01100110  ($66 - legs)
$D00E: 01100110  ($66 - legs)
$D00F: 00000000  ($00 - bottom - blank)

You can point the VIC-II to custom character sets in RAM—that’s how games create custom graphics. See VIC-II register $D018 for details.

VIC-II Memory Pointers

The VIC-II’s view of memory is controlled by register $D018 (53272 decimal). Full bit layout:

Bits 7-4: Screen RAM location (× $0400 within VIC bank)
Bits 3-1: Character set location (× $0800 within VIC bank)
Bit 0:    Unused (reads as 1)

Default value $14 (20 decimal):

  • Bits 7-4 = 0001 → screen RAM at $0400
  • Bits 3-1 = 010 → charset at $1000 (this is character ROM in VIC bank 0)
  • Bit 0 = 0

The VIC-II only sees a 16 KB window at a time, set by the inverted bottom 2 bits of CIA2’s $DD00. Within that 16 KB window:

$D018 value Screen RAM Charset
$04 $0000 $0000
$14 (default) $0400 $1000 (CharROM)
$24 $0800 $1000
$15 $0400 $0800

To change screen RAM location:

POKE 53272,PEEK(53272) AND 15 OR (bank*16)

Where bank is 0-15. Bank 1 = $0400 (default). Bank 2 = $0800. Bank 15 = $3C00.

Games often move screen RAM to different locations for double-buffering (draw to hidden screen, then flip).

Performance Notes

PRINT is slow because it:

  1. Converts PETSCII to screen codes
  2. Updates cursor position
  3. Handles scrolling
  4. Processes control codes

POKE is fast because it:

  1. Writes directly to memory
  2. No conversion overhead
  3. No cursor tracking
  4. Instant display update

For games, always POKE when drawing static screens, UI elements, or fast-updating content.

Limitations

  • Screen RAM is 1000 bytes, not 1024. Poking beyond 2023 wraps into other memory.
  • Colour RAM only uses low 4 bits (0-15). High bits ignored.
  • Screen RAM can be moved, but must align to 1KB boundaries ($0400, $0800, $0C00, etc.).
  • Character ROM can’t be written to (it’s ROM!). Copy to RAM for custom characters.

Advanced techniques

Double Buffering

Draw to hidden screen ($0800), then switch display:

10 REM Draw to hidden screen at $0800
20 FOR I=2048 TO 3047:POKE I,160:NEXT
30 REM Flip display to $0800
40 POKE 53272,(PEEK(53272) AND 15) OR 32

Smooth Scrolling

Combine screen RAM updates with VIC-II hardware scrolling registers ($D011, $D016) for smooth pixel-perfect scrolling.

Screen Splits

Use raster interrupts to change $D018 mid-frame, displaying different screen RAM on different parts of the screen. Status bars, split-screen games, and parallax effects all use this.

What moves and what does not

The Programmer’s Reference Guide describes the screen as “1000 possible locations” from 1024 to 2023, paired with “a group of 1000 locations called COLOR MEMORY or COLOR RAM” at 55296, each “4 bits wide”. Screen memory can be moved anywhere in the 16K the VIC-II “can access (‘see’)” at a time, and that 16K window is itself one of four banks chosen by bits 0 and 1 of CIA#2’s port A at $DD00 — after setting those bits to outputs in $DD02. Colour memory is the exception: it “can NOT move. It is always located at locations 55296 ($D800) through 56295 ($DBE7).” Every flip-the-display trick on the C64 follows from that split — the screen and character pointers move, the colour nybbles stay put.

Demo coders pushed the same registers further. FLI (Flexible Line Interpretation) rewrites $D018 and $D011 on every raster line so the VIC-II re-reads its colour information “each pixel-line instead of each char-line”, giving a bitmap eight times the colour resolution — at the cost of the “FLI bug”, a strip on the left of the screen where the forced colour fetch reads $FF, and of most of the raster time: Codebase64’s display loop is timed so that 23 cycles of code plus 40 cycles of colour DMA fill each 63-cycle PAL line exactly.

See also

Not yet fact-checked. This entry was drafted by an AI and nobody has verified it. The dates, figures and technical details may be wrong. Use it to find your bearings, then confirm anything that matters against a primary source.