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

Understanding What We Built

Deep dive into the scaffold. System takeover. Copper lists. Hardware sprites. VBlank.

5% of Signal

What You’re Building

Understanding.

This unit produces the same output as Unit 2. No new features. Instead, we stop and understand everything we’ve built. The code is heavily commented—read it alongside this explanation.

By the end of this unit:

  • You understand why we take over the system
  • You know how Copper lists create the display
  • You know how hardware sprites work
  • You understand VBlank timing

Unit 3 Screenshot

Why Theory Matters

We could keep adding features. Movement works, the display looks good. But programming without understanding is fragile. One wrong register, one misunderstood concept, and you’re staring at a blank screen with no idea why.

This unit builds the foundation for everything that follows.

Taking Over the Machine

The first thing our code does is disable the operating system:

```asm move.w #$7fff,INTENA(a5) ; Disable all interrupts move.w #$7fff,INTREQ(a5) ; Clear pending interrupts move.w #$7fff,DMACON(a5) ; Disable all DMA ```

Why Take Over?

AmigaOS is a preemptive multitasking operating system. It runs multiple programs simultaneously by rapidly switching between them using interrupts.

For a game, this creates problems:

  • Timing uncertainty: The OS might interrupt your code at any moment
  • Resource conflicts: The OS might be using DMA you need
  • Overhead: The OS consumes CPU time and memory

By disabling interrupts and DMA, we get predictable timing and full hardware access.

The Magic Number $7FFF

Amiga control registers use bit 15 as a SET/CLR flag:

  • Bit 15 = 0: Clear the bits that are 1 in bits 0-14
  • Bit 15 = 1: Set the bits that are 1 in bits 0-14

So `$7fff` means “clear all of bits 0-14”. Later, `$83a0` enables only what we need.

The Custom Chipset

Agnus (Address Generator)

Contains the Copper and Blitter. Manages all DMA operations.

Denise (Display Encoder)

Creates the video signal—sprites, bitplanes, colours.

Paula (Ports and Audio)

Four channels of 8-bit audio, floppy disk, serial/parallel ports.

The Copper

A simple co-processor that runs synchronised to the video beam. It can only:

  1. WAIT for a specific screen position
  2. MOVE a value to a hardware register

Our playfield is just colour changes:

```asm dc.w $2c07,$fffe ; WAIT for line 44 dc.w COLOR00,COLOUR_HOME ; MOVE green to colour 0

dc.w $4007,$fffe ; WAIT for line 64 dc.w COLOR00,COLOUR_WATER ; MOVE blue to colour 0 ```

No bitmap. Just the Copper changing one colour register as the beam scans down.

Copper Instruction Format

WAIT: `$VVHH,$FFFE` (VV=vertical, HH=horizontal) MOVE: `$XXYY,$VVVV` (XX=register/2, VVVV=value)

Hardware Sprites

The Amiga has 8 hardware sprites. Each is 16 pixels wide.

Without hardware sprites, moving an object means erasing and redrawing 50 times per second. With sprites, just write new coordinates—the chipset handles the rest.

Sprite Data Format

```asm frog_data: dc.w $b450,$c400 ; Control words (Y, X positions) dc.w $0000,$0000 ; Line 1 (plane0, plane1) dc.w $07e0,$0000 ; Line 2 ; … more lines … dc.w $0000,$0000 ; End marker ```

Two bitplanes give 4 colours: transparent, colour 17, colour 18, colour 19.

Vertical Blank

The video beam scans 50 times per second (PAL). Vertical blank is when the beam returns to the top.

Update graphics during VBlank to avoid tearing (old and new frames mixed together).

```asm wait_vblank: move.l #$1ff00,d1 .wait: move.l VPOSR(a5),d0 and.l d1,d0 bne.s .wait rts ```

Joystick Decoding

The Amiga uses quadrature encoding from Atari hardware:

```asm move.w JOY1DAT(a5),d0 ; Read raw move.w d0,d1 lsr.w #1,d1 ; Shift copy eor.w d1,d0 ; XOR decodes it ```

After decoding: Bit 8=Up, Bit 0=Down, Bit 9=Left, Bit 1=Right.

The DMACON Mystery

We enable DMA with `$83a0`:

  • Bit 15: SET (enable)
  • Bit 9: DMAEN (master)
  • Bit 8: BPLEN (bitplane)
  • Bit 7: COPEN (Copper)
  • Bit 5: SPREN (sprite)

Why BPLEN with no bitplanes? Hardware quirk: sprites don’t render without BPLEN enabled.

Key Takeaways

  • System takeover gives predictable timing
  • The Copper changes registers in sync with the beam
  • Hardware sprites are drawn automatically
  • VBlank is when we safely update graphics
  • BPLEN must be enabled for sprites (hardware quirk)
  • $7fff disables; $8xxx enables

The Code

;══════════════════════════════════════════════════════════════════════════════
; SIGNAL - A Frogger-style game for the Commodore Amiga
; Unit 3: Understanding What We Built
;
; This is the same code as Unit 2, with extensive comments explaining
; every aspect of how the Amiga's custom chipset creates our game display.
;══════════════════════════════════════════════════════════════════════════════

;══════════════════════════════════════════════════════════════════════════════
; TWEAKABLE VALUES
;══════════════════════════════════════════════════════════════════════════════
; These constants let you experiment without understanding the code.
; Change them, rebuild, see results.

FROG_START_X    equ 160         ; Horizontal: 0=left edge, 320=right edge
FROG_START_Y    equ 180         ; Vertical: 0=top, 256=bottom (PAL)

MOVE_SPEED      equ 2           ; Pixels moved per frame (at 50fps PAL)

; Screen boundaries for the frog
MIN_X           equ 48          ; Sprites can't go left of ~44
MAX_X           equ 280         ; Or right of ~304
MIN_Y           equ 44          ; Top of playfield (home zone)
MAX_Y           equ 196         ; Bottom of playfield (start zone)

FROG_HEIGHT     equ 16          ; Sprite is 16 pixels tall

; Colours in $0RGB format (0-15 for each component)
; Example: $0F00 = red, $00F0 = green, $000F = blue, $0FFF = white
COLOUR_HOME     equ $0080       ; Home zone: dark green
COLOUR_WATER    equ $0048       ; Water: dark blue
COLOUR_WAVE     equ $006b       ; Water highlight: lighter blue
COLOUR_MEDIAN   equ $0080       ; Safe median: dark green
COLOUR_ROAD     equ $0444       ; Road: dark grey
COLOUR_MARKER   equ $0666       ; Lane markings: lighter grey
COLOUR_START    equ $0080       ; Start zone: dark green
COLOUR_BORDER   equ $0070       ; Border: slightly different green

; Sprite palette (colours 17-19 in the Amiga palette)
; Sprites use a separate palette from the playfield
COLOUR_FROG     equ $00f0       ; Bright green body
COLOUR_EYES     equ $0ff0       ; Yellow eyes
COLOUR_OUTLINE  equ $0000       ; Black outline

;══════════════════════════════════════════════════════════════════════════════
; HARDWARE REGISTER DEFINITIONS
;══════════════════════════════════════════════════════════════════════════════
; The Amiga's custom chipset is memory-mapped starting at $DFF000.
; Each register is accessed as an offset from this base address.

CUSTOM      equ $dff000         ; Custom chip base address

; DMA and interrupt control
DMACONR     equ $002            ; DMA control read (tells us what's enabled)
DMACON      equ $096            ; DMA control write (enables/disables DMA channels)
INTENA      equ $09a            ; Interrupt enable (which interrupts are active)
INTREQ      equ $09c            ; Interrupt request (which interrupts are pending)

; Timing
VPOSR       equ $004            ; Vertical beam position (high bits)
                                ; Used with VHPOSR ($006) for full position

; Input
JOY1DAT     equ $00c            ; Joystick port 2 data register

; Copper (display co-processor)
COP1LC      equ $080            ; Copper list 1 location (32-bit address)
COPJMP1     equ $088            ; Copper jump strobe (writing here starts copper)

; Colours
COLOR00     equ $180            ; Background colour (colour 0)
                                ; COLOR01-COLOR31 follow at $182, $184, etc.

; Sprite registers
SPR0PTH     equ $120            ; Sprite 0 pointer high word
SPR0PTL     equ $122            ; Sprite 0 pointer low word
                                ; SPR1PTH/L at $124/$126, and so on up to SPR7

;══════════════════════════════════════════════════════════════════════════════
; CODE SECTION
;══════════════════════════════════════════════════════════════════════════════
; The section directive tells the assembler how to organise the output.
;
; "code_c" means "code in Chip RAM". Chip RAM is the first 512K (or more)
; that the custom chipset can access. The Copper and sprites MUST be in
; Chip RAM - they can't see Fast RAM.
;
; Without "_c", data would go to Fast RAM, which is faster for the CPU
; but invisible to the custom chipset.

            section code,code_c

start:
            lea     CUSTOM,a5           ; A5 = $DFF000 (custom chip base)
                                        ; We keep this in A5 throughout the program
                                        ; for quick access to hardware registers

;──────────────────────────────────────────────────────────────────────────────
; SYSTEM TAKEOVER
;──────────────────────────────────────────────────────────────────────────────
; AmigaOS is a preemptive multitasking operating system. It uses interrupts
; to switch between tasks, and DMA for disk, sound, and graphics.
;
; For a game that needs precise timing and full hardware control, we disable
; all of this. The OS stops running; we own the machine.
;
; This is why you need to reset to exit - there's no OS to return to!

            move.w  #$7fff,INTENA(a5)   ; Disable ALL interrupts
                                        ; $7fff = bits 0-14 set, bit 15 clear
                                        ; Bit 15 is SET/CLR: 0=disable, 1=enable
                                        ; So this disables bits 0-14

            move.w  #$7fff,INTREQ(a5)   ; Clear any pending interrupt requests
                                        ; Even disabled interrupts might be waiting

            move.w  #$7fff,DMACON(a5)   ; Disable ALL DMA channels
                                        ; Same SET/CLR bit 15 logic
                                        ; Stops copper, sprites, bitplanes, audio, disk

;──────────────────────────────────────────────────────────────────────────────
; INITIALISE FROG POSITION
;──────────────────────────────────────────────────────────────────────────────

            move.w  #FROG_START_X,frog_x    ; Set initial X position
            move.w  #FROG_START_Y,frog_y    ; Set initial Y position

;──────────────────────────────────────────────────────────────────────────────
; SET UP SPRITE POINTER
;──────────────────────────────────────────────────────────────────────────────
; The Copper needs to know where sprite data is in memory. We write the
; address of our sprite data into the Copper list.
;
; Amiga addresses are 32-bit, but each Copper instruction is only 32 bits
; total (16-bit register + 16-bit value). So we need TWO instructions:
; one for the high 16 bits, one for the low 16 bits.

            lea     frog_data,a0        ; A0 = address of sprite data
            move.l  a0,d0               ; D0 = same address (32-bit)
            swap    d0                  ; D0 high word now in low word
            lea     sprpth_val,a1       ; A1 = where to write high word
            move.w  d0,(a1)             ; Write high word to Copper list
            swap    d0                  ; Restore: low word back in low word
            lea     sprptl_val,a1       ; A1 = where to write low word
            move.w  d0,(a1)             ; Write low word to Copper list

;──────────────────────────────────────────────────────────────────────────────
; UPDATE SPRITE CONTROL WORDS
;──────────────────────────────────────────────────────────────────────────────

            bsr     update_sprite       ; Set initial sprite position

;──────────────────────────────────────────────────────────────────────────────
; INSTALL COPPER LIST
;──────────────────────────────────────────────────────────────────────────────
; The Copper is a simple processor that runs in sync with the video beam.
; It can WAIT for a specific screen position, or MOVE a value to a register.
; Our Copper list sets colours at specific scanlines to create the playfield.

            lea     copperlist,a0       ; A0 = address of our Copper list
            move.l  a0,COP1LC(a5)       ; Tell hardware where list is
            move.w  d0,COPJMP1(a5)      ; "Strobe" register - writing ANY value
                                        ; here makes the Copper jump to COP1LC

;──────────────────────────────────────────────────────────────────────────────
; ENABLE DMA
;──────────────────────────────────────────────────────────────────────────────
; Now we selectively enable only what we need:
;
; $83a0 = %1000 0011 1010 0000
;         │    │  │ │  │
;         │    │  │ │  └─ Bit 5: SPREN (sprite DMA enable)
;         │    │  │ └──── Bit 7: COPEN (Copper DMA enable)
;         │    │  └────── Bit 8: BPLEN (bitplane DMA enable)
;         │    └───────── Bit 9: DMAEN (master DMA enable)
;         └────────────── Bit 15: SET (1=enable the bits below)
;
; IMPORTANT: We need BPLEN even though we have no bitplanes!
; Without it, sprites don't render correctly. This is a hardware quirk.

            move.w  #$83a0,DMACON(a5)   ; Enable master + copper + sprites + bitplanes

;══════════════════════════════════════════════════════════════════════════════
; MAIN LOOP
;══════════════════════════════════════════════════════════════════════════════
; This runs 50 times per second (PAL) or 60 times per second (NTSC).
; Each iteration:
;   1. Wait for vertical blank (beam at bottom of screen)
;   2. Read joystick input
;   3. Update frog position based on input
;   4. Update sprite control words for new position

mainloop:
            bsr.s   wait_vblank         ; Wait for vertical blank
            bsr.s   read_joystick       ; Read joystick -> D0
            bsr.s   move_frog           ; Move frog based on D0
            bsr     update_sprite       ; Update sprite position

            ; Check left mouse button (active low at $BFE001 bit 6)
            btst    #6,$bfe001          ; Test bit 6 of CIA-A PRA
            bne.s   mainloop            ; If not pressed (bit=1), continue

            ; Button pressed - but we have nowhere to go!
            ; On a real Amiga, you'd restore the system here.
            ; For now, just keep looping until reset.
            bra.s   mainloop

;══════════════════════════════════════════════════════════════════════════════
; SUBROUTINES
;══════════════════════════════════════════════════════════════════════════════

;──────────────────────────────────────────────────────────────────────────────
; WAIT_VBLANK - Wait for vertical blank
;──────────────────────────────────────────────────────────────────────────────
; The video beam scans from top-left to bottom-right, then returns to top.
; "Vertical blank" is when the beam is returning - no visible output.
; This is the safe time to update graphics without visible tearing.
;
; VPOSR contains the beam position. We wait until it's at line 0.

wait_vblank:
            move.l  #$1ff00,d1          ; Mask: bits 16-8 (vertical position)
.wait:
            move.l  VPOSR(a5),d0        ; Read beam position
            and.l   d1,d0               ; Mask out horizontal position
            bne.s   .wait               ; Loop until vertical = 0
            rts

;──────────────────────────────────────────────────────────────────────────────
; READ_JOYSTICK - Read and decode joystick input
;──────────────────────────────────────────────────────────────────────────────
; JOY1DAT contains joystick data, but it's encoded weirdly (inherited from
; the Atari 400/800). The vertical movement affects the horizontal bits
; through XOR, so we need to decode it.
;
; Raw JOY1DAT:
;   Bit 9: Y1 XOR X1 (left signal)
;   Bit 8: Y1 (up signal before decode)
;   Bit 1: Y0 XOR X0 (right signal)
;   Bit 0: Y0 (down signal before decode)
;
; After XOR decoding, bits represent actual directions.

read_joystick:
            move.w  JOY1DAT(a5),d0      ; Read raw joystick data
            move.w  d0,d1               ; Copy to D1
            lsr.w   #1,d1               ; Shift D1 right by 1
            eor.w   d1,d0               ; XOR with shifted copy
                                        ; This decodes the quadrature encoding
            rts
            ; Result in D0:
            ;   Bit 8 = up
            ;   Bit 0 = down
            ;   Bit 9 = left
            ;   Bit 1 = right

;──────────────────────────────────────────────────────────────────────────────
; MOVE_FROG - Update frog position based on joystick
;──────────────────────────────────────────────────────────────────────────────
; For each direction:
;   1. Test if that direction bit is set
;   2. Calculate new position
;   3. Check against boundary
;   4. Store new position if within bounds

move_frog:
            ; --- Check Up (bit 8) ---
            btst    #8,d0               ; Test up bit
            beq.s   .no_up              ; Skip if not pressed
            move.w  frog_y,d1           ; Get current Y
            sub.w   #MOVE_SPEED,d1      ; Subtract (up = decrease Y)
            cmp.w   #MIN_Y,d1           ; Compare with top boundary
            blt.s   .no_up              ; Skip if past boundary
            move.w  d1,frog_y           ; Store new Y
.no_up:
            ; --- Check Down (bit 0) ---
            btst    #0,d0
            beq.s   .no_down
            move.w  frog_y,d1
            add.w   #MOVE_SPEED,d1      ; Add (down = increase Y)
            cmp.w   #MAX_Y,d1
            bgt.s   .no_down            ; Skip if past boundary
            move.w  d1,frog_y
.no_down:
            ; --- Check Left (bit 9) ---
            btst    #9,d0
            beq.s   .no_left
            move.w  frog_x,d1
            sub.w   #MOVE_SPEED,d1      ; Subtract (left = decrease X)
            cmp.w   #MIN_X,d1
            blt.s   .no_left
            move.w  d1,frog_x
.no_left:
            ; --- Check Right (bit 1) ---
            btst    #1,d0
            beq.s   .no_right
            move.w  frog_x,d1
            add.w   #MOVE_SPEED,d1      ; Add (right = increase X)
            cmp.w   #MAX_X,d1
            bgt.s   .no_right
            move.w  d1,frog_x
.no_right:
            rts

;──────────────────────────────────────────────────────────────────────────────
; UPDATE_SPRITE - Write position to sprite control words
;──────────────────────────────────────────────────────────────────────────────
; Hardware sprites have control words at the start of their data:
;
; Word 0: VSTART[7:0] << 8 | HSTART[8:1]
;         (vertical start position, horizontal start / 2)
;
; Word 1: VSTOP[7:0] << 8 | VSTART[8] << 2 | VSTOP[8] << 1 | HSTART[0]
;         (vertical stop position, plus extra bits for large positions)
;
; For our 16-pixel tall sprite starting at Y positions < 256, we can
; simplify: just pack VSTART and HSTART/2 into word 0, VSTOP into word 1.

update_sprite:
            lea     frog_data,a0        ; A0 = sprite data start
            move.w  frog_y,d0           ; D0 = Y position (VSTART)
            move.w  frog_x,d1           ; D1 = X position (HSTART)

            ; Build control word 0: VSTART << 8 | HSTART >> 1
            move.w  d0,d2               ; D2 = VSTART
            lsl.w   #8,d2               ; Shift to high byte
            lsr.w   #1,d1               ; HSTART / 2 (sprites use half-res X)
            or.b    d1,d2               ; Combine into low byte
            move.w  d2,(a0)             ; Write to sprite control word 0

            ; Build control word 1: VSTOP << 8
            add.w   #FROG_HEIGHT,d0     ; D0 = VSTOP (VSTART + height)
            lsl.w   #8,d0               ; Shift to high byte
            move.w  d0,2(a0)            ; Write to sprite control word 1

            rts

;══════════════════════════════════════════════════════════════════════════════
; VARIABLES
;══════════════════════════════════════════════════════════════════════════════
; These are in the code section so they're in Chip RAM with everything else.
; The 68000 can access them with PC-relative addressing for efficiency.

frog_x:     dc.w    160             ; Current horizontal position
frog_y:     dc.w    180             ; Current vertical position

;══════════════════════════════════════════════════════════════════════════════
; COPPER LIST
;══════════════════════════════════════════════════════════════════════════════
; The Copper executes simple instructions synchronised to the video beam.
; Each instruction is 32 bits (two 16-bit words).
;
; MOVE instruction: $XXYY,$VVVV
;   XX = register offset / 2 (bits 8-1 of register address)
;   YY = 00 (identifies this as a MOVE)
;   VVVV = 16-bit value to write
;
; WAIT instruction: $VVHH,$FFFE
;   VV = vertical position to wait for
;   HH = horizontal position to wait for
;   $FFFE = identifies this as a WAIT (bits 0 and 15 clear)
;
; The $07 in our WAITs means "wait for horizontal position 7" which is
; just after the left edge of the visible screen.

copperlist:
            dc.w    COLOR00,$0000       ; MOVE: Set background to black

            ; --- Sprite 0 palette (colours 17-19) ---
            ; Sprites 0-1 share colours 16-19 ($1A0-$1A6)
            ; Colour 16 ($1A0) is transparent, 17-19 are the sprite colours
            dc.w    $01a2,COLOUR_FROG   ; MOVE: Colour 17 = frog body
            dc.w    $01a4,COLOUR_EYES   ; MOVE: Colour 18 = eyes
            dc.w    $01a6,COLOUR_OUTLINE ; MOVE: Colour 19 = outline

            ; --- Sprite 0 pointer ---
            ; These values are filled in by the CPU at startup
            dc.w    SPR0PTH             ; MOVE: SPR0PTH register ($120)
sprpth_val: dc.w    $0000               ; Value: high word of sprite address
            dc.w    SPR0PTL             ; MOVE: SPR0PTL register ($122)
sprptl_val: dc.w    $0000               ; Value: low word of sprite address

            ; === HOME ZONE (line $2C = 44) ===
            dc.w    $2c07,$fffe          ; WAIT for line 44, position 7
            dc.w    COLOR00,COLOUR_HOME  ; MOVE: Background = green

            ; === WATER ZONE (5 lanes with wave highlights) ===
            dc.w    $4007,$fffe          ; WAIT for line 64
            dc.w    COLOR00,COLOUR_WATER ; Dark blue

            dc.w    $4c07,$fffe          ; WAIT for line 76
            dc.w    COLOR00,COLOUR_WAVE  ; Light blue highlight

            dc.w    $5407,$fffe          ; WAIT for line 84
            dc.w    COLOR00,COLOUR_WATER ; Dark blue

            dc.w    $5c07,$fffe          ; WAIT for line 92
            dc.w    COLOR00,COLOUR_WAVE  ; Light blue highlight

            dc.w    $6407,$fffe          ; WAIT for line 100
            dc.w    COLOR00,COLOUR_WATER ; Dark blue

            ; === MEDIAN (safe zone, line $6C = 108) ===
            dc.w    $6c07,$fffe
            dc.w    COLOR00,COLOUR_MEDIAN

            ; === ROAD ZONE (4 lanes with markings) ===
            dc.w    $7807,$fffe          ; Line 120 - road
            dc.w    COLOR00,COLOUR_ROAD

            dc.w    $8407,$fffe          ; Line 132 - marking
            dc.w    COLOR00,COLOUR_MARKER

            dc.w    $8807,$fffe          ; Line 136 - road
            dc.w    COLOR00,COLOUR_ROAD

            dc.w    $9407,$fffe          ; Line 148 - marking
            dc.w    COLOR00,COLOUR_MARKER

            dc.w    $9807,$fffe          ; Line 152 - road
            dc.w    COLOR00,COLOUR_ROAD

            dc.w    $a407,$fffe          ; Line 164 - marking
            dc.w    COLOR00,COLOUR_MARKER

            dc.w    $a807,$fffe          ; Line 168 - road
            dc.w    COLOR00,COLOUR_ROAD

            ; === START ZONE (line $B4 = 180) ===
            dc.w    $b407,$fffe
            dc.w    COLOR00,COLOUR_START

            dc.w    $c007,$fffe          ; Line 192 - border
            dc.w    COLOR00,COLOUR_BORDER

            ; === BOTTOM (line $F0 = 240) ===
            dc.w    $f007,$fffe
            dc.w    COLOR00,$0000        ; Black

            ; === END OF COPPER LIST ===
            dc.w    $ffff,$fffe          ; WAIT for impossible position
                                         ; This effectively halts the Copper
                                         ; until next frame when it restarts

;══════════════════════════════════════════════════════════════════════════════
; SPRITE DATA
;══════════════════════════════════════════════════════════════════════════════
; Hardware sprites are 16 pixels wide and up to 256 lines tall.
; Each line is 4 bytes: two 16-bit words (plane 0 and plane 1).
;
; The two planes combine to give 4 colours per pixel:
;   Plane0=0, Plane1=0 -> Transparent
;   Plane0=1, Plane1=0 -> Colour 17 (green body)
;   Plane0=0, Plane1=1 -> Colour 18 (yellow eyes)
;   Plane0=1, Plane1=1 -> Colour 19 (black outline)

            even                        ; Ensure word alignment
frog_data:
            ; Control words (updated by update_sprite)
            dc.w    $b450,$c400         ; Default: Y=180, X=160

            ; 16 lines of image data (plane0, plane1)
            ; Each pair: plane0 bits | plane1 bits
            dc.w    $0000,$0000         ; ................
            dc.w    $07e0,$0000         ; .....######.....
            dc.w    $1ff8,$0420         ; ...##########...  (with eye hints)
            dc.w    $3ffc,$0a50         ; ..############..
            dc.w    $7ffe,$1248         ; .##############.  (eyes visible)
            dc.w    $7ffe,$1008         ; .##############.
            dc.w    $ffff,$2004         ; ################
            dc.w    $ffff,$0000         ; ################
            dc.w    $ffff,$0000         ; ################
            dc.w    $7ffe,$2004         ; .##############.
            dc.w    $7ffe,$1008         ; .##############.
            dc.w    $3ffc,$0810         ; ..############..
            dc.w    $1ff8,$0420         ; ...##########...
            dc.w    $07e0,$0000         ; .....######.....
            dc.w    $0000,$0000         ; ................
            dc.w    $0000,$0000         ; ................

            ; End marker (required by hardware)
            dc.w    $0000,$0000         ; Tells chipset "no more sprite data"

What’s Next

Theory complete. In Unit 4, we’ll refine the playfield colours and layout.

What Changed

