[← Home](../README.md) · [Graphics](README.md)
# Hardware Sprites — DMA Engine, Multiplexing, and Tricks
## Overview
The Amiga has **8 hardware sprites**, each 16 pixels wide with 3 colours + transparent. Sprites are entirely DMA-driven — the custom chips fetch sprite data from Chip RAM and composite them over the playfield with zero CPU overhead. The Copper reloads sprite pointers every frame.
Sprite 0 is reserved by Intuition for the **mouse pointer**. Sprites 1–7 are available for application use.
```mermaid
flowchart LR
subgraph "Chip RAM"
SD0["Sprite 0 data
(mouse pointer)"]
SD1["Sprite 1 data"]
SD2["Sprite 2-7 data"]
end
subgraph "Custom Chips (Denise/Lisa)"
DMA["Sprite DMA
(8 channels)"] --> MUX["Priority MUX"]
PF["Playfield
(bitplane data)"] --> MUX
MUX --> DAC["Colour DAC
→ Video Out"]
end
SD0 --> DMA
SD1 --> DMA
SD2 --> DMA
subgraph "Copper"
COP["Copper List"] -->|"Set SPRxPTH/L
every frame"| DMA
end
style DMA fill:#e8f4fd,stroke:#2196f3,color:#333
style COP fill:#fff9c4,stroke:#f9a825,color:#333
```
---
## Sprite DMA Data Format
Each sprite is stored as a contiguous block in Chip RAM:
```
┌──────────────────────────────────────────┐
│ Header Word 0: VSTART/HSTART │ Position control
│ Header Word 1: VSTOP/Control │
├──────────────────────────────────────────┤
│ Line 0: DATA word (bit 0 of each pixel) │ ← 16 pixels per line
│ Line 0: DATB word (bit 1 of each pixel) │
├──────────────────────────────────────────┤
│ Line 1: DATA word │
│ Line 1: DATB word │
├──────────────────────────────────────────┤
│ ...repeat for each line... │
├──────────────────────────────────────────┤
│ Terminator: 0x0000 │ End marker
│ Terminator: 0x0000 │
└──────────────────────────────────────────┘
```
### Pixel Colour Encoding
```
Pixel colour = (DATB_bit << 1) | DATA_bit
00 = transparent (playfield shows through)
01 = sprite colour 1
10 = sprite colour 2
11 = sprite colour 3
```
### Header Bit Layout
```
Word 0 (SPRxPOS):
Bits 15–8: VSTART[7:0] (vertical start line, low 8 bits)
Bits 7–0: HSTART[8:1] (horizontal start, in low-res pixels ÷ 2)
Word 1 (SPRxCTL):
Bits 15–8: VSTOP[7:0] (vertical stop line, low 8 bits)
Bit 7: unused
Bit 6: unused
Bit 5: unused
Bit 4: unused
Bit 3: VSTART[8] (bit 8 of start — for lines > 255)
Bit 2: VSTOP[8] (bit 8 of stop)
Bit 1: HSTART[0] (low bit of horizontal position)
Bit 0: ATTACH (1 = attached to previous sprite)
```
> [!IMPORTANT]
> **Horizontal position is in low-res pixel units ÷ 2** — a sprite can only be positioned on even low-res pixel boundaries. In hires/superhires modes, this means sprites have coarser horizontal positioning than playfield pixels.
---
## Sprite Colour Palette
Each pair of sprites shares 3 colour registers (colour 0 = transparent for all):
| Sprite Pair | Colour Registers | Custom Addresses | Notes |
|---|---|---|---|
| 0–1 | `COLOR17`–`COLOR19` | `$DFF1A2`–`$DFF1A6` | Pair with mouse pointer |
| 2–3 | `COLOR21`–`COLOR23` | `$DFF1AA`–`$DFF1AE` | |
| 4–5 | `COLOR25`–`COLOR27` | `$DFF1B2`–`$DFF1B6` | |
| 6–7 | `COLOR29`–`COLOR31` | `$DFF1BA`–`$DFF1BE` | |
```c
/* Set sprite 0-1 colours directly: */
custom->color[17] = 0xF00; /* red */
custom->color[18] = 0x0F0; /* green */
custom->color[19] = 0xFFF; /* white */
```
---
## Attached Sprites — 15 Colours
Two sprites from the same pair can be **attached** to form a single 15-colour (+ transparent) sprite:
```mermaid
flowchart LR
subgraph "Normal (2 independent sprites)"
S0["Sprite 0
3 colours"] --- S1["Sprite 1
3 colours"]
end
subgraph "Attached (1 wide-colour sprite)"
SA["Sprites 0+1 attached
4 bits per pixel
15 colours + transparent"]
end
style SA fill:#c8e6c9,stroke:#2e7d32,color:#333
```
When attached, the even sprite provides bits 0–1 and the odd sprite provides bits 2–3 of the colour index. The 4-bit value indexes into colour registers 16–31.
```c
/* Enable attachment: set bit 0 of odd sprite's CTL word */
oddSpriteData[1] |= 0x0001; /* ATTACH bit */
```
---
## Sprite Multiplexing — More Than 8 Sprites
The hardware only supports 8 simultaneous sprites, but demos and games use **multiplexing** to display many more by reusing sprite channels on different scanlines:
```mermaid
flowchart TD
subgraph "Screen (top to bottom)"
Z1["Lines 0-50:
Sprite 2 = Enemy A"]
Z2["Lines 60-110:
Sprite 2 = Enemy B"]
Z3["Lines 120-170:
Sprite 2 = Enemy C"]
Z4["Lines 180-230:
Sprite 2 = Enemy D"]
end
COP["Copper List"] -->|"WAIT line 0
Set SPR2PT → Enemy A"| Z1
COP -->|"WAIT line 55
Set SPR2PT → Enemy B"| Z2
COP -->|"WAIT line 115
Set SPR2PT → Enemy C"| Z3
COP -->|"WAIT line 175
Set SPR2PT → Enemy D"| Z4
style COP fill:#fff9c4,stroke:#f9a825,color:#333
```
The Copper waits for a line after one sprite ends, then reprograms the sprite pointer register for the next object. This gives effectively **unlimited sprites** as long as they don't overlap vertically on the same channel.
### Multiplexing Rules
| Rule | Explanation |
|---|---|
| No vertical overlap on same channel | Two sprites using the same DMA channel cannot appear on the same scanline |
| 1-line gap required | After a sprite ends (VSTOP), the DMA channel needs at least 1 blank line before starting the next |
| Copper must be fast enough | Copper WAIT + MOVE takes 2 DMA cycles; pointer reload = 2 MOVEs (PTH+PTL) |
| Max per scanline: 8 | Absolute hardware limit — 8 DMA channels, one fetch per channel per line |
---
## AGA Sprite Enhancements
| Feature | OCS/ECS | AGA |
|---|---|---|
| Width | 16 pixels | 16, 32, or 64 pixels (via FMODE) |
| Colours (single) | 3 + transparent | 3 + transparent |
| Colours (attached) | 15 + transparent | 15 + transparent |
| Horizontal resolution | Low-res ÷ 2 | Same (unchanged) |
```c
/* AGA: wider sprites via FMODE register */
custom->fmode = 3; /* 4× fetch — sprites become 64 pixels wide */
/* WARNING: this also affects bitplane fetch! */
```
---
## Sprite-Playfield Priority
Sprites interact with playfields via the **priority control register** (`BPLCON2`):
```c
/* BPLCON2 ($DFF104) priority bits: */
/* PF2P2-PF2P0: playfield 2 priority vs sprites */
/* PF1P2-PF1P0: playfield 1 priority vs sprites */
/* Priority order (front to back): */
/* SP01 > SP23 > SP45 > SP67 > PF1 > PF2 (default) */
/* Make sprites appear behind playfield 1: */
custom->bplcon2 = 0x0024; /* PF1 in front of all sprites */
```
```mermaid
flowchart LR
subgraph "Default Priority (front → back)"
direction LR
S01["Sprites 0-1"] --- S23["Sprites 2-3"] --- S45["Sprites 4-5"] --- S67["Sprites 6-7"] --- PF1["Playfield 1"] --- PF2["Playfield 2"]
end
style S01 fill:#ffcdd2,stroke:#c62828,color:#333
style PF1 fill:#c8e6c9,stroke:#2e7d32,color:#333
style PF2 fill:#bbdefb,stroke:#1565c0,color:#333
```
The priority can be configured so that sprites appear **between** playfield 1 and playfield 2 — creating the illusion of depth (e.g., a character walking behind foreground objects but in front of the background).
---
## Sprite Pointer Registers
| Register | Address | Sprite |
|---|---|---|
| `SPR0PTH/L` | `$DFF120–$DFF123` | Sprite 0 (mouse pointer) |
| `SPR1PTH/L` | `$DFF124–$DFF127` | Sprite 1 |
| `SPR2PTH/L` | `$DFF128–$DFF12B` | Sprite 2 |
| `SPR3PTH/L` | `$DFF12C–$DFF12F` | Sprite 3 |
| `SPR4PTH/L` | `$DFF130–$DFF133` | Sprite 4 |
| `SPR5PTH/L` | `$DFF134–$DFF137` | Sprite 5 |
| `SPR6PTH/L` | `$DFF138–$DFF13B` | Sprite 6 |
| `SPR7PTH/L` | `$DFF13C–$DFF13F` | Sprite 7 |
These must be set **every frame** — typically by the Copper list during vertical blank. If not set, the sprite DMA will fetch from wherever the pointer was left, producing garbage.
---
## OS-Level Sprite API
```c
/* graphics.library — system-friendly sprite access */
struct SimpleSprite ss;
WORD sprnum;
/* Obtain a free sprite (Intuition reserves sprite 0 for the pointer): */
sprnum = GetSprite(&ss, -1); /* -1 = any available */
if (sprnum >= 0)
{
ss.x = 100;
ss.y = 50;
ss.height = 16;
/* Set sprite image data (must be in Chip RAM!): */
ChangeSprite(NULL, &ss, spriteData);
/* Move sprite to screen position: */
MoveSprite(NULL, &ss, 100, 50);
/* Release when done: */
FreeSprite(sprnum);
}
```
### Custom Mouse Pointer
```c
/* Change the Intuition mouse pointer: */
UWORD pointerData[] = {
0x0000, 0x0000, /* reserved (position) */
/* 16 lines of sprite data: */
0x8000, 0xC000, /* line 0 */
0xC000, 0xE000, /* line 1 */
0xE000, 0xF000, /* line 2 */
/* ... etc ... */
0x0000, 0x0000 /* terminator */
};
SetPointer(window, pointerData, height, width, xOffset, yOffset);
/* Restore default: */
ClearPointer(window);
```
---
## Common Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Sprite data not in Chip RAM | DMA can't access it — sprite invisible or garbage | Use `AllocMem(MEMF_CHIP)` for sprite data |
| Not setting pointer every frame | Sprite DMA reads stale pointer → random data displayed | Use Copper list to reload SPRxPT |
| Forgetting `FreeSprite` | Sprite channel stays reserved → other apps can't use it | Always free in cleanup |
| Using sprite 0 directly | Conflicts with Intuition's mouse pointer | Use `GetSprite(-1)` to get a free one |
---
## References
- HRM: *Amiga Hardware Reference Manual* — Sprites chapter
- NDK39: `graphics/sprite.h`, `hardware/custom.h`
- ADCD 2.1: `GetSprite`, `MoveSprite`, `ChangeSprite`, `FreeSprite`
- See also: [copper_programming.md](copper_programming.md) — Copper-driven sprite multiplexing
- See also: [rastport.md](rastport.md) — BOBs (software sprites via Blitter)
- See also: [memory_types.md](../01_hardware/common/memory_types.md) — sprite data must reside in Chip RAM (sprite DMA)