Writing to video memory

For most things to do with graphics, we need to write to video memory, so learning how to do it is one of the basic steps you need to learn. This page explains that, and other pages will refer here in case you need it later.

What we need

Before we begin, we need some of the constants that were explained in the page about setting up the video hardware. Two important addresses are the VDP control and data ports, which is where we write to do everything:

VdpCtrl:  equ $C00004  ; VDP control port
VdpData:  equ $C00000  ; VDP data port

But also importantly, when we cleared video memory we defined a few constants that were used to write to memory in the first place, we're gonna need them:

VRAM_ADDR_CMD:  equ $40000000
CRAM_ADDR_CMD:  equ $C0000000
VSRAM_ADDR_CMD: equ $40000010

Setting the address

Fixed addresses

To tell the VDP into which address (and which memory) we want to write, we need to send a 32-bit value (a longword) to VdpCtrl. The value must be as follows:

Written as a calculation it becomes the following mess:

((address AND $3FFF) << 16) OR ((address AND $C000) >> 14) OR constant

Doing this manually every time would be annoying, so we're gonna wrap this neatly into macros (also note the use of a generic macro that all the other macros call, to make it even easier):

SetXramAddr: macro addr, cmd
    move.l  #(((addr)&$3FFF)<<16))|(((addr)&$C000)>>14)|(cmd), (VdpCtrl)
    endm

SetVramAddr: macro addr
    SetXramAddr addr, VRAM_ADDR_CMD
    endm

SetCramAddr: macro addr
    SetXramAddr addr, CRAM_ADDR_CMD
    endm

SetVsramAddr: macro addr
    SetXramAddr addr, VSRAM_ADDR_CMD
    endm

To set to VRAM address 1234 (for example):

    SetVramAddr 1234

Variable addresses

What if the address isn't fixed? Say we want to make a generic routine to load data into video memory (e.g. tiles), it won't know ahead of time what address it has to write to. Worse yet, if you try to do it like in the macros above but using registers, the resulting code will be an awful mess.

Thankfully, there's a neat trick: bits 13-0 are left in the first word while bits 15-14 are in the second word. A register contains two words. The trick here is that a longword bit shift will affect the whole register, but a word bit shift will only affect the low word (and leave the other word alone). So the idea is:

  1. Clear higher word if needed
  2. Push bits 15-14 into the high word using a longword shift
  3. Push bits 13-0 back in place using a word shift
  4. Use swap to flip the words into correct order
  5. Do the rest

The result is like follows:

; The following modifies the register with
; the address (one of d0-d7), but nothing else

SetXramAddrReg: macro reg, cmd
    and.l   #$FFFF, reg
    lsl.l   #2, reg
    lsr.w   #2, reg
    swap    reg
    or.l    #cmd, reg
    move.l  reg, (VdpCtrl)
    endm

SetVramAddrReg: macro reg
    SetXramAddrReg reg, VRAM_ADDR_CMD
    endm

SetCramAddrReg: macro reg
    SetXramAddrReg reg, CRAM_ADDR_CMD
    endm

SetVsramAddrReg: macro reg
    SetXramAddrReg reg, VSRAM_ADDR_CMD
    endm

Then you could do stuff like:

    move.w  #1234, d0
    SetVramAddrReg d0

Writing the data

Now that we've set the address we need to write the data into video memory. This part is easier: write everything to VdpData (do not increment the address, i.e. always write to the same location). Note that you must write words or longwords (not bytes).

Simple example to copy arbitrary data to VRAM:

; d0.w = VRAM address
; d1.w = number of words (*not* bytes)
; a0.l = pointer to data

CopyToVram:
    ; Tell VDP where we'll write
    SetVramAddrReg  d0
    
    ; Copy the data to VRAM
    ; Note the substract because DBF
    ; stops at -1 instead of 0 and
    ; how we do *not* increment a1
    subq.w  #1, d1
    blo.s   @End
    lea     (VdpData), a1
@Loop:
    move.w  (a0)+, (a1)
    dbf     d1, @Loop
    
@End:
    ; We're done
    rts

Further optimization tips: