6502 Part 4 - Custom Video Layer

4 Jan 2025

Introduction

It's been fun working with the ZX Spectrum's video layer. However, we've come across limitations where it has become cumbersome to write in. If we want to build some more advanced applications such as games, we'll need a better solution. To do this, we'll build our own video layer. It'll still be a 256x192 display, but controlled by something more akin to the NES's Pixel Processing Unit (PPU). The idea is that it will give us smoother animations and easier control over graphics in general.

Requirements

  • Graphics registers
CTRL        $2000   ---- ---M
STATUS      $2001   DDDD DDDD
OBJPTR      $2002   AAAA AAAA AAAA AAAA
GRAPTR      $2004   AAAA AAAA AAAA AAAA
BSCRLX      $2006   XXXX XXXX
BSCRLY      $2007   YYYY YYYY
WSCRLX      $2008   XXXX XXXX
WSCRLY      $2009   YYYY YYYY
  • Object memory

Object memory is 256 bytes total, comprising of an array of 64 elements, 4 bytes in size. Each consecutive 4 bytes corresponds to an object.

XPOS    1   XXXX XXXX   X position of the sprite
YPOS    2   YYYY YYYY   Y position of the sprite
ATTR    3   7654 3210   Attribute register
            |||| ||||
            |||| |+++ Palette of sprite
            |||+-+--- Selection (00: default 8x8; 01: right 4x8; 10: left 4x8; 11: double 8x16)
            ||+------ Priority (0: in front of background; 1: behind background)
            |+------- Flip sprite horizontally
            +-------- Flip sprite vertically
INDX    4   NNNN NNNN   Sprite index in the sprite table
  • Graphics memory

Graphics memory is in total 8kb, comprising of a sprite table, background table, and window array.

$0000-$0fff     $1000   Sprite table
$1000-$13ff     $0400   Background table
$1400-$14ff     $0100   Background attribute table
$1500-$18df     $02c0   Window array
$18c0-$18ff     $0040   Palettes
  • Sprite table

The sprite table is 4kb total. It's a 16x16 grid of 2x 8 byte chunks. Each byte represents a row of the sprite. The sprite table is used by both object memory and background memory.

0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f
00
10
20
30
40
50
60
70
80
90
a0
b0
c0
d0
e0
f0

The sprite itself is composed of 2x 8 byte rows. Each byte represents a row of the 8x8 sprite. The first 8 bytes represent the first bit in the palette table, the second bit represents the latter.

0   0000 0000       8   0000 0000
1   0000 0000       9   0000 0000
2   0000 0000       a   0000 0000
3   0000 0000       b   0000 0000
4   0000 0000       c   0000 0000
5   0000 0000       d   0000 0000
6   0000 0000       e   0000 0000
7   0000 0000       f   0000 0000
  • Background table

The background table is a 32x32 grid of sprites. It wraps around defined by the scroll registers.

  • Background attribute table

Defines the attributes for each background 2x2 group. An attribute is a byte that defines the palette for which that 2x2 sprite group will use.

  • Window array

TODO - out of scope of today

  • Palettes

A palette define the colours that are given to sprites. Each palette is a 4 byte sequence, where the first byte is ignored. There are 16 palettes, the first 8 are used for sprites, the second 8 are used for backgrounds.

Text

ctrl = $2000
status = $2001
objptr = $2002
graptr = $2004
bscrlx = $2006
bscrly = $2007
wscrlx = $2008
wscrly = $2009

sprite_table = $4000
background_table = $5000
background_attr_table = $5400
window_array = $5500
palettes = $58c0
object_table = $3000

web3_status = $6000
web3_request = $6001
mouse_down = $3FFD
mouse_x = $3FFE
mouse_y = $3FFF

.macro STAI address
    LDX address
    STX $00
    LDX address + 1
    STX $01
    LDY #$00
    STA ($00),Y
.end

.macro TX2 address_1, address_2
    LDA address_1
    STA address_2
    LDA address_1 + 1
    STA address_2 + 1
.end

.macro TX16 address_1, address_2
    LDA address_1
    STA address_2
    LDA address_1 + 1
    STA address_2 + 1
    LDA address_1 + 2
    STA address_2 + 2
    LDA address_1 + 3
    STA address_2 + 3
    LDA address_1 + 4
    STA address_2 + 4
    LDA address_1 + 5
    STA address_2 + 5
    LDA address_1 + 6
    STA address_2 + 6
    LDA address_1 + 7
    STA address_2 + 7
    LDA address_1 + 8
    STA address_2 + 8
    LDA address_1 + 9
    STA address_2 + 9
    LDA address_1 + 10
    STA address_2 + 10
    LDA address_1 + 11
    STA address_2 + 11
    LDA address_1 + 12
    STA address_2 + 12
    LDA address_1 + 13
    STA address_2 + 13
    LDA address_1 + 14
    STA address_2 + 14
    LDA address_1 + 15
    STA address_2 + 15
.end

.macro CMP2 address_1, address_2
    LDA address_1
    CMP address_2
    BNE done
    LDA address_1 + 1
    CMP address_2 + 1
done:
    NOP
.end

init:
    ; Set the graphics mode to 1
    LDA #$01
    STA ctrl

    ; Set the graphics pointer
    LDA #<sprite_table
    STA graptr
    LDA #>sprite_table
    STA graptr + 1

    ; Set the object pointer
    LDA #<object_table
    STA objptr
    LDA #>object_table
    STA objptr + 1

    ; Set the sprites
    ; TODO, not working after introducing this
    TX16 sprite_sheet, sprite_table
    TX16 sprite_sheet + $10, sprite_table + $10
    TX16 sprite_sheet + $20, sprite_table + $20
    TX16 sprite_sheet + $30, sprite_table + $30

    ; Set the first object
    LDA #$FD
    STA object_table ; X
    LDA #$10
    STA object_table + 1 ; Y
    LDA #%00011000
    STA object_table + 2 ; attr
    LDA #$00
    STA object_table + 3 ; sprite

    ; Set the first palette
    LDA #%01010000
    STA palettes
    LDA #%00010101
    STA palettes + 1
    LDA #%00010100
    STA palettes + 2
    LDA #%00000101
    STA palettes + 3

    ; Set the second palette
    LDA #%01101010
    STA palettes + 4
    LDA #%01111110
    STA palettes + 5
    LDA #%01110100
    STA palettes + 6
    LDA #%11000101
    STA palettes + 7

    ; Set the attribute block
    LDA #$01
    STA background_attr_table + 2
    LDA #$01
    STA background_attr_table + 19

    JSR init_background_sprite_sheet

    JMP loop

; X, Y registers as coordinates
xy_to_background_table_addr_i:
    .word $0000
xy_to_background_table_addr_result:
    .word $0000
xy_to_background_table_addr:
    ; reset
    LDA #$00
    STA xy_to_background_table_addr_i
    STA xy_to_background_table_addr_i + 1
    STA xy_to_background_table_addr_result
    STA xy_to_background_table_addr_result + 1
    ; X / 8
    TXA
    AND #%11111000
    ROR
    ROR
    ROR
    STA xy_to_background_table_addr_i
    ; first part of Y - 00111000 -> 11100000
    TYA
    AND #%00111000
    ROL
    ROL
    ORA xy_to_background_table_addr_i
    STA xy_to_background_table_addr_i
    ; second part of Y - 11000000 -> 00000011
    TYA
    AND #%11000000
    ROL
    ROL
    ROL
    STA xy_to_background_table_addr_i + 1
    ; calculate address = background table + i
    CLC
    LDA #<background_table
    ADC xy_to_background_table_addr_i
    STA xy_to_background_table_addr_result
    LDA #>background_table
    ADC xy_to_background_table_addr_i + 1
    STA xy_to_background_table_addr_result + 1
    RTS

init_background_sprite_sheet_i:
    .byte $00
init_background_sprite_sheet:
    ; iterate 256 - 0 to 255
    LDA #$00
    STA init_background_sprite_sheet_i
init_background_sprite_sheet_loop:
    ; calculate X
    LDA init_background_sprite_sheet_i
    AND #%00001111
    ROL
    ROL
    ROL
    TAX
    ; calculate Y
    LDA init_background_sprite_sheet_i
    AND #$F0
    ROR
    TAY
    JSR xy_to_background_table_addr
    TX2 xy_to_background_table_addr_result, $00
    ; store tile
    LDA init_background_sprite_sheet_i
    LDY #$00
    STA ($00),Y
    INC init_background_sprite_sheet_i
    BNE init_background_sprite_sheet_loop
    RTS

draw_sprite_to_background_sprite:
    .byte $00
draw_sprite_to_background_x:
    .byte $00
draw_sprite_to_background_y:
    .byte $00
draw_sprite_to_background_byte_a:
    .byte $00
draw_sprite_to_background_byte_b:
    .byte $00
