SEGA Genesis: Scrolling

The SEGA Genesis has dedicated scrolling hardware, with different scroll modes. In this post, I'll show how to do vertically synchronized scrolling on the Genesis.

Scroll Modes

Line scroll enables awesome parallax effects
Full screen scrolling in Mega Turrican

In general, scrolling is the result of changing an offset of some displayed graphics.

The Genesis VDP has support for both vertical and horizontal scrolling, and they seem to be handled in different ways, but here I will only focus on horizontal scrolling. Horizontal scrolling can be performed in 3 different modes on the Genesis:

The full screen scroll is simple enough, every pixel is offset horizontally with the same amount (see the Mega Turrican GIF for an example). It only requires updating one number every frame, the offset.

The line scroll is more powerful, as every line on the screen can be offset with a different value. This allows for some crazy pseudo-3D effects, like the one parallax background scroll in Thunder Force IV.

The cell scroll is similar to the line scroll, but instead of every line being offset with a potentially different value, the offsets are 8 lines apart, requiring less data to be updated every frame.

Full Screen Scrolling

Firstly, I wanted to just scroll in full screen mode, incrementing a horizontal scroll offset every frame. To do this, we:

The two first items require setting up VDP registers.

Scroll Mode

The full screen scroll mode is located in VDP register $0B. It has a few bits that I'll ignore for this example, and just set the two horizontal scroll bits. They can have these binary values:

For this example, we want to enable 'Full screen scroll' and set all other bits to 0, which just yields a bit fat 0:

0000 0000

So, we end up with setting the VDP register $0B to 0. As explained earlier, VDP registers are set using this bit pattern:

Bits    [10?R RRRR DDDD DDDD]
R order [...4 3210 .... ....] register
D order [.... .... 7654 3210] data
VDP register $0B (0 1011) = $0 (0000 0000)
Bits: [10.. .... .... ....]
      [10.0 1011 .... ....] register $0B
      [10.0 1011 0000 0000] data
      [1000 1011 0000 0000] add zeroes
Hex:      8    B    0    0

To set the horizontal scroll mode to full screen scroll, we end up with this line:

move.w #$8B00,vdp_control

Next, let's set the scroll offset.

Scroll Offset Address

Biomechanic monstrocity from Wings of Wor

The horizontal scroll offset is just a single number in our example, but could be a table with entries for each line, if we were using line scroll mode. We tell the VDP of where our number is located by writing the address to VDP register $0D.

I arbitrarily picked the VRAM address $D000, so we want to 'set VDP register $0D = $D000'. Scroll tables addresses must be a multiple of $400, and is specified using only the 6 most significant bits:

Bits: [?MAA AAAA]
- ? is ignored (set to 0)
- M enables 128 KB mode (set to 0)
- A is bits 15-10 of the address

We start out with this:

[.0.. ....]

And our address $D000, which in binary is 1101000000000000, is specified using the 6 most significant bits: 110100. Like this:

[.011 0100]

Add a zero, and now we have our data byte:

[0011 0100]
    3    4

We can now make the full command word:

Bits    [10?R RRRR DDDD DDDD]
R order [...4 3210 .... ....] register
D order [.... .... 7654 3210] data
VDP register $0D (0 1101) = $34 (0011 0100)
Bits: [10.. .... .... ....]
      [10.0 1101 .... ....] register $0D
      [10.0 1101 0011 0100] data = $34
      [1000 1101 0011 0100] add zeroes
Hex:      8    D    3    4

So, to set the scroll table address to $D000, we use:

move.w #$8D34,vdp_control

Writing Scroll Table to VRAM

So far, all I did was update the VDP registers. In order to actually update the scroll offset, I have to update the contents of VRAM address $D000. We do this like explained in an earlier post:

Bits    [BBAA AAAA AAAA AAAA 0000 0000 BBBB 00AA]
B order [10.. .... .... .... .... .... 5432 ....] oper. type
A order [..DC BA98 7654 3210 .... .... .... ..FE] address
VRAM Write (00 0001) to addr $D000 (1101 0000 0000 0000):
        [01.. .... .... .... .... .... 0000 ....] oper. type
        [0101 0000 0000 0000 .... .... 0000 ..11] VRAM address
        [0101 0000 0000 0000 0000 0000 0000 0011] add zeroes
  Hex:      5    0    0    0    0    0    0    3

Now we have our 32-bit command word, and can write it to the VDP Control Port to set up our VRAM write:

vdp_control = $C00004
move.l #$50000003,vdp_control   ; Set up VDP write to VRAM address $D000

Now that the VRAM write operation is set up, we can write the scroll offset to vdp_data:

move.w #0,vdp_data            ; scroll offset = 0

Smooth Scrolling

The final code was ridiculously short compared to the work that went into it:

vdp_control = $C00004
vdp_data    = $C00000

    ; Full screen scroll mode
    move.w #$8B00,vdp_control
    ; Scroll data is in $D000
    move.w #$8D43,vdp_control

    ; do stuff...

    jsr WaitVBlankEnd

    ; Set up VDP write to VRAM address $D000
    move.l #$50000003,vdp_control
    move.w d4,vdp_data     ; write d4 to VDP
    add.w  #1,d4        ; d4 (x scroll) += 1

    jmp MainLoop

Bug Hunting

While working on the scrolling code, I had accidentally written:

move.w #$8D43,vdp_control

and had trouble figuring out what the problem was. I had previously tested and debugged using mednafen and MAME, but ended up installing yet another Genesis emulator, . It had a VDP debug view that was very informative, it showed the VDP registers, and I could easily see that horizontal scroll address was set to $10C00, not $D000.

How did $10C00 come into this?

Well, $34 was the 6 most significant bits of the number $D000, or what you get if you divide $D000 with $400. If we divide $10C00 with $400, we get $43, which I had accidentally written instead of $34.