Unit 2 → Unit 3
+364-195
1-;──────────────────────────────────────────────────────────────
1+;══════════════════════════════════════════════════════════════════════════════
22 ; SIGNAL - A Frogger-style game for the Commodore Amiga
3-; Unit 2: Moving the Frog
3+; Unit 3: Understanding What We Built
44 ;
5-; Push the joystick, the frog moves. Interactivity!
6-;──────────────────────────────────────────────────────────────
5+; This is the same code as Unit 2, with extensive comments explaining
6+; every aspect of how the Amiga's custom chipset creates our game display.
7+;══════════════════════════════════════════════════════════════════════════════
78
8-;══════════════════════════════════════════════════════════════
9+;══════════════════════════════════════════════════════════════════════════════
910 ; TWEAKABLE VALUES
10-;══════════════════════════════════════════════════════════════
11+;══════════════════════════════════════════════════════════════════════════════
12+; These constants let you experiment without understanding the code.
13+; Change them, rebuild, see results.
1114
12-FROG_START_X equ 160 ; Starting horizontal position
13-FROG_START_Y equ 180 ; Starting vertical position
15+FROG_START_X equ 160 ; Horizontal: 0=left edge, 320=right edge
16+FROG_START_Y equ 180 ; Vertical: 0=top, 256=bottom (PAL)
1417
15-MOVE_SPEED equ 2 ; Pixels per frame (try 1, 2, or 4)
18+MOVE_SPEED equ 2 ; Pixels moved per frame (at 50fps PAL)
1619
17-; Boundaries (where the frog can move)
18-MIN_X equ 48 ; Left edge
19-MAX_X equ 280 ; Right edge
20-MIN_Y equ 44 ; Top of play area
21-MAX_Y equ 196 ; Bottom of play area
20+; Screen boundaries for the frog
21+MIN_X equ 48 ; Sprites can't go left of ~44
22+MAX_X equ 280 ; Or right of ~304
23+MIN_Y equ 44 ; Top of playfield (home zone)
24+MAX_Y equ 196 ; Bottom of playfield (start zone)
2225
23-; Sprite dimensions
24-FROG_HEIGHT equ 16
26+FROG_HEIGHT equ 16 ; Sprite is 16 pixels tall
2527
26-; Colours
27-COLOUR_HOME equ $0080 ; Home zone: green
28+; Colours in $0RGB format (0-15 for each component)
29+; Example: $0F00 = red, $00F0 = green, $000F = blue, $0FFF = white
30+COLOUR_HOME equ $0080 ; Home zone: dark green
2831 COLOUR_WATER equ $0048 ; Water: dark blue
2932 COLOUR_WAVE equ $006b ; Water highlight: lighter blue
30-COLOUR_MEDIAN equ $0080 ; Safe median: green
33+COLOUR_MEDIAN equ $0080 ; Safe median: dark green
3134 COLOUR_ROAD equ $0444 ; Road: dark grey
32-COLOUR_MARKER equ $0666 ; Road marking: light grey
33-COLOUR_START equ $0080 ; Start zone: green
34-COLOUR_BORDER equ $0070 ; Border: darker green
35+COLOUR_MARKER equ $0666 ; Lane markings: lighter grey
36+COLOUR_START equ $0080 ; Start zone: dark green
37+COLOUR_BORDER equ $0070 ; Border: slightly different green
3538
36-; Frog colours
37-COLOUR_FROG equ $00f0 ; Frog body: bright green
38-COLOUR_EYES equ $0ff0 ; Frog eyes: yellow
39-COLOUR_OUTLINE equ $0000 ; Frog outline: black
39+; Sprite palette (colours 17-19 in the Amiga palette)
40+; Sprites use a separate palette from the playfield
41+COLOUR_FROG equ $00f0 ; Bright green body
42+COLOUR_EYES equ $0ff0 ; Yellow eyes
43+COLOUR_OUTLINE equ $0000 ; Black outline
4044
41-;══════════════════════════════════════════════════════════════
42-; HARDWARE REGISTERS
43-;══════════════════════════════════════════════════════════════
45+;══════════════════════════════════════════════════════════════════════════════
46+; HARDWARE REGISTER DEFINITIONS
47+;══════════════════════════════════════════════════════════════════════════════
48+; The Amiga's custom chipset is memory-mapped starting at $DFF000.
49+; Each register is accessed as an offset from this base address.
4450
45-CUSTOM equ $dff000
51+CUSTOM equ $dff000 ; Custom chip base address
4652
47-; Custom chip register offsets
48-DMACONR equ $002
49-VPOSR equ $004
50-JOY1DAT equ $00c ; Joystick port 2 data
51-COP1LC equ $080
52-COPJMP1 equ $088
53-DMACON equ $096
54-INTENA equ $09a
55-INTREQ equ $09c
56-COLOR00 equ $180
57-SPR0PTH equ $120
58-SPR0PTL equ $122
53+; DMA and interrupt control
54+DMACONR equ $002 ; DMA control read (tells us what's enabled)
55+DMACON equ $096 ; DMA control write (enables/disables DMA channels)
56+INTENA equ $09a ; Interrupt enable (which interrupts are active)
57+INTREQ equ $09c ; Interrupt request (which interrupts are pending)
5958
60-;══════════════════════════════════════════════════════════════
61-; CODE SECTION (in chip RAM)
62-;══════════════════════════════════════════════════════════════
59+; Timing
60+VPOSR equ $004 ; Vertical beam position (high bits)
61+ ; Used with VHPOSR ($006) for full position
62+
63+; Input
64+JOY1DAT equ $00c ; Joystick port 2 data register
65+
66+; Copper (display co-processor)
67+COP1LC equ $080 ; Copper list 1 location (32-bit address)
68+COPJMP1 equ $088 ; Copper jump strobe (writing here starts copper)
69+
70+; Colours
71+COLOR00 equ $180 ; Background colour (colour 0)
72+ ; COLOR01-COLOR31 follow at $182, $184, etc.
73+
74+; Sprite registers
75+SPR0PTH equ $120 ; Sprite 0 pointer high word
76+SPR0PTL equ $122 ; Sprite 0 pointer low word
77+ ; SPR1PTH/L at $124/$126, and so on up to SPR7
78+
79+;══════════════════════════════════════════════════════════════════════════════
80+; CODE SECTION
81+;══════════════════════════════════════════════════════════════════════════════
82+; The section directive tells the assembler how to organise the output.
83+;
84+; "code_c" means "code in Chip RAM". Chip RAM is the first 512K (or more)
85+; that the custom chipset can access. The Copper and sprites MUST be in
86+; Chip RAM - they can't see Fast RAM.
87+;
88+; Without "_c", data would go to Fast RAM, which is faster for the CPU
89+; but invisible to the custom chipset.
90+
6391 section code,code_c
6492
6593 start:
66- lea CUSTOM,a5 ; Custom chip base in A5
94+ lea CUSTOM,a5 ; A5 = $DFF000 (custom chip base)
95+ ; We keep this in A5 throughout the program
96+ ; for quick access to hardware registers
6797
68- ; --- Take over the machine ---
69- move.w #$7fff,INTENA(a5) ; Disable all interrupts
70- move.w #$7fff,INTREQ(a5) ; Clear pending interrupts
71- move.w #$7fff,DMACON(a5) ; Disable all DMA
98+;──────────────────────────────────────────────────────────────────────────────
99+; SYSTEM TAKEOVER
100+;──────────────────────────────────────────────────────────────────────────────
101+; AmigaOS is a preemptive multitasking operating system. It uses interrupts
102+; to switch between tasks, and DMA for disk, sound, and graphics.
103+;
104+; For a game that needs precise timing and full hardware control, we disable
105+; all of this. The OS stops running; we own the machine.
106+;
107+; This is why you need to reset to exit - there's no OS to return to!
72108
73- ; --- Initialise frog position ---
74- move.w #FROG_START_X,frog_x
75- move.w #FROG_START_Y,frog_y
109+ move.w #$7fff,INTENA(a5) ; Disable ALL interrupts
110+ ; $7fff = bits 0-14 set, bit 15 clear
111+ ; Bit 15 is SET/CLR: 0=disable, 1=enable
112+ ; So this disables bits 0-14
76113
77- ; --- Set sprite pointer in copper list ---
78- lea frog_data,a0
79- move.l a0,d0
80- swap d0
81- lea sprpth_val,a1
82- move.w d0,(a1)
83- swap d0
84- lea sprptl_val,a1
85- move.w d0,(a1)
114+ move.w #$7fff,INTREQ(a5) ; Clear any pending interrupt requests
115+ ; Even disabled interrupts might be waiting
86116
87- ; --- Update sprite position ---
88- bsr update_sprite
117+ move.w #$7fff,DMACON(a5) ; Disable ALL DMA channels
118+ ; Same SET/CLR bit 15 logic
119+ ; Stops copper, sprites, bitplanes, audio, disk
89120
90- ; --- Install copper list ---
91- lea copperlist,a0
92- move.l a0,COP1LC(a5)
93- move.w d0,COPJMP1(a5)
121+;──────────────────────────────────────────────────────────────────────────────
122+; INITIALISE FROG POSITION
123+;──────────────────────────────────────────────────────────────────────────────
94124
95- ; --- Enable DMA ---
96- move.w #$83a0,DMACON(a5) ; Master + copper + sprites + bitplanes
125+ move.w #FROG_START_X,frog_x ; Set initial X position
126+ move.w #FROG_START_Y,frog_y ; Set initial Y position
97127
98-;══════════════════════════════════════════════════════════════
128+;──────────────────────────────────────────────────────────────────────────────
129+; SET UP SPRITE POINTER
130+;──────────────────────────────────────────────────────────────────────────────
131+; The Copper needs to know where sprite data is in memory. We write the
132+; address of our sprite data into the Copper list.
133+;
134+; Amiga addresses are 32-bit, but each Copper instruction is only 32 bits
135+; total (16-bit register + 16-bit value). So we need TWO instructions:
136+; one for the high 16 bits, one for the low 16 bits.
137+
138+ lea frog_data,a0 ; A0 = address of sprite data
139+ move.l a0,d0 ; D0 = same address (32-bit)
140+ swap d0 ; D0 high word now in low word
141+ lea sprpth_val,a1 ; A1 = where to write high word
142+ move.w d0,(a1) ; Write high word to Copper list
143+ swap d0 ; Restore: low word back in low word
144+ lea sprptl_val,a1 ; A1 = where to write low word
145+ move.w d0,(a1) ; Write low word to Copper list
146+
147+;──────────────────────────────────────────────────────────────────────────────
148+; UPDATE SPRITE CONTROL WORDS
149+;──────────────────────────────────────────────────────────────────────────────
150+
151+ bsr update_sprite ; Set initial sprite position
152+
153+;──────────────────────────────────────────────────────────────────────────────
154+; INSTALL COPPER LIST
155+;──────────────────────────────────────────────────────────────────────────────
156+; The Copper is a simple processor that runs in sync with the video beam.
157+; It can WAIT for a specific screen position, or MOVE a value to a register.
158+; Our Copper list sets colours at specific scanlines to create the playfield.
159+
160+ lea copperlist,a0 ; A0 = address of our Copper list
161+ move.l a0,COP1LC(a5) ; Tell hardware where list is
162+ move.w d0,COPJMP1(a5) ; "Strobe" register - writing ANY value
163+ ; here makes the Copper jump to COP1LC
164+
165+;──────────────────────────────────────────────────────────────────────────────
166+; ENABLE DMA
167+;──────────────────────────────────────────────────────────────────────────────
168+; Now we selectively enable only what we need:
169+;
170+; $83a0 = %1000 0011 1010 0000
171+; │ │ │ │ │
172+; │ │ │ │ └─ Bit 5: SPREN (sprite DMA enable)
173+; │ │ │ └──── Bit 7: COPEN (Copper DMA enable)
174+; │ │ └────── Bit 8: BPLEN (bitplane DMA enable)
175+; │ └───────── Bit 9: DMAEN (master DMA enable)
176+; └────────────── Bit 15: SET (1=enable the bits below)
177+;
178+; IMPORTANT: We need BPLEN even though we have no bitplanes!
179+; Without it, sprites don't render correctly. This is a hardware quirk.
180+
181+ move.w #$83a0,DMACON(a5) ; Enable master + copper + sprites + bitplanes
182+
183+;══════════════════════════════════════════════════════════════════════════════
99184 ; MAIN LOOP
100-;══════════════════════════════════════════════════════════════
185+;══════════════════════════════════════════════════════════════════════════════
186+; This runs 50 times per second (PAL) or 60 times per second (NTSC).
187+; Each iteration:
188+; 1. Wait for vertical blank (beam at bottom of screen)
189+; 2. Read joystick input
190+; 3. Update frog position based on input
191+; 4. Update sprite control words for new position
101192
102193 mainloop:
103194 bsr.s wait_vblank ; Wait for vertical blank
104- bsr.s read_joystick ; Read joystick into D0
105- bsr.s move_frog ; Move frog based on input
106- bsr update_sprite ; Update sprite control words
195+ bsr.s read_joystick ; Read joystick -> D0
196+ bsr.s move_frog ; Move frog based on D0
197+ bsr update_sprite ; Update sprite position
107198
108- ; Check left mouse button for exit
109- btst #6,$bfe001
110- bne.s mainloop
199+ ; Check left mouse button (active low at $BFE001 bit 6)
200+ btst #6,$bfe001 ; Test bit 6 of CIA-A PRA
201+ bne.s mainloop ; If not pressed (bit=1), continue
111202
112- ; Button pressed - exit
113- bra.s mainloop ; (Actually just loop - reset to exit)
203+ ; Button pressed - but we have nowhere to go!
204+ ; On a real Amiga, you'd restore the system here.
205+ ; For now, just keep looping until reset.
206+ bra.s mainloop
114207
115-;──────────────────────────────────────────────────────────────
116-; Wait for vertical blank
117-;──────────────────────────────────────────────────────────────
208+;══════════════════════════════════════════════════════════════════════════════
209+; SUBROUTINES
210+;══════════════════════════════════════════════════════════════════════════════
211+
212+;──────────────────────────────────────────────────────────────────────────────
213+; WAIT_VBLANK - Wait for vertical blank
214+;──────────────────────────────────────────────────────────────────────────────
215+; The video beam scans from top-left to bottom-right, then returns to top.
216+; "Vertical blank" is when the beam is returning - no visible output.
217+; This is the safe time to update graphics without visible tearing.
218+;
219+; VPOSR contains the beam position. We wait until it's at line 0.
220+
118221 wait_vblank:
119- move.l #$1ff00,d1
222+ move.l #$1ff00,d1 ; Mask: bits 16-8 (vertical position)
120223 .wait:
121- move.l VPOSR(a5),d0
122- and.l d1,d0
123- bne.s .wait
224+ move.l VPOSR(a5),d0 ; Read beam position
225+ and.l d1,d0 ; Mask out horizontal position
226+ bne.s .wait ; Loop until vertical = 0
124227 rts
125228
126-;──────────────────────────────────────────────────────────────
127-; Read joystick
128-; Output: D0 = decoded directions
129-; Bit 8 = up, Bit 0 = down, Bit 9 = left, Bit 1 = right
130-;──────────────────────────────────────────────────────────────
229+;──────────────────────────────────────────────────────────────────────────────
230+; READ_JOYSTICK - Read and decode joystick input
231+;──────────────────────────────────────────────────────────────────────────────
232+; JOY1DAT contains joystick data, but it's encoded weirdly (inherited from
233+; the Atari 400/800). The vertical movement affects the horizontal bits
234+; through XOR, so we need to decode it.
235+;
236+; Raw JOY1DAT:
237+; Bit 9: Y1 XOR X1 (left signal)
238+; Bit 8: Y1 (up signal before decode)
239+; Bit 1: Y0 XOR X0 (right signal)
240+; Bit 0: Y0 (down signal before decode)
241+;
242+; After XOR decoding, bits represent actual directions.
243+
131244 read_joystick:
132- move.w JOY1DAT(a5),d0 ; Read joystick data
133- move.w d0,d1
134- lsr.w #1,d1 ; Shift for XOR decode
135- eor.w d1,d0 ; XOR decode for up/down
245+ move.w JOY1DAT(a5),d0 ; Read raw joystick data
246+ move.w d0,d1 ; Copy to D1
247+ lsr.w #1,d1 ; Shift D1 right by 1
248+ eor.w d1,d0 ; XOR with shifted copy
249+ ; This decodes the quadrature encoding
136250 rts
251+ ; Result in D0:
252+ ; Bit 8 = up
253+ ; Bit 0 = down
254+ ; Bit 9 = left
255+ ; Bit 1 = right
137256
138-;──────────────────────────────────────────────────────────────
139-; Move frog based on joystick input
140-; Input: D0 = decoded joystick directions
141-;──────────────────────────────────────────────────────────────
257+;──────────────────────────────────────────────────────────────────────────────
258+; MOVE_FROG - Update frog position based on joystick
259+;──────────────────────────────────────────────────────────────────────────────
260+; For each direction:
261+; 1. Test if that direction bit is set
262+; 2. Calculate new position
263+; 3. Check against boundary
264+; 4. Store new position if within bounds
265+
142266 move_frog:
143- ; --- Check Up ---
144- btst #8,d0
145- beq.s .no_up
146- move.w frog_y,d1
147- sub.w #MOVE_SPEED,d1
148- cmp.w #MIN_Y,d1
149- blt.s .no_up
150- move.w d1,frog_y
267+ ; --- Check Up (bit 8) ---
268+ btst #8,d0 ; Test up bit
269+ beq.s .no_up ; Skip if not pressed
270+ move.w frog_y,d1 ; Get current Y
271+ sub.w #MOVE_SPEED,d1 ; Subtract (up = decrease Y)
272+ cmp.w #MIN_Y,d1 ; Compare with top boundary
273+ blt.s .no_up ; Skip if past boundary
274+ move.w d1,frog_y ; Store new Y
151275 .no_up:
152- ; --- Check Down ---
276+ ; --- Check Down (bit 0) ---
153277 btst #0,d0
154278 beq.s .no_down
155279 move.w frog_y,d1
156- add.w #MOVE_SPEED,d1
280+ add.w #MOVE_SPEED,d1 ; Add (down = increase Y)
157281 cmp.w #MAX_Y,d1
158- bgt.s .no_down
282+ bgt.s .no_down ; Skip if past boundary
159283 move.w d1,frog_y
160284 .no_down:
161- ; --- Check Left ---
285+ ; --- Check Left (bit 9) ---
162286 btst #9,d0
163287 beq.s .no_left
164288 move.w frog_x,d1
165- sub.w #MOVE_SPEED,d1
289+ sub.w #MOVE_SPEED,d1 ; Subtract (left = decrease X)
166290 cmp.w #MIN_X,d1
167291 blt.s .no_left
168292 move.w d1,frog_x
169293 .no_left:
170- ; --- Check Right ---
294+ ; --- Check Right (bit 1) ---
171295 btst #1,d0
172296 beq.s .no_right
173297 move.w frog_x,d1
174- add.w #MOVE_SPEED,d1
298+ add.w #MOVE_SPEED,d1 ; Add (right = increase X)
175299 cmp.w #MAX_X,d1
176300 bgt.s .no_right
177301 move.w d1,frog_x
178302 .no_right:
179303 rts
180304
181-;──────────────────────────────────────────────────────────────
182-; Update sprite control words from frog_x/frog_y
183-;──────────────────────────────────────────────────────────────
305+;──────────────────────────────────────────────────────────────────────────────
306+; UPDATE_SPRITE - Write position to sprite control words
307+;──────────────────────────────────────────────────────────────────────────────
308+; Hardware sprites have control words at the start of their data:
309+;
310+; Word 0: VSTART[7:0] << 8 | HSTART[8:1]
311+; (vertical start position, horizontal start / 2)
312+;
313+; Word 1: VSTOP[7:0] << 8 | VSTART[8] << 2 | VSTOP[8] << 1 | HSTART[0]
314+; (vertical stop position, plus extra bits for large positions)
315+;
316+; For our 16-pixel tall sprite starting at Y positions < 256, we can
317+; simplify: just pack VSTART and HSTART/2 into word 0, VSTOP into word 1.
318+
184319 update_sprite:
185- lea frog_data,a0
186- move.w frog_y,d0 ; D0 = Y position
187- move.w frog_x,d1 ; D1 = X position
320+ lea frog_data,a0 ; A0 = sprite data start
321+ move.w frog_y,d0 ; D0 = Y position (VSTART)
322+ move.w frog_x,d1 ; D1 = X position (HSTART)
188323
189- ; --- Control word 1: VSTART<<8 | HSTART>>1 ---
324+ ; Build control word 0: VSTART << 8 | HSTART >> 1
190325 move.w d0,d2 ; D2 = VSTART
191326 lsl.w #8,d2 ; Shift to high byte
192- lsr.w #1,d1 ; HSTART / 2
193- or.b d1,d2 ; Combine
194- move.w d2,(a0) ; Write to sprite
327+ lsr.w #1,d1 ; HSTART / 2 (sprites use half-res X)
328+ or.b d1,d2 ; Combine into low byte
329+ move.w d2,(a0) ; Write to sprite control word 0
195330
196- ; --- Control word 2: VSTOP<<8 | control bits ---
197- add.w #FROG_HEIGHT,d0 ; VSTOP = VSTART + height
331+ ; Build control word 1: VSTOP << 8
332+ add.w #FROG_HEIGHT,d0 ; D0 = VSTOP (VSTART + height)
198333 lsl.w #8,d0 ; Shift to high byte
199- move.w d0,2(a0) ; Write to sprite
334+ move.w d0,2(a0) ; Write to sprite control word 1
200335
201336 rts
202337
203-;──────────────────────────────────────────────────────────────
204-; Variables
205-;──────────────────────────────────────────────────────────────
206-frog_x: dc.w 160
207-frog_y: dc.w 180
338+;══════════════════════════════════════════════════════════════════════════════
339+; VARIABLES
340+;══════════════════════════════════════════════════════════════════════════════
341+; These are in the code section so they're in Chip RAM with everything else.
342+; The 68000 can access them with PC-relative addressing for efficiency.
208343
209-;══════════════════════════════════════════════════════════════
344+frog_x: dc.w 160 ; Current horizontal position
345+frog_y: dc.w 180 ; Current vertical position
346+
347+;══════════════════════════════════════════════════════════════════════════════
210348 ; COPPER LIST
211-;══════════════════════════════════════════════════════════════
349+;══════════════════════════════════════════════════════════════════════════════
350+; The Copper executes simple instructions synchronised to the video beam.
351+; Each instruction is 32 bits (two 16-bit words).
352+;
353+; MOVE instruction: $XXYY,$VVVV
354+; XX = register offset / 2 (bits 8-1 of register address)
355+; YY = 00 (identifies this as a MOVE)
356+; VVVV = 16-bit value to write
357+;
358+; WAIT instruction: $VVHH,$FFFE
359+; VV = vertical position to wait for
360+; HH = horizontal position to wait for
361+; $FFFE = identifies this as a WAIT (bits 0 and 15 clear)
362+;
363+; The $07 in our WAITs means "wait for horizontal position 7" which is
364+; just after the left edge of the visible screen.
212365
213366 copperlist:
214- dc.w COLOR00,$0000 ; Black border at top
367+ dc.w COLOR00,$0000 ; MOVE: Set background to black
215368
216369 ; --- Sprite 0 palette (colours 17-19) ---
217- dc.w $01a2,COLOUR_FROG
218- dc.w $01a4,COLOUR_EYES
219- dc.w $01a6,COLOUR_OUTLINE
370+ ; Sprites 0-1 share colours 16-19 ($1A0-$1A6)
371+ ; Colour 16 ($1A0) is transparent, 17-19 are the sprite colours
372+ dc.w $01a2,COLOUR_FROG ; MOVE: Colour 17 = frog body
373+ dc.w $01a4,COLOUR_EYES ; MOVE: Colour 18 = eyes
374+ dc.w $01a6,COLOUR_OUTLINE ; MOVE: Colour 19 = outline
220375
221376 ; --- Sprite 0 pointer ---
222- dc.w SPR0PTH
223-sprpth_val: dc.w $0000
224- dc.w SPR0PTL
225-sprptl_val: dc.w $0000
377+ ; These values are filled in by the CPU at startup
378+ dc.w SPR0PTH ; MOVE: SPR0PTH register ($120)
379+sprpth_val: dc.w $0000 ; Value: high word of sprite address
380+ dc.w SPR0PTL ; MOVE: SPR0PTL register ($122)
381+sprptl_val: dc.w $0000 ; Value: low word of sprite address
226382
227- ; === HOME ZONE ===
228- dc.w $2c07,$fffe
229- dc.w COLOR00,COLOUR_HOME
383+ ; === HOME ZONE (line $2C = 44) ===
384+ dc.w $2c07,$fffe ; WAIT for line 44, position 7
385+ dc.w COLOR00,COLOUR_HOME ; MOVE: Background = green
230386
231- ; === WATER ZONE (5 lanes) ===
232- dc.w $4007,$fffe
233- dc.w COLOR00,COLOUR_WATER
387+ ; === WATER ZONE (5 lanes with wave highlights) ===
388+ dc.w $4007,$fffe ; WAIT for line 64
389+ dc.w COLOR00,COLOUR_WATER ; Dark blue
234390
235- dc.w $4c07,$fffe
236- dc.w COLOR00,COLOUR_WAVE
391+ dc.w $4c07,$fffe ; WAIT for line 76
392+ dc.w COLOR00,COLOUR_WAVE ; Light blue highlight
237393
238- dc.w $5407,$fffe
239- dc.w COLOR00,COLOUR_WATER
394+ dc.w $5407,$fffe ; WAIT for line 84
395+ dc.w COLOR00,COLOUR_WATER ; Dark blue
240396
241- dc.w $5c07,$fffe
242- dc.w COLOR00,COLOUR_WAVE
397+ dc.w $5c07,$fffe ; WAIT for line 92
398+ dc.w COLOR00,COLOUR_WAVE ; Light blue highlight
243399
244- dc.w $6407,$fffe
245- dc.w COLOR00,COLOUR_WATER
400+ dc.w $6407,$fffe ; WAIT for line 100
401+ dc.w COLOR00,COLOUR_WATER ; Dark blue
246402
247- ; === MEDIAN (safe zone) ===
403+ ; === MEDIAN (safe zone, line $6C = 108) ===
248404 dc.w $6c07,$fffe
249405 dc.w COLOR00,COLOUR_MEDIAN
250406
251- ; === ROAD ZONE (4 lanes) ===
252- dc.w $7807,$fffe
407+ ; === ROAD ZONE (4 lanes with markings) ===
408+ dc.w $7807,$fffe ; Line 120 - road
253409 dc.w COLOR00,COLOUR_ROAD
254410
255- dc.w $8407,$fffe
411+ dc.w $8407,$fffe ; Line 132 - marking
256412 dc.w COLOR00,COLOUR_MARKER
257413
258- dc.w $8807,$fffe
414+ dc.w $8807,$fffe ; Line 136 - road
259415 dc.w COLOR00,COLOUR_ROAD
260416
261- dc.w $9407,$fffe
417+ dc.w $9407,$fffe ; Line 148 - marking
262418 dc.w COLOR00,COLOUR_MARKER
263419
264- dc.w $9807,$fffe
420+ dc.w $9807,$fffe ; Line 152 - road
265421 dc.w COLOR00,COLOUR_ROAD
266422
267- dc.w $a407,$fffe
423+ dc.w $a407,$fffe ; Line 164 - marking
268424 dc.w COLOR00,COLOUR_MARKER
269425
270- dc.w $a807,$fffe
426+ dc.w $a807,$fffe ; Line 168 - road
271427 dc.w COLOR00,COLOUR_ROAD
272428
273- ; === START ZONE ===
429+ ; === START ZONE (line $B4 = 180) ===
274430 dc.w $b407,$fffe
275431 dc.w COLOR00,COLOUR_START
276432
277- dc.w $c007,$fffe
433+ dc.w $c007,$fffe ; Line 192 - border
278434 dc.w COLOR00,COLOUR_BORDER
279435
280- ; === BOTTOM ===
436+ ; === BOTTOM (line $F0 = 240) ===
281437 dc.w $f007,$fffe
282- dc.w COLOR00,$0000
438+ dc.w COLOR00,$0000 ; Black
283439
284- ; End of copper list
285- dc.w $ffff,$fffe
440+ ; === END OF COPPER LIST ===
441+ dc.w $ffff,$fffe ; WAIT for impossible position
442+ ; This effectively halts the Copper
443+ ; until next frame when it restarts
286444
287-;──────────────────────────────────────────────────────────────
445+;══════════════════════════════════════════════════════════════════════════════
288446 ; SPRITE DATA
289-;──────────────────────────────────────────────────────────────
290- even
447+;══════════════════════════════════════════════════════════════════════════════
448+; Hardware sprites are 16 pixels wide and up to 256 lines tall.
449+; Each line is 4 bytes: two 16-bit words (plane 0 and plane 1).
450+;
451+; The two planes combine to give 4 colours per pixel:
452+; Plane0=0, Plane1=0 -> Transparent
453+; Plane0=1, Plane1=0 -> Colour 17 (green body)
454+; Plane0=0, Plane1=1 -> Colour 18 (yellow eyes)
455+; Plane0=1, Plane1=1 -> Colour 19 (black outline)
456+
457+ even ; Ensure word alignment
291458 frog_data:
292- dc.w $b450,$c400 ; Control words (updated by code)
459+ ; Control words (updated by update_sprite)
460+ dc.w $b450,$c400 ; Default: Y=180, X=160
293461
294- ; 16 lines of sprite data (plane0, plane1)
295- dc.w $0000,$0000
296- dc.w $07e0,$0000
297- dc.w $1ff8,$0420
298- dc.w $3ffc,$0a50
299- dc.w $7ffe,$1248
300- dc.w $7ffe,$1008
301- dc.w $ffff,$2004
302- dc.w $ffff,$0000
303- dc.w $ffff,$0000
304- dc.w $7ffe,$2004
305- dc.w $7ffe,$1008
306- dc.w $3ffc,$0810
307- dc.w $1ff8,$0420
308- dc.w $07e0,$0000
309- dc.w $0000,$0000
310- dc.w $0000,$0000
462+ ; 16 lines of image data (plane0, plane1)
463+ ; Each pair: plane0 bits | plane1 bits
464+ dc.w $0000,$0000 ; ................
465+ dc.w $07e0,$0000 ; .....######.....
466+ dc.w $1ff8,$0420 ; ...##########... (with eye hints)
467+ dc.w $3ffc,$0a50 ; ..############..
468+ dc.w $7ffe,$1248 ; .##############. (eyes visible)
469+ dc.w $7ffe,$1008 ; .##############.
470+ dc.w $ffff,$2004 ; ################
471+ dc.w $ffff,$0000 ; ################
472+ dc.w $ffff,$0000 ; ################
473+ dc.w $7ffe,$2004 ; .##############.
474+ dc.w $7ffe,$1008 ; .##############.
475+ dc.w $3ffc,$0810 ; ..############..
476+ dc.w $1ff8,$0420 ; ...##########...
477+ dc.w $07e0,$0000 ; .....######.....
478+ dc.w $0000,$0000 ; ................
479+ dc.w $0000,$0000 ; ................
311480
312- ; End marker
313- dc.w $0000,$0000
481+ ; End marker (required by hardware)
482+ dc.w $0000,$0000 ; Tells chipset "no more sprite data"
314483