Skip to content
Techniques & Technology

Hardware Scrolling

Smooth movement without moving data

Hardware scroll registers let the display shift smoothly pixel-by-pixel without copying memory—essential for fast, smooth side-scrolling games.

C64AmigaNES graphicsscrollingoptimisation 1977–present

Overview

Moving a game world pixel-by-pixel by copying screen memory is impossibly slow on 8-bit hardware. Hardware scrolling solves this: dedicated registers shift the display position, and you only update the newly-revealed edge. The result is butter-smooth scrolling at full frame rate.

Commodore 64

The VIC-II provides smooth scroll registers:

Horizontal scroll (0-7 pixels)

; $D016 bits 0-2 = X scroll offset
scroll_x:   .byte 0

smooth_scroll_right:
    dec scroll_x
    lda scroll_x
    and #$07
    sta scroll_x
    ora $d016
    and #$f8
    ora scroll_x
    sta $d016

    ; When scroll_x wraps from 0 to 7,
    ; shift screen memory left by 1 character
    lda scroll_x
    cmp #$07
    bne .no_coarse
    jsr shift_screen_left
.no_coarse:
    rts

Vertical scroll (0-7 pixels)

; $D011 bits 0-2 = Y scroll offset
scroll_y:   .byte 0

smooth_scroll_down:
    inc scroll_y
    lda scroll_y
    and #$07
    sta scroll_y

    lda $d011
    and #$f8
    ora scroll_y
    sta $d011

    ; When scroll_y wraps, shift screen up
    lda scroll_y
    bne .no_coarse
    jsr shift_screen_up
.no_coarse:
    rts

Colour RAM challenge

VIC-II colour RAM doesn’t scroll with screen memory. Solutions:

  • Use same colour for scrolling area
  • Copy colour RAM along with screen (slow)
  • Use character colour (bits 8-11 of character data in multicolour mode)

NES

The NES has excellent scroll hardware:

Scroll registers

; Write scroll position during VBlank
set_scroll:
    lda scroll_x
    sta $2005           ; X scroll
    lda scroll_y
    sta $2005           ; Y scroll
    rts

Nametable switching

The NES has 2-4 nametables. Scrolling spans them:

; $2000 bits 0-1 select base nametable
; Combined with $2005, allows seamless scrolling

    lda #%00000000      ; nametable 0
    sta $2000
    ; or
    lda #%00000001      ; nametable 1
    sta $2000

Mid-screen scroll changes

Using sprite 0 hit or mapper IRQ:

; Status bar at top (no scroll)
    lda #0
    sta $2005
    sta $2005

; Wait for sprite 0 hit
.wait:
    lda $2002
    and #$40
    beq .wait

; Game area (scrolled)
    lda game_scroll_x
    sta $2005
    lda game_scroll_y
    sta $2005

Amiga

The Amiga scrolls by adjusting bitplane pointers:

Coarse scroll (16-pixel steps)

; Change BPLxPT to scroll whole words
scroll_coarse:
    ; Move display start forward by 2 bytes (16 pixels)
    add.l   #2,bitplane_ptr
    move.l  bitplane_ptr,bpl1pt
    ...

Fine scroll (0-15 pixels)

; BPLCON1 contains scroll delay values
; Bits 0-3: playfield 1 scroll
; Bits 4-7: playfield 2 scroll

scroll_fine:
    move.w  scroll_x,d0
    and.w   #$0f,d0         ; 0-15
    move.w  d0,$dff102      ; BPLCON1

Dual playfield parallax

; Playfield 1 scrolls at full speed
; Playfield 2 scrolls at half speed

update_scroll:
    add.w   #2,pf1_scroll   ; fast
    add.w   #1,pf2_scroll   ; slow

    move.w  pf1_scroll,d0
    and.w   #$0f,d0
    move.w  pf2_scroll,d1
    and.w   #$0f,d1
    lsl.w   #4,d1
    or.w    d1,d0
    move.w  d0,$dff102

Coarse scroll techniques

When smooth scroll wraps, the screen must shift:

Character-based (C64)

; Shift screen left by 1 character
shift_screen_left:
    ldx #0
.loop:
    lda $0401,x         ; source (one char right)
    sta $0400,x         ; destination
    inx
    cpx #39             ; 40 columns - 1
    bne .loop

    ; Draw new column at right edge
    jsr draw_right_column
    rts

Tile-based (NES)

; Update single column of nametable
; Done gradually during gameplay, not all at once
update_column:
    lda column_to_update
    ; Set VRAM address
    ; Write 30 tiles (one column)
    ; Spread across multiple frames if needed

Bidirectional scrolling

Scrolling both directions requires:

  • Tracking scroll position (potentially >256)
  • Buffer around visible area
  • Updating both leading edges
; 16-bit scroll position
scroll_x_lo:    .byte 0
scroll_x_hi:    .byte 0

update_scroll:
    lda direction
    beq .scroll_right

.scroll_left:
    ; Decrement scroll, wrap at 0
    ...
    jsr update_left_edge
    rts

.scroll_right:
    ; Increment scroll, wrap at map width
    ...
    jsr update_right_edge
    rts

Performance comparison

MethodSpeedMemorySmoothness
Software scrollVery slowLowDepends
Hardware scrollFastMediumExcellent
Page flippingMediumHighGood

See also