Skip to content
Game 1 Unit 3 of 128 1 hr learning time

Draw Terrain

Filled rectangles build a landscape in the bitplane. A subroutine draws byte-aligned rectangles. Three calls create ground, a cliff, and a platform.

2% of Exodus

Unit 2 filled everything below a line with terrain — one flat horizon. Real landscapes have shape: high ground, low ground, ledges, gaps. This unit draws terrain directly into the bitplane using a reusable subroutine.

The CPU writes rectangles of set bits into the bitplane. Where bits are 1, COLOR01 (terrain brown) appears. Where they’re 0, the sky gradient shows through. Three rectangles build a landscape with a cliff face and a floating platform.

Exodus Unit 3

Low ground on the left. High ground on the right, forming a cliff face where the two levels meet. A floating platform hangs above the step. This is the start of a real terrain puzzle — later, creatures will need to navigate these features.

Subroutines: BSR and RTS

Until now, all code ran in a straight line from top to bottom. But drawing three rectangles with the same logic means repeating the same code three times — or using a subroutine.

BSR (Branch to Subroutine) pushes the return address onto the stack and jumps to a label. RTS (Return from Subroutine) pops that address and jumps back. The calling code resumes exactly where it left off:

            bsr     draw_rect       ; Jump to draw_rect, push return address
            ; ...execution continues here after RTS

This is the 68000’s equivalent of a function call. Parameters go in registers before the call. The subroutine does its work and returns.

The draw_rect Subroutine

;──────────────────────────────────────────────────────────────
; draw_rect — Fill a byte-aligned rectangle in the bitplane
;
; Input:  D0.W = x position (must be multiple of 8)
;         D1.W = y position
;         D2.W = width in pixels (must be multiple of 8)
;         D3.W = height in pixels
; Destroys: D0-D5, A0-A1
;──────────────────────────────────────────────────────────────
draw_rect:
            ; Calculate starting address: bitplane + y*40 + x/8
            lea     bitplane,a0
            mulu    #BYTES_PER_ROW,d1   ; D1 = y * 40 (32-bit result)
            add.l   d1,a0               ; A0 += y offset
            lsr.w   #3,d0               ; D0 = x / 8
            add.w   d0,a0               ; A0 += x offset

            ; Convert width from pixels to bytes
            lsr.w   #3,d2               ; D2 = width / 8

            ; Outer loop: rows
            subq.w  #1,d3               ; Height - 1 for DBRA
.row:
            move.w  d2,d5
            subq.w  #1,d5               ; Width_bytes - 1 for DBRA
            move.l  a0,a1               ; A1 = row write pointer
.col:
            move.b  #$ff,(a1)+          ; Fill 8 pixels (one byte)
            dbra    d5,.col

            add.w   #BYTES_PER_ROW,a0   ; Advance to next row
            dbra    d3,.row
            rts

The subroutine takes four parameters in D0-D3: x position, y position, width, and height. All coordinates are in pixels, but x and width must be multiples of 8 (byte-aligned) to keep the drawing simple.

Address calculation — the starting byte in the bitplane is y × 40 + x ÷ 8. MULU (Multiply Unsigned) computes y × BYTES_PER_ROW as a 32-bit result. LSR.W #3 shifts right by 3 bits, which divides by 8 — converting pixel positions to byte offsets.

Nested loops — the outer loop counts rows, the inner loop fills bytes across one row. Each byte written as $FF sets 8 pixels to 1. After each row, ADD.W #BYTES_PER_ROW,A0 advances the row pointer by 40 bytes to the next line.

DBRA in both loops — the inner loop uses D5 as a temporary copy of the width (in bytes), so the width value survives for the next row. The outer loop counts down in D3 (height). Both registers are pre-decremented by 1 because DBRA loops count+1 times.

Building the Landscape

Three calls to draw_rect build the terrain:

            ; --- Draw terrain into bitplane ---
            ; Bitplane starts as zeros (sky everywhere).
            ; Each draw_rect call fills a rectangle with 1s (terrain).

            ; Low ground (left side)
            move.w  #GROUND_L_X,d0
            move.w  #GROUND_L_Y,d1
            move.w  #GROUND_L_W,d2
            move.w  #GROUND_L_H,d3
            bsr     draw_rect

            ; High ground (right side — cliff face)
            move.w  #GROUND_R_X,d0
            move.w  #GROUND_R_Y,d1
            move.w  #GROUND_R_W,d2
            move.w  #GROUND_R_H,d3
            bsr     draw_rect

            ; Floating platform
            move.w  #PLATFORM_X,d0
            move.w  #PLATFORM_Y,d1
            move.w  #PLATFORM_W,d2
            move.w  #PLATFORM_H,d3
            bsr     draw_rect

Each call loads four values into D0-D3 and branches to the subroutine. The rectangles overlap where they need to — the low ground and high ground share the bottom of the screen.

The terrain parameters are defined as constants at the top of the file, making them easy to tweak:

; Low ground (left side)
GROUND_L_X          equ 0
GROUND_L_Y          equ 152
GROUND_L_W          equ 128
GROUND_L_H          equ 104

; High ground (right side — forms a cliff face)
GROUND_R_X          equ 128
GROUND_R_Y          equ 120
GROUND_R_W          equ 192
GROUND_R_H          equ 136

; Floating platform (above the step)
PLATFORM_X          equ 24
PLATFORM_Y          equ 104
PLATFORM_W          equ 72
PLATFORM_H          equ 8

Why Byte-Aligned?

The x position and width must be multiples of 8. This means every rectangle starts and ends on a byte boundary in the bitplane. Without this constraint, the first and last bytes of each row would need bit masking — setting some bits while preserving others. That’s more complex code for no visual benefit at this stage.

The Blitter can handle arbitrary pixel alignment efficiently. For now, byte-aligned rectangles keep the CPU drawing code simple and fast.

Experiment: Reshape the Terrain

Change the terrain constants to create different landscapes:

; Wide flat ground
GROUND_L_X          equ 0
GROUND_L_Y          equ 180
GROUND_L_W          equ 320
GROUND_L_H          equ 76

Or add a gap by making the left ground narrower:

GROUND_L_W          equ 96          ; Narrower — gap before cliff

Try moving the platform higher or making it wider. Every change is just a number — the subroutine handles the rest.

The Complete Code

;──────────────────────────────────────────────────────────────
; EXODUS - A terrain puzzle for the Commodore Amiga
; Unit 3: Draw Terrain
;
; Filled rectangles build a landscape in the bitplane.
; A subroutine (BSR/RTS) draws byte-aligned rectangles.
; Three calls create ground, a cliff, and a platform.
;──────────────────────────────────────────────────────────────

;══════════════════════════════════════════════════════════════
; TWEAKABLE VALUES
;══════════════════════════════════════════════════════════════

; Colours ($0RGB)
COLOUR_SKY_DEEP     equ $0016
COLOUR_SKY_UPPER    equ $0038
COLOUR_SKY_MID      equ $005B
COLOUR_SKY_LOWER    equ $007D
COLOUR_SKY_HORIZON  equ $009E

COLOUR_TERRAIN      equ $0741       ; Earth brown

; Terrain rectangles (x and width must be multiples of 8)
; Low ground (left side)
GROUND_L_X          equ 0
GROUND_L_Y          equ 152
GROUND_L_W          equ 128
GROUND_L_H          equ 104         ; Extends to bottom (152+104=256)

; High ground (right side — forms a cliff face)
GROUND_R_X          equ 128
GROUND_R_Y          equ 120
GROUND_R_W          equ 192
GROUND_R_H          equ 136         ; Extends to bottom (120+136=256)

; Floating platform (above the step)
PLATFORM_X          equ 24
PLATFORM_Y          equ 104
PLATFORM_W          equ 72
PLATFORM_H          equ 8

;══════════════════════════════════════════════════════════════
; DISPLAY CONSTANTS
;══════════════════════════════════════════════════════════════

