Skip to content

The screen

The video card is a 6502-PICOVDP, and the machine uses it first as a text screen: 40 columns by 24 rows of 6 × 8 cells, each character with a color pair of its own, drawn in a character set the card carries itself. The screen comes up the first time anything is printed — BASIC's header, on a normal start — so by the time your program runs it is already there.

That is the part of the card this chapter covers. Underneath the text it has three more screen layouts, 256 colors, two layers and 64 sprites, and the graphics modes is where those start.

Text mode, as the machine leaves it

VideoClearFill the screen with spaces in the current pen, cursor to the top left
VideoSetCursorX = column 0–39, Y = row 0–23
VideoGetCursorThe same two, back out
VideoPutCharPut the character in A at the cursor, without moving it
VideoChroutRawPut it there and advance, wrapping and scrolling as needed
VideoScrollEverything up one line
VideoSetColorThe pen: letters and background, one nibble each — and the border
InitVideoPut the whole thing back to text mode, character set and colors included

The difference between VideoPutChar and VideoChroutRaw is the one to keep straight. VideoPutChar stamps. VideoChroutRaw stamps and moves along — and unlike Chrout it does not interpret anything, so all 256 characters are available to it.

Drawing something

asm
; Drawing on the screen directly — a framed sign, built out of the box-drawing
; characters the machine already has in its character set.
;
; Console output goes wherever the machine's console goes. This does not: it
; puts characters at chosen positions on the screen, which is how a game draws
; and how anything with a layout draws.

.setcpu "65C02"

.include "6502-VDP.inc"

.segment "CODE"

BasicStartup:
  .byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00

BOX_LEFT  = 8                   ; column of the left-hand edge
BOX_TOP   = 6                   ; row of the top edge
BOX_WIDTH = 24                  ; including both edges

; The box-drawing corners and edges, by character code.
TOP_LEFT     = $C9
TOP_RIGHT    = $BB
BOTTOM_LEFT  = $C8
BOTTOM_RIGHT = $BC
ACROSS       = $CD
DOWN         = $BA

Left  := $40                    ; what DrawRow puts at each end and in between
Middle := $41
Right := $42
Row   := $43

Start:
  lda HW_PRESENT
  and #HW_VID                   ; no screen, nothing to draw on
  beq NoScreen

  lda #(TMS_LT_YELLOW * 16) | TMS_DK_BLUE
  jsr VideoSetColor             ; letters, then background
  jsr VideoClear                ; fills the screen with those colors

  lda #TOP_LEFT
  sta Left
  lda #ACROSS
  sta Middle
  lda #TOP_RIGHT
  sta Right
  lda #BOX_TOP
  sta Row
  jsr DrawRow

  lda #DOWN                     ; three hollow rows
  sta Left
  sta Right
  lda #' '
  sta Middle
  ldx #3
@sides:
  phx
  inc Row
  jsr DrawRow
  plx
  dex
  bne @sides

  lda #BOTTOM_LEFT
  sta Left
  lda #ACROSS
  sta Middle
  lda #BOTTOM_RIGHT
  sta Right
  inc Row
  jsr DrawRow

  ldx #16                       ; centered in the box
  ldy #BOX_TOP + 2
  jsr VideoSetCursor
  ldy #0
@title:
  lda Title,y
  beq Done
  jsr VideoChroutRaw            ; stamps the character and moves along
  iny
  bra @title

; Leave the cursor somewhere sensible. Whatever prints next — including
; BASIC's own prompt — carries on from wherever this program left it.
Done:
  ldx #0
  ldy #20
  jmp VideoSetCursor

NoScreen:
  lda #<NoScreenMsg
  ldy #>NoScreenMsg
  jsr PrintStr
  rts

; One row of the box: an edge, a run of middles, an edge.
DrawRow:
  ldx #BOX_LEFT
  ldy Row
  jsr VideoSetCursor
  lda Left
  jsr VideoChroutRaw
  ldx #BOX_WIDTH - 2
@across:
  lda Middle
  jsr VideoChroutRaw            ; keeps X and Y for us
  dex
  bne @across
  lda Right
  jmp VideoChroutRaw

Title:        .byte "THE ACE", $00
NoScreenMsg:  .byte "NO SCREEN TO DRAW ON", CHAR_CR, CHAR_LF, $00
╔══════════════════════╗
║                      ║
║       THE ACE        ║
║                      ║
╚══════════════════════╝
A box drawn from the half of the character set PRINT cannot reach. Open the full emulator

Three things in there are worth pulling out.

The check at the top. HW_PRESENT says what the machine found at power-on. Guarding a screenful of drawing with and #HW_VID costs four bytes and means the program says something sensible instead of drawing into a card that is not there. What's fitted is the whole chapter on this.

The pen. VideoSetColor takes the letter color in the high nibble and the background in the low one, so light yellow on dark blue is (TMS_LT_YELLOW * 16) | TMS_DK_BLUE. The names are all in 6502-VDP.inc. Setting the pen changes nothing already on the screen; it colors what is drawn next, and VideoClear fills the whole screen with it — which is why the program sets the pen first and clears second.

Leaving the cursor somewhere sensible. Whatever prints next carries on from wherever you left the cursor, including BASIC's own OK. Setting it to a sensible row before returning is the difference between a tidy screen and a prompt in the middle of your artwork.

A blue screen with a double-lined box drawn in pale yellow, THE ACE centered inside it, and OK below.
Every character of that frame is above 126, so PRINT cannot reach a single one of them.

A color for every character

Every cell on the screen keeps two things: the character, and the color pair it was drawn in. So a program can change the pen between one character and the next, and the screen fills up with as many colors as it likes:

asm
; A color for every character, and a color that changes everywhere at once.
;
; The pen is the pair of colors the next character is drawn in. Change it
; between lines and each line keeps the colors it was printed with. The palette
; is what those color numbers mean, so changing one entry recolors every
; character drawn with it, on the spot.

.setcpu "65C02"

.include "6502-VDP.inc"

.segment "CODE"

BasicStartup:
  .byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00

ORANGE_R  = $0F                 ; a palette entry is %0000RRRR ...
ORANGE_GB = $80                 ; ... then %GGGGBBBB

Color := $40

Start:
  jsr KernalVersion             ; A = major version
  cmp #2
  bcc NoCard                    ; before 2.0 there is no palette to change
  jsr VdpInfo                   ; carry set: no 6502-PICOVDP
  bcs NoCard

  lda #(TMS_WHITE << 4) | TMS_BLACK
  jsr VideoSetColor             ; white on black, and a black border
  jsr VideoClear                ; the whole screen in that pen

  lda #<Title
  ldy #>Title
  jsr PrintStr

  lda #TMS_MED_GREEN            ; every color from 2 to 15, on black
  sta Color
@line:
  lda Color
  asl a
  asl a
  asl a
  asl a                         ; the color as the foreground nibble
  ora #TMS_BLACK
  jsr VideoSetColor             ; only what prints from here on
  lda Color
  asl a
  tax
  lda Names,x
  ldy Names+1,x
  jsr PrintStr
  inc Color
  lda Color
  cmp #16
  bne @line

  lda #(TMS_WHITE << 4) | TMS_BLACK
  jsr VideoSetColor
  lda #<Change
  ldy #>Change
  jsr PrintStr
  jsr WaitKey

  ldx #TMS_MAGENTA              ; entry 13 becomes orange ...
  lda #ORANGE_R
  ldy #ORANGE_GB
  jsr VdpSetPalette             ; ... and so does every character drawn in it

  lda #<Restore
  ldy #>Restore
  jsr PrintStr
  jsr WaitKey

  jmp InitVideo                 ; row 0 of the palette back as it was

NoCard:
  lda #<NeedsCard
  ldy #>NeedsCard
  jmp PrintStr

; Wait for a key without printing it: Chrin would echo it to the screen.
WaitKey:
  jsr BufferSize                ; how many keys are waiting
  beq WaitKey
  jmp ReadBuffer                ; take one, and say nothing

Names:
  .word 0, 0
  .word MedGreen, LtGreen, DkBlue, LtBlue, DkRed, Cyan, MedRed, LtRed
  .word DkYellow, LtYellow, DkGreen, Magenta, Gray, White

