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-platform data-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

AspectDetail
Also calledCircular buffer, cyclic buffer
OperationsO(1) read, O(1) write
MemoryFixed, pre-allocated
Common usesAudio, networking, input history

How It Works

PointerPurpose
Head (write)Where the next write goes
Tail (read)Where the next read comes from
WrapWhen 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

OperationLogic
Writebuffer[head] = value; head = (head + 1) % size
Readvalue = buffer[tail]; tail = (tail + 1) % size
Full(head + 1) % size == tail
Emptyhead == 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).

FrameHead pointer (snake's head)Tail pointer (snake's tail)
MoveWrite new position; advance headRead (erase) old position; advance tail
GrowWrite new position; advance headLeave the tail alone — the snake gains a segment
ShrinkDon't writeAdvance 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

UseBenefit
Fighting gamesStore last N inputs for combos
ReplaysRecord input history
Undo systemsFixed-depth undo stack
Ghost racingStore position history

Audio

ApplicationWhy Ring Buffer
StreamingProducer/consumer decoupling
EffectsDelay lines are ring buffers
MixingLock-free audio threads
Amiga PaulaEach 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 DMCThe 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 sizeWrap methodCycles
256 bytesNatural 8-bit overflow — no AND needed0
128 bytesAND #$7F2
64 bytesAND #$3F2
Non-power-of-twoCompare 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

SystemUsage
FMOD/WwiseAudio streaming buffers
TCP/IP stacksPacket receive buffers
GGPO (rollback)Input history for netcode
LoggingCircular 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

AdvantageDisadvantage
No allocationFixed maximum size
O(1) operationsCan overflow if full
Cache-friendlyMust handle wrap-around
Lock-free possiblePower-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