Skip to content

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

LayoutVMODECellsPixelsOn the screen
TextVC_VMODE_TEXT40 × 24 of 6 × 8240 × 192A border on all four sides
CompactVC_VMODE_COMPACT32 × 24 of 8 × 8256 × 192A border on all four sides
GraphicsVC_VMODE_GRAPHICS32 × 30 of 8 × 8256 × 240A border left and right
FullVC_VMODE_FULL40 × 30 of 8 × 8320 × 240The 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:

BitsColors in a tileBytes for an 8 × 8 tileTiles
128256
2416512
41632512
825664256

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–2WhereSize
VC_LCTRL_ATTR_CELLA byte for every cellSame as the name table
VC_LCTRL_ATTR_GROUPA byte for every eight tiles32 bytes
VC_LCTRL_ATTR_ROWA byte for every pixel row of every tile2 KB
VC_LCTRL_ATTR_NONENothing: 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:

BitsHolds
3–0Which palette row the cell's colors come from (not at 8 bits)
4Flip the tile left to right
5Flip it upside down
6Priority: draw this cell in front of sprites
7A 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:

RowColors
0The sixteen text-mode colors
1Grays, black to white
2–13Twelve 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
14Browns
15Blue-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:

asm
; 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
Four tiles and a palette row per cell. Press a key to go back to text. Open the full emulator
A screen filled with small square tiles — beveled blocks, stripes, checkers and white diamonds — colored in diagonal bands of red, orange, yellow, green, cyan, blue, violet and magenta, with black strips at the left and right.
Four tiles. Every color change is an attribute byte, not a different tile.

The recipe is the same for any mode:

  1. Display off, by writing MODE1 without VC_MODE1_DISP. The tables go in without anything half-built on the screen.
  2. The layout, with VdpSetMode.
  3. The layer: L0CTRL for bits and attributes, then the table bases, the palette row, the scroll.
  4. 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.
  5. 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

CallDoes
VdpInfoWhether there is a card, its firmware version and what it can do — see What's fitted
VdpWriteRegWrite a register: A the value, X the register
VdpSetModeChoose a layout: A = 1 Text, 2 Compact, 3 Graphics, 4 Full
VdpPoke, VdpPeekOne byte of the card's memory, anywhere in the 64 KB
VdpSetPaletteOne palette entry: X the entry, A red, Y green and blue
WaitVBlankReturn when the card finishes a picture
VdpLoadFileA file from the memory card, into the card's memory
VdpLoadFontThe card's character set, into layer 0's pattern table
VdpSpriteOne sprite's position, shape and colors
VdpSetScrollScroll a layer
VdpLayerShow or hide a layer
VdpStatusRead 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.

Written for BIOS v2.0. Released under the MIT License.