The **GEL (Graphics ELement)** system is the Amiga's high-level animation framework, built into `graphics.library`. It provides a managed abstraction over the three animation-capable hardware subsystems — **hardware sprites**, the **Blitter**, and the **Copper** — unifying them into a single sorted, rendered, and collision-detected object list.
The GEL system manages three object types in a priority-sorted doubly-linked list:
| Object | Full Name | Backing Hardware | Size Limit | Color Limit | CPU Cost | Typical Use |
The key insight is that the GEL system **does not bypass the OS** — it uses `graphics.library` internally and cooperates with Intuition. This makes it the correct choice for system-friendly applications. Games that take over the hardware typically implement their own blitter-based object systems instead.
### Why a Unified System?
On most platforms of the era, sprites and software-drawn objects were entirely separate subsystems. The Amiga's GEL system merges them:
- **VSprites** can transparently **fall back to BOB rendering** when all 8 hardware sprite channels are exhausted — the application code doesn't change
- All GEL objects share a **single Y-sorted render list**, so draw order is automatic
- **Collision detection** works uniformly across VSprites and BOBs
- The `SortGList()` → `DrawGList()` → `WaitTOF()` loop handles everything in one pass
### Historical Context — The 1985 Competitive Landscape
The GEL system was architecturally unprecedented. No competing platform of the era offered anything resembling a unified, hardware-abstracted animation compositor:
| **Amiga (OCS)** | 8 DMA sprites, 16px wide, Copper-multiplexed | Blitter-composited BOBs, arbitrary size | **Yes** — GEL system merges both into one sorted, collision-detected render list |
| **Atari ST** | None — no hardware sprites at all | CPU-driven block copies (no blitter until STE in 1989) | No — everything is manual software rendering |
| **Commodore 64** | 8 hardware sprites (VIC-II), 24px wide | Character-based; no blitter | No — sprites and character graphics are completely separate; collision is hw-only |
| **NES (Famicom)** | 64 OAM sprites, 8px wide, 8 per scanline | Tile-based background via PPU | No — OAM and background are independent subsystems with no unified API |
| **Apple Macintosh** | None | QuickDraw (CPU-only, 1-bit) | No — no sprites, no DMA, no animation framework whatsoever |
| **IBM PC (CGA/EGA)** | None | CPU block copies to video RAM | No — no hardware assistance; animation is entirely application responsibility |
| **Atari 7800** | Up to 100 sprites via display list | Background tiles | Partially — display list is a flat sorted structure, but no OS API or collision framework |
The Amiga was the only home computer where the OS itself provided:
This was essentially a **scene graph with a composer** — a concept that wouldn't become mainstream until GPU-accelerated window managers appeared 20 years later ([Quartz Composer](https://developer.apple.com/documentation/quartz/quartz-composer), Aero 2006).
> The arcade world had more sophisticated sprite hardware (Namco System 16, Sega System 16 — 128+ sprites with scaling and rotation), but these were fixed-function ASICs with no OS, no API, and no concept of resource sharing between applications. The Amiga's innovation was the **software architecture** layered on top of capable-but-limited hardware.
### Modern Analogies
The GEL system's design foreshadows patterns that became standard decades later:
| GEL System (1985) | Modern Equivalent | Shared Concept |
|---|---|---|
| Unified VSprite + BOB render list | **macOS WindowServer / Core Animation** | A compositor merges GPU-accelerated layers (≈ VSprites) and software-rendered surfaces (≈ BOBs) into a single display pass — apps don't choose the backend |
| `SortGList()` → `DrawGList()` | **Vulkan/Metal render graph** | A sorted submission pipeline where the framework decides resource scheduling and draw order |
| VSprite hw→sw fallback | **GPU tile-based deferred rendering** | When fast-path resources (tile memory, shader units) are exhausted, the driver transparently falls back to a slower path |
| `DoCollision()` with MeMask/HitMask | **Unity/Unreal collision layers** | Bitmask-based collision filtering — each object declares what it is and what it can hit |
| AnimOb velocity + acceleration | **Core Animation implicit animations** | The framework applies physics (timing curves, momentum) so the app just sets target values |
| Copper-driven sprite pointer reload | **Display controller scanout** | A DMA engine composites layers in sync with the display refresh — no CPU in the hot path |
### Pros and Cons (in 1985 Context)
| | Pro | Con |
|---|---|---|
| **Abstraction** | Write once — system picks hw sprites or blitter automatically | Abstraction layer costs CPU cycles on a 7.09 MHz 68000 |
| **Unified collision** | One API for all object types; no manual overlap checks | O(n²) scaling is brutal with >30 objects and no spatial index |
| **Auto sort/draw** | Correct painter's-order for free | Y-only sort — no X or Z priority control without workarounds |
| **OS integration** | Cooperates with Intuition; no system takeover required | Competing with Workbench for blitter time and DMA slots |
| **Animation engine** | Built-in velocity, acceleration, frame sequencing | Rigid fixed-point math; no easing curves, no interpolation |
| **Save/restore cycle** | Clean non-destructive compositing | 3 blitter ops per BOB per frame — expensive at scale |
---
## Hardware Foundation — Blitter, Copper, and Sprites
The GEL system is an API layer over three independent DMA engines. Understanding which hardware backs each operation is essential for performance tuning.
- Performs the **cookie-cut blit** (minterm `$CA`) to composite BOB imagery onto the playfield: `A`=mask, `B`=source image, `C`=background read-back, `D`=output
- **Saves and restores background** under each BOB via the `SaveBuffer` — this is what makes BOBs non-destructive
- Handles arbitrary sizes (not limited to 16px width like hardware sprites)
- See [blitter_programming.md](blitter_programming.md) for minterm details
**Copper (VSprite pointer management)**
- Reloads `SPRxPTH`/`SPRxPTL` registers every frame during vertical blank
- The GEL system's `DrawGList()` builds Copper instructions that point each hardware sprite channel to the correct VSprite data
- Enables **sprite multiplexing** — reusing the same hardware channel for multiple VSprites at different Y positions
- See [copper_programming.md](copper_programming.md) for Copper list construction
**Hardware Sprites (VSprite rendering)**
- 8 DMA channels in Denise/Lisa fetch sprite data from Chip RAM with **zero CPU overhead**
- Each channel: 16 pixels wide, 3 colors + transparent (or 15 colors when attached in pairs)
- The GEL system assigns VSprites to available hardware channels automatically; overflow VSprites are rendered as BOBs via the Blitter
### DMA Budget Impact
All three engines share the Chip RAM bus. During a typical animation frame:
| Active display | Sprite DMA fetches | 2 words/line/sprite | Continuous, interleaved with bitplane DMA |
| Between frames | Blitter save/restore + draw | Varies with BOB count | Competes with CPU for bus |
| Between frames | Blitter collision masks | Varies | Only if `DoCollision()` is called |
> [!WARNING]
> **BOBs are expensive.** Each BOB requires three blitter operations per frame: (1) restore previous background, (2) save new background, (3) cookie-cut blit. With 20 BOBs at 32×32 pixels across 4 bitplanes, the blitter is busy for a significant fraction of the frame time. Profile early.
A **VSprite** is the fundamental GEL object. It represents a small, moveable graphic element that the system will render using **hardware sprite DMA** if a channel is available, or **fall back to blitter-based BOB rendering** if all 8 hardware channels are occupied.
This dual-path rendering is the key architectural feature: the application creates VSprites without specifying how they will be rendered. The GEL manager decides at `DrawGList()` time based on availability.
> `ImageData`, `BorderLine`, `CollMask`, and `SprColors` **must reside in Chip RAM** (`AllocMem(size, MEMF_CHIP|MEMF_CLEAR)`). The Blitter and sprite DMA cannot access Fast RAM.
A **BOB** is an arbitrary-sized bitmap overlay composited onto the playfield using the Blitter. Unlike VSprites (which are limited to 16 pixels wide and 3 colors), BOBs can be **any size** and use the **full playfield palette**.
Every BOB has a paired **VSprite** that provides its position, collision data, and list linkage. The BOB struct adds blitter-specific fields: the save buffer, the shadow mask, and optional double-buffering and animation component links.
### The BOB Rendering Cycle
Each frame, `DrawGList()` performs three blitter operations per BOB:
```
Frame N:
1. RESTORE — Blit SaveBuffer back to screen at OldX,OldY
(erase previous frame's image)
2. SAVE — Copy screen rectangle at new X,Y into SaveBuffer
(preserve background before drawing)
3. DRAW — Cookie-cut blit: ImageData through ImageShadow
onto screen at X,Y (composite the BOB)
```
This is why `SaveBuffer` must be allocated large enough to hold the BOB's footprint plus alignment padding. The `ImageShadow` is a 1-bitplane mask — 1 where the BOB is opaque, 0 where transparent.
An **AnimOb** (Animation Object) adds automatic **frame sequencing**, **velocity**, and **acceleration** on top of the BOB system. It models a complete animated entity — a walking character, an explosion, a rotating power-up — as a tree of **AnimComps** (animation components), each being a sequence of animation frames.
### Concepts
| Term | Meaning |
|---|---|
| **AnimOb** | The top-level animation object with position, velocity, acceleration |
| **AnimComp** | One "part" of the object (e.g., a character's body, or its sword) |
| **Sequence** | A ring of AnimComps linked via `NextSeq`/`PrevSeq` — the animation frames |
| **Clock/Timer** | Per-component frame counter; when it hits `TimeSet`, advances to next frame |
| **RingXTrans/RingYTrans** | Position offset applied when the sequence wraps (for walk cycles) |
| **YVel/XVel** | Per-frame velocity (added to position each `Animate()` call) |
| **YAccel/XAccel** | Per-frame acceleration (added to velocity each `Animate()` call) |
### How Animate() Works
Each call to `Animate()`:
1. For every AnimOb in the list:
- Add `XAccel` to `XVel`, `YAccel` to `YVel`
- Add `XVel` to `AnX`, `YVel` to `AnY`
2. For every AnimComp in the AnimOb:
- Decrement `Timer`
- If `Timer` reaches 0:
- Advance to `NextSeq` (next frame in the ring)
- Reset `Timer` to `TimeSet`
- Update the BOB's `ImageData` and `ImageShadow`
- Apply component's relative offset to AnimOb position
/* Mark enemy for removal, spawn explosion, etc. */
}
}
```
> [!NOTE]
> `DoCollision()` performs **O(n²)** pairwise checks. For large object counts (50+), this becomes a significant per-frame cost. Consider spatial partitioning or manual collision checks for performance-critical games.
---
## Double Buffering with BOBs
To eliminate flicker, BOBs can be double-buffered using `DBufPacket`:
```c
struct DBufPacket dbuf;
WORD *buf1, *buf2;
/* Allocate two buffers: */
LONG bufSize = /* same as SaveBuffer calculation */;
buf1 = AllocMem(bufSize, MEMF_CHIP | MEMF_CLEAR);
buf2 = AllocMem(bufSize, MEMF_CHIP | MEMF_CLEAR);
dbuf.BufY = dbuf.BufX = 0;
dbuf.BufPath = NULL; /* set by system */
myBob.DBuffer = &dbuf;
/* The system alternates between buf1 and buf2 each frame,
so the restore operation never tears the current frame's display. */
```
For full-screen double buffering (swapping entire bitplane pointers), use `ChangeVPBitMap()` instead — that's a ViewPort-level mechanism independent of the GEL system.
---
## When to Use the GEL System
### Good Use Cases
| Scenario | Why GELs Work Well |
|---|---|
| **System-friendly applications** | GELs cooperate with Intuition and the OS display system |
| **High-performance games** (20+ objects) | GEL overhead: sorting, collision, save/restore per BOB | Custom blitter object manager with dirty-rectangle lists |
| **System-takeover games** | GELs assume OS is running; adds overhead | Direct blitter/sprite register programming |
| **Large scrolling playfields** | GELs don't manage scrolling; BOB restore interacts poorly with scroll | Copper-driven scroll with manual blitter objects |
| **Complex z-ordering** | GELs sort by Y only; no arbitrary Z | Custom draw-order lists |
| **Tile-based rendering** | GELs aren't designed for tile maps | Blitter tile-copy routines |
> [!TIP]
> Most **commercial Amiga games** (Turrican, Shadow of the Beast, Speedball 2) did **not** use the GEL system. They implemented custom blitter object engines for maximum control over DMA scheduling, draw order, and memory layout. The GEL system is best understood as a convenience layer for **system-friendly software** and as a **reference implementation** of the patterns that custom engines replicate.
---
## Complete Example — Animated BOB
```c
#include <exec/types.h>
#include <graphics/gels.h>
#include <graphics/gfxmacros.h>
#include <proto/graphics.h>
#include <proto/exec.h>
#define BOB_WIDTH_WORDS 2 /* 32 pixels */
#define BOB_HEIGHT 24
#define BOB_DEPTH 4 /* 16 colors */
/* Pre-computed image data (must be in Chip RAM): */
extern WORD shipImage[]; /* BOB_WIDTH_WORDS * BOB_HEIGHT * BOB_DEPTH words */
extern WORD shipMask[]; /* BOB_WIDTH_WORDS * BOB_HEIGHT words (1 plane) */
struct GelsInfo gi;
struct VSprite headVS, tailVS;
struct VSprite shipVS;
struct Bob shipBob;
void SetupGELSystem(struct RastPort *rp)
{
memset(&gi, 0, sizeof(gi));
InitGels(&headVS, &tailVS, &gi);
rp->GelsInfo = &gi;
}
void CreateShipBob(void)
{
/* VSprite (provides position and collision): */
memset(&shipVS, 0, sizeof(shipVS));
shipVS.X = 100;
shipVS.Y = 80;
shipVS.Height = BOB_HEIGHT;
shipVS.Width = BOB_WIDTH_WORDS;
shipVS.Depth = BOB_DEPTH;
shipVS.Flags = SAVEBACK | OVERLAY;
shipVS.MeMask = 1; /* I am type 1 */
shipVS.HitMask = 2; /* I collide with type 2 */
shipVS.PlanePick = 0x0F; /* draw into planes 0-3 */
shipVS.PlaneOnOff = 0x00;
shipVS.ImageData = shipImage; /* Chip RAM */
/* Collision mask: */
shipVS.CollMask = AllocMem(
BOB_WIDTH_WORDS * BOB_HEIGHT * sizeof(WORD),
MEMF_CHIP | MEMF_CLEAR);
shipVS.BorderLine = AllocMem(
BOB_WIDTH_WORDS * sizeof(WORD),
MEMF_CHIP | MEMF_CLEAR);
InitMasks(&shipVS); /* auto-generate collision mask from image */