Title:     .byte "EVERY LINE IN ITS OWN PEN", CHAR_CR, CHAR_LF, CHAR_CR, CHAR_LF, $00
MedGreen:  .byte " 2 MEDIUM GREEN", CHAR_CR, CHAR_LF, $00
LtGreen:   .byte " 3 LIGHT GREEN", CHAR_CR, CHAR_LF, $00
DkBlue:    .byte " 4 DARK BLUE", CHAR_CR, CHAR_LF, $00
LtBlue:    .byte " 5 LIGHT BLUE", CHAR_CR, CHAR_LF, $00
DkRed:     .byte " 6 DARK RED", CHAR_CR, CHAR_LF, $00
Cyan:      .byte " 7 CYAN", CHAR_CR, CHAR_LF, $00
MedRed:    .byte " 8 MEDIUM RED", CHAR_CR, CHAR_LF, $00
LtRed:     .byte " 9 LIGHT RED", CHAR_CR, CHAR_LF, $00
DkYellow:  .byte "10 DARK YELLOW", CHAR_CR, CHAR_LF, $00
LtYellow:  .byte "11 LIGHT YELLOW", CHAR_CR, CHAR_LF, $00
DkGreen:   .byte "12 DARK GREEN", CHAR_CR, CHAR_LF, $00
Magenta:   .byte "13 MAGENTA", CHAR_CR, CHAR_LF, $00
Gray:      .byte "14 GRAY", CHAR_CR, CHAR_LF, $00
White:     .byte "15 WHITE", CHAR_CR, CHAR_LF, $00
Change:    .byte CHAR_CR, CHAR_LF, "PRESS A KEY TO CHANGE COLOR 13", CHAR_CR, CHAR_LF, $00
Restore:   .byte "PRESS A KEY TO PUT IT BACK", CHAR_CR, CHAR_LF, $00
NeedsCard: .byte "NEEDS BIOS 2 AND A 6502-PICOVDP", CHAR_CR, CHAR_LF, $00
Fourteen lines in fourteen pens. Press a key, and every character drawn in color 13 changes at once. Open the full emulator
A black screen listing the colors 2 to 15 by name, each line in its own color, with line 13 shown in orange, and two lines asking for a key.
After the first key: nothing on the screen was redrawn, and line 13 is orange anyway.

The border follows the pen. The strip around the text takes the background nibble of the last VideoSetColor, so a program that sets white on black gets a black frame to go with it. The card's register 7 cannot be read back, so the Kernal keeps the border in VID_BORDER — which is how the border you asked for is still there after a program has been off in another screen layout and come back, and why a screen brought up by a COLOR with a border of its own comes up in that border rather than flashing the old one first.

A color number is a palette entry. The card doesn't store "magenta" in a cell; it stores 13, and looks 13 up in its palette every time it draws a line. VdpSetPalette rewrites an entry — X is the entry, A and Y the red, then the green and blue, four bits each — and everything drawn with that number changes on the next line the card draws. That is what the program does to line 13.

InitVideo puts the colors back. It restores the first sixteen palette entries, and the border from VID_BORDER, along with everything else about text mode — but it doesn't clear the screen, so the program's last instruction returns the orange line to magenta without losing a character.

The colors

These are the first sixteen of the card's 256, the ones text mode uses and the ones the TMS_* names in the include refer to. Color 0 is the one to be careful with: text mode draws it as black, but in a graphics layer it is usually transparent, letting whatever is behind show through.

#NameConstant
0TransparentTMS_TRANSPARENT
1BlackTMS_BLACK
2Medium greenTMS_MED_GREEN
3Light greenTMS_LT_GREEN
4Dark blueTMS_DK_BLUE
5Light blueTMS_LT_BLUE
6Dark redTMS_DK_RED
7CyanTMS_CYAN
8Medium redTMS_MED_RED
9Light redTMS_LT_RED
10Dark yellowTMS_DK_YELLOW
11Light yellowTMS_LT_YELLOW
12Dark greenTMS_DK_GREEN
13MagentaTMS_MAGENTA
14GrayTMS_GRAY
15WhiteTMS_WHITE