SCREEN_WIDTH    equ 320
SCREEN_HEIGHT   equ 256
BYTES_PER_ROW   equ SCREEN_WIDTH/8      ; 40
BITPLANE_SIZE   equ BYTES_PER_ROW*SCREEN_HEIGHT  ; 10240

;══════════════════════════════════════════════════════════════
; HARDWARE REGISTERS
;══════════════════════════════════════════════════════════════

CUSTOM      equ $dff000
DMACON      equ $096
INTENA      equ $09a
INTREQ      equ $09c
COP1LC      equ $080
COPJMP1     equ $088
BPLCON0     equ $100
BPLCON1     equ $102
BPLCON2     equ $104
BPL1MOD     equ $108
DIWSTRT     equ $08e
DIWSTOP     equ $090
DDFSTRT     equ $092
DDFSTOP     equ $094
BPL1PTH     equ $0e0
BPL1PTL     equ $0e2
COLOR00     equ $180
COLOR01     equ $182
VPOSR       equ $004

;══════════════════════════════════════════════════════════════
; CODE (Chip RAM)
;══════════════════════════════════════════════════════════════

            section code,code_c

start:
            lea     CUSTOM,a5

            ; --- Take over the machine ---
            move.w  #$7fff,INTENA(a5)
            move.w  #$7fff,INTREQ(a5)
            move.w  #$7fff,DMACON(a5)

            ; --- Draw terrain into bitplane ---
            ; Bitplane starts as zeros (sky everywhere).
            ; Each draw_rect call fills a rectangle with 1s (terrain).

            ; Low ground (left side)
            move.w  #GROUND_L_X,d0
            move.w  #GROUND_L_Y,d1
            move.w  #GROUND_L_W,d2
            move.w  #GROUND_L_H,d3
            bsr     draw_rect

            ; High ground (right side — cliff face)
            move.w  #GROUND_R_X,d0
            move.w  #GROUND_R_Y,d1
            move.w  #GROUND_R_W,d2
            move.w  #GROUND_R_H,d3
            bsr     draw_rect

            ; Floating platform
            move.w  #PLATFORM_X,d0
            move.w  #PLATFORM_Y,d1
            move.w  #PLATFORM_W,d2
            move.w  #PLATFORM_H,d3
            bsr     draw_rect

            ; --- Patch bitplane pointer into Copper list ---
            lea     bitplane,a0
            move.l  a0,d0
            swap    d0
            lea     bpl1pth_val,a1
            move.w  d0,(a1)
            swap    d0
            lea     bpl1ptl_val,a1
            move.w  d0,(a1)

            ; --- Install Copper list ---
            lea     copperlist,a0
            move.l  a0,COP1LC(a5)
            move.w  d0,COPJMP1(a5)

            ; --- Enable DMA ---
            move.w  #$8380,DMACON(a5)   ; SET + DMAEN + COPEN + BPLEN

            ; === Main Loop ===
mainloop:
            move.l  #$1ff00,d1
.vbwait:
            move.l  VPOSR(a5),d0
            and.l   d1,d0
            bne.s   .vbwait

            btst    #6,$bfe001
            bne.s   mainloop

.halt:
            bra.s   .halt

;──────────────────────────────────────────────────────────────
; draw_rect — Fill a byte-aligned rectangle in the bitplane
;
; Input:  D0.W = x position (must be multiple of 8)
;         D1.W = y position
;         D2.W = width in pixels (must be multiple of 8)
;         D3.W = height in pixels
; Destroys: D0-D5, A0-A1
;──────────────────────────────────────────────────────────────
draw_rect:
            ; Calculate starting address: bitplane + y*40 + x/8
            lea     bitplane,a0
            mulu    #BYTES_PER_ROW,d1   ; D1 = y * 40 (32-bit result)
            add.l   d1,a0               ; A0 += y offset
            lsr.w   #3,d0               ; D0 = x / 8
            add.w   d0,a0               ; A0 += x offset

            ; Convert width from pixels to bytes
            lsr.w   #3,d2               ; D2 = width / 8

            ; Outer loop: rows
            subq.w  #1,d3               ; Height - 1 for DBRA
