The graphics modes
Text mode is one of four screen layouts the video card can draw. The other three are grids of 8 × 8 tiles, and in all four the card works the same way: tables of bytes in its own memory say what goes where, and the card draws the picture from them sixty times a second. Your program writes the tables; it never draws a pixel.
The four layouts
| Layout | VMODE | Cells | Pixels | On the screen |
|---|---|---|---|---|
| Text | VC_VMODE_TEXT | 40 × 24 of 6 × 8 | 240 × 192 | A border on all four sides |
| Compact | VC_VMODE_COMPACT | 32 × 24 of 8 × 8 | 256 × 192 | A border on all four sides |
| Graphics | VC_VMODE_GRAPHICS | 32 × 30 of 8 × 8 | 256 × 240 | A border left and right |
| Full | VC_VMODE_FULL | 40 × 30 of 8 × 8 | 320 × 240 | The whole screen, no border |
VdpSetMode picks one. Graphics is the one to reach for first; Full is Graphics widened to the edges of the screen, at the price of a name table that needs 1,200 bytes instead of 960 and a horizontal scroll that needs nine bits.
The layout says nothing about color. That is set per layer, and any layout can have any of the choices below.
Programs written for the TMS9918A
The card comes out of reset pretending to be a TMS9918A, with VMODE at 0, and while it stays at 0 the old chip's own mode bits choose the picture: its text mode and Graphics I, sprites and all. That is how a cartridge written for the old chip still runs, since it programs the card before anything is printed. Once the text screen is up, VMODE is 1, and a program that wants the old modes writes 0 there first. Graphics II and Multicolor aren't there at all — a program that uses either draws the wrong picture — and the old chip is documented in the BIOS 1.6 edition of this guide.
Three tables make a picture
The name table has a byte for each cell, left to right and top to bottom, saying which tile goes there.
The pattern table holds the tiles themselves: what each one looks like, pixel by pixel.
The attribute table, if the layer has one, holds a byte for each cell saying how to color it.
Each table lives wherever a register says it does. The name and attribute table registers count in steps of 1 KB, the pattern table's in steps of 2 KB, so L0PAT = 4 puts layer 0's tiles at $2000.
Bits per pixel
Every pixel of a tile is a number, and the layer's control register, L0CTRL, says how many bits it has:
| Bits | Colors in a tile | Bytes for an 8 × 8 tile | Tiles |
|---|---|---|---|
| 1 | 2 | 8 | 256 |
| 2 | 4 | 16 | 512 |
| 4 | 16 | 32 | 512 |
| 8 | 256 | 64 | 256 |
More bits look better and cost more to send: the card's memory is not the limit on this machine, the 65C02's time is. A set of 256 one-bit tiles is 2 KB and goes across in a blink; the same set at 4 bits is four times as long.
Attributes
The same register chooses where a cell's color comes from:
L0CTRL bits 3–2 | Where | Size |
|---|---|---|
VC_LCTRL_ATTR_CELL | A byte for every cell | Same as the name table |
VC_LCTRL_ATTR_GROUP | A byte for every eight tiles | 32 bytes |
VC_LCTRL_ATTR_ROW | A byte for every pixel row of every tile | 2 KB |
VC_LCTRL_ATTR_NONE | Nothing: the whole layer uses one set of colors | — |
At one bit per pixel an attribute is a color pair, foreground in the high nibble and background in the low one, the way text mode's pen is. At 2, 4 and 8 bits it is a set of fields:
| Bits | Holds |
|---|---|
| 3–0 | Which palette row the cell's colors come from (not at 8 bits) |
| 4 | Flip the tile left to right |
| 5 | Flip it upside down |
| 6 | Priority: draw this cell in front of sprites |
| 7 | A ninth bit for the tile number, which is how 2 and 4 bits reach 512 tiles |
The palette
The card shows 256 colors at once, each one of 4,096. They live in its own memory, 512 bytes at $FC00: two bytes an entry, %0000RRRR then %GGGGBBBB. VdpSetPalette writes one, and anything drawn with it changes on the next line the card draws.
Out of reset, the 256 are laid out as sixteen rows of sixteen, so that a 4-bit tile which picks a row gets a whole set of shades:
| Row | Colors |
|---|---|
| 0 | The sixteen text-mode colors |
| 1 | Grays, black to white |
| 2–13 | Twelve hues — red, orange, yellow, chartreuse, green, spring green, cyan, azure, blue, violet, magenta, rose — each dark at 0, pure at 7, nearly white at 15 |
| 14 | Browns |
| 15 | Blue-grays |
Entry 0 of a row is special: a pixel with the value 0 is transparent, and whatever is behind it shows through, unless the layer is set to draw it (VC_LCTRL_OPAQUE). Behind everything is the backdrop, a single color set by the low nibble of the COLOR register, which also fills the border.
A screen of tiles
This program builds a Graphics-mode screen from four 4-bit tiles, giving every cell a palette row of its own:
; A screen of tiles in Graphics mode: 32 × 30 cells of 8 × 8 pixels, sixteen
; colors in every cell, and a palette row for each cell to pick its colors from.
;
; Three tables make the picture, all in the card's own memory: patterns (what
; each tile looks like), names (which tile goes in each cell) and attributes
; (which palette row each cell draws in). Press a key to go back to text.
.setcpu "65C02"
.include "6502-VDP.inc"
.segment "CODE"
BasicStartup:
.byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00
COLS = 32 ; Graphics mode
ROWS = 30
NAMES = $0000 ; one byte per cell: which tile
ATTRIBUTES = $0400 ; one byte per cell: which palette row
PATTERNS = $2000 ; 32 bytes per 4-bit tile
TILE_BYTES = 32 ; 8 rows × 4 bytes, two pixels to a byte
TILE_COUNT = 4
GRAY_ROW = 1 ; palette row 1 is a gray ramp
FIRST_HUE = 2 ; rows 2-13 are twelve hues
HUES = 12
Column := $40
Row := $41
Temp := $42
Start:
jsr KernalVersion ; A = major version
cmp #2
bcc NoCard
jsr VdpInfo ; carry set: no 6502-PICOVDP
bcc Setup
NoCard:
lda #<NeedsCard
ldy #>NeedsCard
jmp PrintStr
; -----------------------------------------------------------------------------
; The registers, with the display off so nothing half-built is ever seen.
; -----------------------------------------------------------------------------
Setup:
lda #0
ldx #VC_REG_MODE1 ; display off
jsr VdpWriteReg
lda #VC_VMODE_GRAPHICS ; 32 × 30 cells of 8 × 8
jsr VdpSetMode
lda #VC_LCTRL_4BPP | VC_LCTRL_ATTR_CELL | VC_LCTRL_ENABLE | VC_LCTRL_OPAQUE
ldx #VC_REG_L0CTRL ; 4 bits a pixel, an attribute per cell,
jsr VdpWriteReg ; layer on, color 0 drawn rather than see-through
ldy #0
@register:
lda Registers+1,y ; the value
ldx Registers,y ; the register
jsr VdpWriteReg ; keeps Y
iny
iny
cpy #RegistersEnd - Registers
bne @register
; -----------------------------------------------------------------------------
; The patterns: four tiles, straight down the data port.
; -----------------------------------------------------------------------------
lda #<PATTERNS
ldx #>PATTERNS
jsr PointAt
ldx #0
@pattern:
lda Tiles,x
sta VC_DATA ; the card's pointer moves on by itself
inx
cpx #TILE_BYTES * TILE_COUNT
bne @pattern
; -----------------------------------------------------------------------------
; The names: tiles 0-3 in a two-by-two repeat.
; -----------------------------------------------------------------------------
lda #<NAMES
ldx #>NAMES
jsr PointAt
stz Row
@nameRow:
stz Column
@nameCell:
lda Row
and #1
asl a ; 2 on odd rows
sta Temp
lda Column
and #1 ; + 1 on odd columns
ora Temp
sta VC_DATA
inc Column
lda Column
cmp #COLS
bne @nameCell
inc Row
lda Row
cmp #ROWS
bne @nameRow
; -----------------------------------------------------------------------------
; The attributes: a palette row per cell. Tile 3 draws in gray; everything
; else takes a hue that steps every two cells across and every two down, so
; the colors run in diagonal bands.
; -----------------------------------------------------------------------------
lda #<ATTRIBUTES
ldx #>ATTRIBUTES
jsr PointAt
stz Row
@attrRow:
stz Column
@attrCell:
lda Column
and Row
and #1 ; odd column and odd row: tile 3
beq @hue
lda #GRAY_ROW
bra @store
@hue:
lda Column
lsr a
sta Temp
lda Row
lsr a
clc
adc Temp ; 0-29
@wrap:
cmp #HUES
bcc @inRange
sbc #HUES ; carry is set here
bra @wrap
@inRange:
adc #FIRST_HUE ; carry is clear here
@store:
sta VC_DATA
inc Column
lda Column
cmp #COLS
bne @attrCell
inc Row
lda Row
cmp #ROWS
bne @attrRow
lda #VC_MODE1_DISP ; display on
ldx #VC_REG_MODE1
jsr VdpWriteReg
; -----------------------------------------------------------------------------
; Wait for a key, then text mode and the card's own character set back.
; -----------------------------------------------------------------------------
@wait:
jsr BufferSize
beq @wait
jsr ReadBuffer
jsr InitVideo
jmp VideoClear ; the tables still hold tiles, not text
; Point port A at a card address below $4000, for writing. A = low, X = high.
PointAt:
sta VC_REG
txa
ora #VC_ADDR_WRITE
sta VC_REG
rts
; Register, value — everything about layer 0 the mode doesn't set.
Registers:
.byte VC_REG_L0NAME, NAMES >> 10 ; table bases count in 1 KB ...
.byte VC_REG_L0ATTR, ATTRIBUTES >> 10
.byte VC_REG_L0PAT, PATTERNS >> 11 ; ... and patterns in 2 KB
.byte VC_REG_L0PAL, 0
.byte VC_REG_L0SCRX, 0 ; text mode may have left it scrolled
.byte VC_REG_L0SCRY, 0
.byte VC_REG_SPRCTRL, 0 ; no sprites
.byte VC_REG_COLOR, TMS_BLACK ; black beside the picture
RegistersEnd:
; Four tiles. Each nibble is a pixel, and its value is a color in the cell's
; palette row: 0 darkest, 7 the pure hue, 15 nearly white.
Tiles:
; A beveled block: light top and left, dark bottom and right
.byte $DD, $DD, $DD, $DB
.byte $DB, $99, $99, $73
.byte $D9, $77, $77, $53
.byte $D9, $77, $77, $53
.byte $D9, $77, $77, $53
.byte $D9, $77, $77, $53
.byte $D7, $55, $55, $53
.byte $B3, $33, $33, $31
; Diagonal stripes
.byte $99, $94, $44, $44
.byte $49, $99, $44, $44
.byte $44, $99, $94, $44
.byte $44, $49, $99, $44
.byte $44, $44, $99, $94
.byte $44, $44, $49, $99
.byte $94, $44, $44, $99
.byte $99, $44, $44, $49
; A checker
.byte $66, $BB, $66, $BB
.byte $66, $BB, $66, $BB
.byte $BB, $66, $BB, $66
.byte $BB, $66, $BB, $66
.byte $66, $BB, $66, $BB
.byte $66, $BB, $66, $BB
.byte $BB, $66, $BB, $66
.byte $BB, $66, $BB, $66
; A diamond
.byte $33, $33, $33, $33
.byte $33, $3F, $F3, $33
.byte $33, $FF, $FF, $33
.byte $3F, $FF, $FF, $F3
.byte $3F, $FF, $FF, $F3
.byte $33, $FF, $FF, $33
.byte $33, $3F, $F3, $33
.byte $33, $33, $33, $33
NeedsCard: .byte "NEEDS BIOS 2 AND A 6502-PICOVDP", CHAR_CR, CHAR_LF, $00
The recipe is the same for any mode:
- Display off, by writing
MODE1withoutVC_MODE1_DISP. The tables go in without anything half-built on the screen. - The layout, with
VdpSetMode. - The layer:
L0CTRLfor bits and attributes, then the table bases, the palette row, the scroll. - The tables, straight down the data port: point port A at an address once, and every byte written after that lands in the next place.
- Display on.
And to get back to text, InitVideo and then VideoClear. InitVideo resets every register text mode depends on and has the card copy its character set in again; VideoClear is needed because the name table still holds tile numbers.
Use the Kernal for registers
VdpWriteReg writes a register and keeps track of what it wrote. The card's registers can't be read back, so the Kernal keeps its own copy of the two layer control registers, and VdpLayer and VdpSetScroll change one bit of them without disturbing the rest. A register written straight to the port bypasses that record.
Above $3FFF
The command that points a port at an address carries fourteen bits, which reaches $3FFF. For the top three quarters of the card's 64 KB, write the top two bits of the address to VBANK first. Put it back to 0 when you're done: the Kernal counts on it. VdpPoke and VdpPeek do both for you, a byte at a time.
The vertical blank
The card draws a picture from the top down, and changing a table while it is drawing tears the picture across the middle. The moment to change things is between pictures, and WaitVBlank returns at the start of that gap.
The gap is shorter than it sounds. In Text and Compact the picture has borders above and below, and there are about 4,500 cycles before drawing starts again; in Graphics and Full there are no borders, and there are about 1,400. That is enough to move sprites and scroll a layer, and not enough to rewrite a name table — which is why a full screen is built with the display off.
Loading from the card
VdpLoadFile reads a file from the memory card straight into the card's memory, at any address: a pattern table drawn on your computer, a screen, a palette. From BASIC the same job is one statement.
VdpLoadFont has the card copy its own character set into layer 0's pattern table, wherever that is. The font is one bit per pixel and six pixels wide, so it works in any layout: in the 8 × 8 ones the letters sit at the left of their cells.
The Kernal's calls for the card
| Call | Does |
|---|---|
VdpInfo | Whether there is a card, its firmware version and what it can do — see What's fitted |
VdpWriteReg | Write a register: A the value, X the register |
VdpSetMode | Choose a layout: A = 1 Text, 2 Compact, 3 Graphics, 4 Full |
VdpPoke, VdpPeek | One byte of the card's memory, anywhere in the 64 KB |
VdpSetPalette | One palette entry: X the entry, A red, Y green and blue |
WaitVBlank | Return when the card finishes a picture |
VdpLoadFile | A file from the memory card, into the card's memory |
VdpLoadFont | The card's character set, into layer 0's pattern table |
VdpSprite | One sprite's position, shape and colors |
VdpSetScroll | Scroll a layer |
VdpLayer | Show or hide a layer |
VdpStatus | Read a status register |
All of them return with the carry set, having done nothing, on a machine with no card, so a program can call them and check the carry rather than detecting the card first. The Kernal has every one in detail.
Drawing something you meant to draw
The tables in the program above were typed by hand, which is fine for four tiles and no way to make a game. Draw them in a tool on your computer, export the bytes, and either .incbin them into your program or put them on the memory card and load them with VdpLoadFile.
Next: layers and sprites.

