Ring Buffer
Circular data, fixed memory
A fixed-size data structure that wraps around, enabling efficient FIFO queues without memory allocation—essential for audio, networking, and game mechanics like snake tails.
Overview
A ring buffer (circular buffer) is a fixed-size array that wraps around from end to beginning, treating the storage as a continuous loop. It enables efficient first-in-first-out (FIFO) operations without memory allocation, making it essential for audio processing, network packets, and game mechanics like snake body segments.
Fast Facts
| Aspect | Detail |
|---|---|
| Also called | Circular buffer, cyclic buffer |
| Operations | O(1) read, O(1) write |
| Memory | Fixed, pre-allocated |
| Common uses | Audio, networking, input history |
How It Works
| Pointer | Purpose |
|---|---|
| Head (write) | Where the next write goes |
| Tail (read) | Where the next read comes from |
| Wrap | When a pointer reaches size, it wraps back to 0 |
A write puts data at the head's current position and advances the head; a read takes data from the tail's position and advances the tail. Wrap-around is what makes the buffer circular — past the end, the next position is the start.
Initial state (3 items queued):
Buffer: [A][B][C][.][.][.][.][.]
^tail ^head
(read here) (write here)
After write D, write E:
Buffer: [A][B][C][D][E][.][.][.]
^tail ^head
After read A:
Buffer: [.][B][C][D][E][.][.][.]
^tail ^head
After 3 more writes (head wraps past end):
Buffer: [H][B][C][D][E][F][G][.]
^tail ^head
(head wrote F, G, H — H landed at index 0)
Both pointers chase each other around the ring; the buffer is empty when they meet, full when the head is one slot behind the tail.
Implementation
| Operation | Logic |
|---|---|
| Write | buffer[head] = value; head = (head + 1) % size |
| Read | value = buffer[tail]; tail = (tail + 1) % size |
| Full | (head + 1) % size == tail |
| Empty | head == tail |
The "full = head one ahead of tail" convention costs one slot — usable capacity is size - 1 rather than size. The alternative is a separate count or full flag; faster on RISC, identical on most 8-bit chips. Pick the one that suits your code.
Gaming Applications
Snake Body
The snake's body is a ring buffer of segment positions. Confusingly, the visible tail of the snake is at the buffer's tail pointer (oldest segment, the one that disappears first), and the snake's head is at the buffer's head pointer (newest segment, the one just added).
| Frame | Head pointer (snake's head) | Tail pointer (snake's tail) |
|---|---|---|
| Move | Write new position; advance head | Read (erase) old position; advance tail |
| Grow | Write new position; advance head | Leave the tail alone — the snake gains a segment |
| Shrink | Don't write | Advance the tail twice — segment falls off |
For an 8-bit Snake with a 64-segment maximum body, that's two zero-page bytes of state (head + tail), 64 × 2 bytes for x/y positions, and four AND-masked pointer increments per frame.
Input Replay
| Use | Benefit |
|---|---|
| Fighting games | Store last N inputs for combos |
| Replays | Record input history |
| Undo systems | Fixed-depth undo stack |
| Ghost racing | Store position history |
Audio
| Application | Why Ring Buffer |
|---|---|
| Streaming | Producer/consumer decoupling |
| Effects | Delay lines are ring buffers |
| Mixing | Lock-free audio threads |
| Amiga Paula | Each of Paula's four DMA channels reads samples from a hardware-managed ring; the CPU rewrites the sample-pointer / length each VBlank to chain new samples in |
| NES DMC | The DMC (sample) channel reads from a CPU-RAM ring and increments through it, re-firing CPU stalls each fetch |
Vintage Implementation (6502)
8-bit CPUs have no divide instruction, so the modulo in (head + 1) % size is expensive — unless size is a power of two, in which case it collapses to a bitwise AND:
| Buffer size | Wrap method | Cycles |
|---|---|---|
| 256 bytes | Natural 8-bit overflow — no AND needed | 0 |
| 128 bytes | AND #$7F | 2 |
| 64 bytes | AND #$3F | 2 |
| Non-power-of-two | Compare and branch (CMP size; BCC ok; SBC size) | 5-7 |
A 64-byte input ring on the C64:
; --- Ring state in zero page ---
ring_head: .byte 0
ring_tail: .byte 0
RING_MASK = $3F ; size = 64
; --- Buffer in main RAM ---
ring_buffer: .res 64
; Write A to the ring (caller checks not-full first)
ring_write:
ldx ring_head
sta ring_buffer,x
inx
txa
and #RING_MASK
sta ring_head
rts
; Read from the ring into A (caller checks not-empty first)
ring_read:
ldx ring_tail
lda ring_buffer,x
inx
txa
and #RING_MASK
sta ring_tail
rts
; Empty if head == tail
ring_empty:
lda ring_head
cmp ring_tail
rts ; Z set if empty
For a 256-byte ring the AND disappears entirely — INX wraps naturally on overflow — and the routine drops to about 9 cycles per write.
Modern Applications
| System | Usage |
|---|---|
| FMOD/Wwise | Audio streaming buffers |
| TCP/IP stacks | Packet receive buffers |
| GGPO (rollback) | Input history for netcode |
| Logging | Circular log files |
Lock-Free Benefits
Ring buffers enable lock-free producer/consumer patterns:
- Audio thread writes, main thread reads
- Network thread writes, game thread reads
- No mutexes needed with single producer/consumer
Trade-offs
| Advantage | Disadvantage |
|---|---|
| No allocation | Fixed maximum size |
| O(1) operations | Can overflow if full |
| Cache-friendly | Must handle wrap-around |
| Lock-free possible | Power-of-two size optimal |
Legacy
Ring buffers appear everywhere in systems programming. Understanding them connects game development to audio engineering, networking, and operating systems—fundamental computer science in action.