0 and 1 look the same — text mode draws color 0 as black. In a graphics layer, 0 is usually transparent instead.

The character set

The 256 glyphs are the IBM code page 437 set: letters, digits, punctuation, box drawing, blocks, arrows, card suits, a handful of Greek. They are not in the ROM. The card carries them in its firmware and copies them into its own memory, at $0800 of the card's 64 KB, and text mode draws from that copy: eight bytes per character, one byte per row, most significant bit on the left, and the top six bits of each byte inside the 6 × 8 cell.

Which means you can change the character set — the classic text-mode trick. Rewrite the eight bytes of a character you never use, and every place that character appears on screen becomes your shape. VdpPoke writes one byte of the card's memory, X and Y the address:

asm
PATTERNS = $0800                ; where text mode keeps the glyphs, in the card

  ldy #0
Copy:
  phy
  tya
  clc
  adc #<(PATTERNS + '*' * 8)    ; the eight bytes that draw a '*'
  tax
  lda MyShape,y
  ldy #>(PATTERNS + '*' * 8)
  jsr VdpPoke
  ply
  iny
  cpy #8
  bne Copy

MyShape:
  .byte %01111000
  .byte %11111100
  .byte %10110100
  .byte %11111100
  .byte %10000100
  .byte %11001100
  .byte %01111000
  .byte %00000000

Draw the shape in the source and you can see it while you type it — six pixels wide, with the two low bits of every byte left clear.

Put it back when you're done

InitVideo has the card copy its own set in again and restores text mode, which makes it the one-line undo for any amount of character-set vandalism. BASIC doesn't call it for you when your program returns, so call it before you do, or the OK prompt will be written in your shapes.

How the screen scrolls

When the cursor runs off the bottom row, nothing is copied. The card can scroll its picture in hardware, so the Kernal moves the whole text layer up by eight pixels — one register write — and clears the row that comes into view at the bottom. That makes a scroll a few hundred cycles instead of the tens of thousands a copy would cost.

The catch is only for a program that writes the card's memory directly. The top row of the screen is not always the top row of the table behind it: VID_TOP ($0392) says which table row is showing at the top, so the cell at a screen position is at

((row + VID_TOP) mod 24) × 40 + column

The Kernal's own calls already account for it — VideoSetCursor and VideoGetCursor speak screen rows — so a program that draws through them never needs to know.

Talking to the card directly

The card has two complete pairs of addresses:

DataCommand and status
Port AVC_DATA $9C00VC_REG / VC_STATUS $9C01
Port BVC_DATA2 $9C02VC_REG2 / VC_STATUS2 $9C03

Each pair has its own pointer into the card's memory, and the Kernal and BASIC only ever use port A. Everything is two writes to the command address — a byte, then a command:

Second writeDoes
%1rrrrrrrVC_REG_WRITE + registerWrites the first byte into register r, 0–127
%01aaaaaaVC_ADDR_WRITE + address high bitsPoints the port at an address, for writing
%00aaaaaaPoints it at an address, for reading
asm
SetReg:                         ; A = value, X = register (VC_REG_*)
  sta VC_REG
  txa
  ora #VC_REG_WRITE
  sta VC_REG
  rts

After an address, every read or write of the data port moves on by one. The command carries only fourteen bits of address, so the top two — for anything at $4000 or above — come from the VBANK register, which you set first.

Two registers belong to both ports

VBANK and VINC (the step the pointer moves by) are shared between the ports, and every Kernal routine expects them at 0 and +1. A program that changes either puts it back before it prints anything, or calls InitVideo, which does.

The two pairs exist so that an interrupt handler never has to fight the foreground. A handler that uses port B cannot land between the two writes of a command on port A, so no sei and cli are needed around video work. Interrupts has a handler that does it.

Reading a status address returns one of sixteen status registers, chosen by STATSEL_A or STATSEL_B; the Kernal's VdpStatus reads one for you. A read also resets that port's first-write-or-second flag, which is the way to get back in step if you are ever unsure.

Next: the graphics modes.

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