draw_sprite_to_background_row:
    .byte $00
draw_sprite_to_background:
    ; init
    LDA #$00
    STA draw_sprite_to_background_row
draw_sprite_to_background_loop:
    ; use ZP for indexing the sprite table
    LDA #<sprite_table
    STA $00
    LDA #>sprite_table
    STA $01

    ; load sprite row
    LDY draw_sprite_to_background_row
    LDA ($00),Y
    STA draw_word_to_background_word
    TYA
    CLC
    ADC #$08
    TAY
    LDA ($00),Y
    STA draw_word_to_background_word + 1

    ; the word
    LDA draw_sprite_to_background_x
    STA draw_word_to_background_x
    LDA draw_sprite_to_background_y
    STA draw_word_to_background_y
    JSR draw_word_to_background

    ; iterate
    CLC
    LDA draw_sprite_to_background_y
    ADC #$08
    STA draw_sprite_to_background_y
    INC draw_sprite_to_background_row
    LDA draw_sprite_to_background_row
    CMP #$08
    BNE draw_sprite_to_background_loop

    RTS

draw_word_to_background_word:
    .word $0000
draw_word_to_background_x:
    .byte $00
draw_word_to_background_y:
    .byte $00
draw_word_to_background_i:
    .byte $08
draw_word_to_background:
    ; reset
    LDA #$08
    STA draw_word_to_background_i
    CLC
    LDA #$38
    ADC draw_word_to_background_x
    STA draw_word_to_background_x
draw_word_to_background_loop:
    ; calculate pixel
    LDA draw_word_to_background_word
    CLC
    AND #$01
    ROL
    STA draw_pixel_to_background_pixel
    LDA draw_word_to_background_word + 1
    AND #$01
    ORA draw_pixel_to_background_pixel
    ; invoke drawing
    STA draw_pixel_to_background_pixel
    LDA draw_word_to_background_x
    STA draw_pixel_to_background_x
    LDA draw_word_to_background_y
    STA draw_pixel_to_background_y
    JSR draw_pixel_to_background
    ; iterate backwards
    SEC
    LDA draw_word_to_background_x
    SBC #$08
    STA draw_word_to_background_x
    CLC
    LDA draw_word_to_background_word
    ROR
    STA draw_word_to_background_word
    CLC
    LDA draw_word_to_background_word + 1
    ROR
    STA draw_word_to_background_word + 1
    DEC draw_word_to_background_i
    BNE draw_word_to_background_loop
    RTS

; draw pixel to background, byte corresponds to colour
draw_pixel_to_background_pixel:
    .byte $00
draw_pixel_to_background_x:
    .byte $00
draw_pixel_to_background_y:
    .byte $00
draw_pixel_to_background:
    LDX draw_pixel_to_background_x
    LDY draw_pixel_to_background_y
    JSR xy_to_background_table_addr
    TX2 xy_to_background_table_addr_result, $00
    LDY #$00
    LDA draw_pixel_to_background_pixel
    STA ($00),Y
    RTS

; TODO - draw a sprite
; TODO - sprite editor
; TODO - background editor
; TODO - palette editor

fine_scroll_x:
    .byte $00, $00, $00

loop:
    ; draw a sprite
    LDA #$00
    STA draw_sprite_to_background_sprite
    LDA #$90
    STA draw_sprite_to_background_x
    LDA #$00
    STA draw_sprite_to_background_y
    JSR draw_sprite_to_background

    ; increment x scroll register
    ; increment y scroll register
    CLC
    LDA #$01
    ADC fine_scroll_x
    STA fine_scroll_x
    LDA #$00
    ADC fine_scroll_x + 1
    STA fine_scroll_x + 1
    LDA #$00
    ADC fine_scroll_x + 2
    STA fine_scroll_x + 2
    LDA fine_scroll_x + 1
    STA bscrlx
    STA bscrly

    JMP loop

sprite_sheet:
    ; sprite 1
    .byte %11111111
    .byte %10000001
    .byte %10000001
    .byte %10000001
    .byte %00000001
    .byte %10000001
    .byte %10000001
    .byte %11111111

    .byte %11111111
    .byte %11110111
    .byte %10110011
    .byte %11111111
    .byte %11101111
    .byte %10111111
    .byte %11111101
    .byte %11111111

    ; sprite 2
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000

    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111

    ; sprite 3
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111

    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000
    .byte %00000000

    ; sprite 4
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111

    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111
    .byte %11111111