Skip to content

Smooth Scrolling

Combine VIC-II fine offsets with a complete character and colour shift. Prepare the next column and budget the costly coarse step.

Code status: maintained routine, executed on a ROM-booted PAL C64 emulator. The check observes initial fine scroll, eight steps, all 1,000 screen cells and all 1,000 colour nibbles. It does not prove tear-free presentation or a frame budget.

Audit execution record · Scope and remaining checks

A pixel at a time, then a whole column

The VIC-II’s fine offset moves the display within a character cell. Once those eight positions run out, the character map must move too. Otherwise the world jumps back instead of continuing to scroll.

Include scroll.inc once, call init_scroll, then call scroll_left once per chosen update. It applies the initial offset immediately, uses 38-column mode to hide the side edges, and preserves the other $D016 bits. Prepare 25 bytes in next_column and 25 colours in next_colour before each coarse step; those become column 39.

Fine steps and complete screen/colour shift

scroll.inc
; =============================================================================
; SMOOTH SCROLLING - C64 (horizontal, leftward)
; Fine-scroll 7->0 with $D016; on wrap, coarse-shift the map one column
; CPU: Light per frame; heavy on the 1-in-8 coarse step
; =============================================================================

VIC_CTRL2   = $D016     ; bits 0-2 = X fine scroll (0-7); bit 3 = 40/38 columns
SCREEN      = $0400

scroll_x    = $FB       ; a free zero-page byte: our shadow of the fine offset

init_scroll:
    lda VIC_CTRL2
    and #%11110111      ; bit 3 = 0 -> 38-column mode (hides the edge characters)
    sta VIC_CTRL2
    lda #7
    sta scroll_x
    jmp set_fine

; Call once a frame (after raster sync) to scroll the world left by one pixel
scroll_left:
    dec scroll_x
    bpl set_fine        ; still 0..7? just update the fine offset
    lda #7              ; passed 0: a whole character has gone by
    sta scroll_x
    jsr coarse_left     ; shift the map one column left, fill the new right edge
set_fine:
    lda VIC_CTRL2
    and #%11111000      ; clear the low 3 bits
    ora scroll_x        ; set the new fine-scroll offset
    sta VIC_CTRL2
    rts

; Caller owns screen $0400-$07e7, colour $d800-$dbe7, ZP $fb,
; and next_column/next_colour (25 bytes each). Colour values use low four bits.
; Prepare the next right-hand column before the wrap step. No map streamer here.
; A, X, flags change; Y survives. Decimal mode must be clear. IRQ/NMI code
; must not change the owned state. This copies 1950 bytes on the coarse step:
; raster synchronisation alone does NOT make it tear-free or fit a frame budget.
; Use a smaller playfield or a separately budgeted buffer scheme when needed.
coarse_left:
    ldx #0
copy_columns:
    !for row,0,24 {
        lda SCREEN+row*40+1,x
        sta SCREEN+row*40,x
        lda $d800+row*40+1,x
        sta $d800+row*40,x
    }
    inx
    cpx #39
    beq fill_column
    jmp copy_columns
fill_column:
    !for row,0,24 {
        lda next_column+row
        sta SCREEN+row*40+39
        lda next_colour+row
        sta $d800+row*40+39
    }
    rts
next_column: !fill 25,32
next_colour: !fill 25,1

Integration contract

This version owns zero-page $FB, screen RAM $0400–$07E7, colour RAM $D800–$DBE7, and the two 25-byte column buffers. Your VIC bank and $D018 must display that screen. Use text mode with matching colour semantics. Clear decimal mode before calling. A, X and flags change; Y survives. Interrupts must preserve the routine’s working registers and must not change its state while it runs.

On the coarse step it copies 975 character bytes and 975 colours, then fills 25 edge cells of each. That is substantial work. Calling it after a raster wait alone does not make those writes fit safely before the beam reaches them. Budget the whole update, reduce the playfield, or use a separately designed buffering scheme. The routine supplies a correct memory shift, not an entire map streamer or a promise of smooth presentation under every workload.

Related: Raster splits, VIC-II.

Explore the cross-platform scrolling family for camera coordinates, seams and implementations on other machines.