.row:
            move.w  d2,d5
            subq.w  #1,d5               ; Width_bytes - 1 for DBRA
            move.l  a0,a1               ; A1 = row write pointer
.col:
            move.b  #$ff,(a1)+          ; Fill 8 pixels (one byte)
            dbra    d5,.col

            add.w   #BYTES_PER_ROW,a0   ; Advance to next row
            dbra    d3,.row
            rts

;══════════════════════════════════════════════════════════════
; COPPER LIST
;══════════════════════════════════════════════════════════════

copperlist:
            ; --- Display window (standard PAL low-res) ---
            dc.w    DIWSTRT,$2c81
            dc.w    DIWSTOP,$2cc1
            dc.w    DDFSTRT,$0038
            dc.w    DDFSTOP,$00d0

            ; --- Bitplane configuration ---
            dc.w    BPLCON0,$1200       ; 1 bitplane + colour burst
            dc.w    BPLCON1,$0000
            dc.w    BPLCON2,$0000
            dc.w    BPL1MOD,$0000

            ; --- Bitplane pointer (patched by CPU) ---
            dc.w    BPL1PTH
bpl1pth_val:
            dc.w    $0000
            dc.w    BPL1PTL
bpl1ptl_val:
            dc.w    $0000

            ; --- Colours ---
            dc.w    COLOR00,COLOUR_SKY_DEEP
            dc.w    COLOR01,COLOUR_TERRAIN

            ; --- SKY GRADIENT ---
            dc.w    $3401,$fffe
            dc.w    COLOR00,COLOUR_SKY_UPPER

            dc.w    $4401,$fffe
            dc.w    COLOR00,COLOUR_SKY_MID

            dc.w    $5401,$fffe
            dc.w    COLOR00,COLOUR_SKY_LOWER

            dc.w    $6001,$fffe
            dc.w    COLOR00,COLOUR_SKY_HORIZON

            ; Black background below sky
            dc.w    $6801,$fffe
            dc.w    COLOR00,$0000

            ; --- END ---
            dc.w    $ffff,$fffe

;══════════════════════════════════════════════════════════════
; BITPLANE DATA (10,240 bytes — all zeros = sky)
;══════════════════════════════════════════════════════════════

            even
bitplane:
            dcb.b   BITPLANE_SIZE,0

If It Doesn’t Work

  • No terrain visible? Check that the three BSR draw_rect calls happen before the Copper list is installed. The bitplane must be filled before the display starts reading it.
  • Rectangle in wrong position? Remember that x and width are in pixels but must be multiples of 8. If GROUND_L_X is 10, the LSR #3 rounds it down to byte 1 (pixel 8), not pixel 10.
  • Only one rectangle appears? The subroutine destroys D0-D5. Each call must reload all four parameters fresh — you can’t reuse values from a previous call.
  • Crash or garbage? Check that draw_rect ends with RTS. Without it, execution falls through into whatever comes next — the Copper list data, which is not valid code.

What You’ve Learnt

  • BSR/RTS — subroutine call and return. BSR pushes the return address onto the stack; RTS pops it and jumps back. Parameters pass through registers.
  • MULU — unsigned 16×16→32 multiply. Used here to compute row offsets (y × 40).
  • Nested DBRA loops — an outer loop for rows and an inner loop for columns. The inner counter is reset from a preserved copy each row.
  • Byte-aligned drawing — restricting x and width to multiples of 8 avoids bit masking. Each byte written sets exactly 8 pixels.
  • Terrain as data — the landscape is defined by a few constants. The same subroutine draws any rectangle, making the terrain easy to reshape.

What’s Next

The terrain is static — drawn once at startup. In Unit 4, a hardware sprite appears on screen. Sprites float independently of the bitplane, drawn by the custom chips without touching the bitmap. This is how the player’s cursor or a creature will appear over the terrain.