amiga-bootcamp/08_graphics/sprites.md
2026-04-26 14:46:18 -04:00

11 KiB
Raw Blame History

← Home · Graphics

Hardware Sprites — DMA Engine, Multiplexing, and Tricks

Overview

The Amiga has 8 hardware sprites, each 16 pixels wide with 3 colors + 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 17 are available for application use.

flowchart LR
    subgraph "Chip RAM"
        SD0["Sprite 0 data<br/>(mouse pointer)"]
        SD1["Sprite 1 data"]
        SD2["Sprite 2-7 data"]
    end

    subgraph "Custom Chips (Denise/Lisa)"
        DMA["Sprite DMA<br/>(8 channels)"] --> MUX["Priority MUX"]
        PF["Playfield<br/>(bitplane data)"] --> MUX
        MUX --> DAC["Color DAC<br/>→ Video Out"]
    end

    SD0 --> DMA
    SD1 --> DMA
    SD2 --> DMA

    subgraph "Copper"
        COP["Copper List"] -->|"Set SPRxPTH/L<br/>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 Color Encoding

Pixel color = (DATB_bit << 1) | DATA_bit

  00 = transparent (playfield shows through)
  01 = sprite color 1
  10 = sprite color 2
  11 = sprite color 3

Header Bit Layout

Word 0 (SPRxPOS):
  Bits 158: VSTART[7:0]     (vertical start line, low 8 bits)
  Bits 70:  HSTART[8:1]     (horizontal start, in low-res pixels ÷ 2)

Word 1 (SPRxCTL):
  Bits 158: 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 Color Palette

Each pair of sprites shares 3 color registers (color 0 = transparent for all):

Sprite Pair Color Registers Custom Addresses Notes
01 COLOR17COLOR19 $DFF1A2$DFF1A6 Pair with mouse pointer
23 COLOR21COLOR23 $DFF1AA$DFF1AE
45 COLOR25COLOR27 $DFF1B2$DFF1B6
67 COLOR29COLOR31 $DFF1BA$DFF1BE
/* Set sprite 0-1 colors directly: */
custom->color[17] = 0xF00;  /* red */
custom->color[18] = 0x0F0;  /* green */
custom->color[19] = 0xFFF;  /* white */

Attached Sprites — 15 Colors

Two sprites from the same pair can be attached to form a single 15-color (+ transparent) sprite:

flowchart LR
    subgraph "Normal (2 independent sprites)"
        S0["Sprite 0<br/>3 colors"] --- S1["Sprite 1<br/>3 colors"]
    end

    subgraph "Attached (1 wide-color sprite)"
        SA["Sprites 0+1 attached<br/>4 bits per pixel<br/>15 colors + transparent"]
    end

    style SA fill:#c8e6c9,stroke:#2e7d32,color:#333

When attached, the even sprite provides bits 01 and the odd sprite provides bits 23 of the color index. The 4-bit value indexes into color registers 1631.

/* 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:

flowchart TD
    subgraph "Screen (top to bottom)"
        Z1["Lines 0-50:<br/>Sprite 2 = Enemy A"]
        Z2["Lines 60-110:<br/>Sprite 2 = Enemy B"]
        Z3["Lines 120-170:<br/>Sprite 2 = Enemy C"]
        Z4["Lines 180-230:<br/>Sprite 2 = Enemy D"]
    end

    COP["Copper List"] -->|"WAIT line 0<br/>Set SPR2PT → Enemy A"| Z1
    COP -->|"WAIT line 55<br/>Set SPR2PT → Enemy B"| Z2
    COP -->|"WAIT line 115<br/>Set SPR2PT → Enemy C"| Z3
    COP -->|"WAIT line 175<br/>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)
Colors (single) 3 + transparent 3 + transparent
Colors (attached) 15 + transparent 15 + transparent
Horizontal resolution Low-res ÷ 2 Same (unchanged)
/* 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):

/* 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 */
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

/* 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

/* 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-driven sprite multiplexing
  • See also: rastport.md — BOBs (software sprites via Blitter)
  • See also: memory_types.md — sprite data must reside in Chip RAM (sprite DMA)