Contents
Introduction
Ring buffers, also called circular buffers, circular queues, or cyclic buffers, are data structures where the end of the storage range wraps back around to the beginning. They are especially useful for First In, First Out (FIFO) patterns and continuous data streams. Ring buffers are a fundamental structure, and some early platforms even included hardware support for ring-buffer-style behavior.
You have probably seen ring buffers at work without realizing it. Device inputs, such as keyboards, often queue commands through a ring buffer. Output streams use them too. Games commonly use ring buffers for player input history, internal state tracking, visual effects, and mechanics such as:
- Serpentine or segmented entities
- Clone power-ups, such as Ninja Gaiden II on NES
- Slaves, options, and satellites in shoot ’em ups
For example, in Ninja Gaiden II, Ryu’s position is recorded into a 64-slot ring buffer as he moves. When Ryu has one spirit clone, the clone reads position data from slot 31 of the buffer. The second clone reads from slot 63. On screen, this creates the convincing illusion that the clones are tracing Ryu’s movement.

OpenBOR
In OpenBOR, ring buffers are usually built from indexed arrays.
The secret sauce is an incremental cursor combined with modulo. Sometimes the cursor is an internal clock, but it is often simpler to keep a manual counter and increment it whenever the buffer updates. On each update, divide the cursor by the ring buffer size and keep the integer remainder. That remainder becomes the ring buffer index.
Sounds complex? It is not. The whole operation is one short line using the modulo operator (%). Modulo returns the integer remainder after division, giving us a simple way to wrap numbers into a fixed range.
int slot = cursor % BUFFER_SIZE; // Wrap cursor into valid buffer range.
If BUFFER_SIZE is 6, then cursor % BUFFER_SIZE can only return values from 0 through 5.
Here is what happens as cursor value increases:
| Cursor | Calculation | Resulting Slot |
|---|---|---|
| 0 | 0 % 6 | 0 |
| 1 | 1 % 6 | 1 |
| 2 | 2 % 6 | 2 |
| 3 | 3 % 6 | 3 |
| 4 | 4 % 6 | 4 |
| 5 | 5 % 6 | 5 |
| 6 | 6 % 6 | 0 |
| 7 | 7 % 6 | 1 |
| 8 | 8 % 6 | 2 |
| 9 | 9 % 6 | 3 |
| 10 | 10 % 6 | 4 |
| 11 | 11 % 6 | 5 |
| 12 | 12 % 6 | 0 |
Notice how the slot wraps back to 0 after it reaches 5. That is the “ring” in ring buffer. The cursor can keep counting upward forever, but modulo keeps the actual storage index inside the valid array range.
Typical ring buffer write logic follows this pattern:
#define BUFFER_SIZE 6
/* Get existing cursor value and initialize to 0 if needed. */
int cursor = get(data, DATA_CURSOR);
if(typeof(cursor) != openborconstant("VT_INTEGER")) {
cursor = 0;
}
/* Apply modulo to get target slot. */
int slot = cursor % BUFFER_SIZE;
/* Write data. */
set(data, DATA_START + slot, value);
/* Increment and write cursor value. */
cursor++;
set(data, DATA_CURSOR, cursor);
Each time this runs, the script writes to the next slot. Once it reaches the end of the buffer, it wraps around and starts overwriting the oldest entries. The full shadow trail example below uses this same pattern to record and draw position history.
Tip: There’s no reason the cursor can’t live inside the ring buffer array. In fact, that’s usually the cleanest approach. This is how our shadow trail example above works, and it is why the flattened layout uses a start offset. The first array element stores the incremental cursor count, while the remaining elements hold the ring buffer data. Everything stays packaged together, avoids an external variable lookup, and keeps the structure organized.
[cursor][x0][y0][z0][sprite0][time0][x1][y1][z1][sprite1][time1]...

Advanced
If you want to get really cocky, constrain your buffer sizes to a power of two:
2, 4, 8, 16, 32, 64, ...
Then you can use a bit mask instead of modulo.
#define BUFFER_SIZE 8
#define BUFFER_MASK (BUFFER_SIZE - 1)
index = cursor & BUFFER_MASK;
cursor++;
Why would you do this? Speed! Speeeeeeed!
Ring buffers are already fast, but bitwise AND is cheaper than division, and modulo is often implemented as division unless the compiler or runtime can optimize it away. On modern hardware, the difference is usually tiny. In a scripting environment like OpenBOR, it may not matter at all unless the operation is happening thousands of times per frame.
Still, on older systems this could be a vital optimization. The Ninja Gaiden II example above uses 64 slots, which is exactly the kind of buffer size you would choose for this trick. A 64-slot buffer can be wrapped with a mask of 63:
index = cursor & 63;
The NES 6502 CPU has no native multiply or divide instruction, so tricks like this mattered. Instead of performing a costly software modulo operation, the code can use a simple bitwise AND – exactly the kind of operation CPUs are built for.
Just remember the catch: this only works when the buffer size is a power of two. If your buffer size is 10, 20, 30, or any other non-power-of-two value, use modulo instead.
Considerations
Advantages
- Fast – When built from an indexed array, ring buffers are extremely light. The modulo and array operations are simple enough for hot-path logic such as player input tracking, position history, and visual effects.
- Efficient – Well-designed ring buffers can use less memory than other structures performing the same task. Their memory cost is also predictable. In the NES Ninja Gaiden II example, Ryu’s position buffer uses only 128 bytes – less storage than this sentence in plain text, and roughly six percent of the NES’s tiny 2KB of work RAM.
- Simple – Ring buffers are easy to assemble once you understand the pattern. You do not need much script to make them work.
- Expandable – Need smoother trails, more afterimages, or longer history? Increase the controlling buffer size constant and the same structure still works.
Drawbacks
- Static – Ring buffers do one job well, but they are not flexible general-purpose containers. When data needs to grow dynamically, another structure is usually a better fit.
- Resident – Ring buffers are efficient, but their memory cost is constant. The storage is reserved whether every slot is currently useful or not.
- Specialized – Ring buffers are ideal for certain tasks and poorly suited to many others. They shine when you need predictable, repeating access to recent history.
1 Comment