Skip to content
Techniques & Technology

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.

cross-platformdata-structureaudionetworkingmemorysnake

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.

See Also