# Circular Buffer Implementation in Windows Terminal: Low-Memory Scrolling with TextBuffer

> Discover the circular buffer implementation in Windows Terminal. Learn how TextBuffer uses modulo arithmetic for O(1) scrolling and efficient low-memory text storage.

- Repository: [Microsoft/terminal](https://github.com/microsoft/terminal)
- Tags: internals
- Published: 2026-02-26

---

**Windows Terminal implements its text storage as a circular buffer in the `TextBuffer` class to enable O(1) scrolling and efficient memory usage by mapping logical viewport coordinates to physical memory offsets via modulo arithmetic.**

The microsoft/terminal repository manages terminal screen content using a sophisticated circular buffer design located in [`src/buffer/out/textBuffer.cpp`](https://github.com/microsoft/terminal/blob/main/src/buffer/out/textBuffer.cpp). This implementation avoids expensive memory moves during scroll operations while maintaining fast random access to individual rows. By decoupling the logical display order from physical memory layout, the terminal can handle high-frequency output streams and large scrollback buffers with minimal overhead.

## Core Architecture and Physical Storage

The `TextBuffer` class allocates a single contiguous block of virtual memory sized for `height + 1` `ROW` structures. The additional row serves as a scratchpad for temporary operations. Rather than committing all memory upfront, the buffer uses demand paging to keep the memory footprint low.

### The `_firstRow` Anchor

The member variable `_firstRow` (type `til::CoordType`) stores the index of the logical top row currently displayed. This value acts as an offset into the physical storage array. When the terminal scrolls, only `_firstRow` changes; the underlying row data remains stationary in memory.

In [`src/buffer/out/textBuffer.cpp`](https://github.com/microsoft/terminal/blob/main/src/buffer/out/textBuffer.cpp), the `_reserve` method handles the initial allocation, while `_getRow` performs the critical translation between logical screen coordinates and physical offsets using the formula `(_firstRow + y) % _height`.

## How Row Mapping Works

Every public API that accesses row data forwards to `TextBuffer::_getRow(til::CoordType y)`. This method implements the circular buffer semantics:

1. **Logical to Physical Translation**: The function adds the requested Y coordinate to `_firstRow` and applies modulo `_height` to wrap around the buffer boundary.
2. **Constant Time Access**: Row lookup requires only integer addition and modulo operations, ensuring O(1) performance regardless of buffer size.
3. **Wrap-Around Handling**: When `_firstRow + y` exceeds the physical buffer size, the modulo operation automatically maps the index back to the beginning of the allocation.

This design allows the UI to treat the buffer as a linear array while the underlying implementation reuses memory blocks cyclically.

## Scrolling Operations Without Memory Moves

### Incrementing the Buffer

When new output pushes content beyond the visible viewport, `IncrementCircularBuffer` executes the circular buffer rotation:

- Prunes hyperlinks from the row being recycled (the oldest visible row).
- Resets that row's contents with the supplied fill attributes.
- Increments `_firstRow` and wraps to 0 if it reaches `_height`.

This operation appears in [`src/buffer/out/textBuffer.cpp`](https://github.com/microsoft/terminal/blob/main/src/buffer/out/textBuffer.cpp) at lines 111-131. The logical viewport shifts down by one row while the physical storage reassigns the previous top row to become the new bottom row.

### Arbitrary Range Scrolling

The `ScrollRows` method handles page-up, page-down, and programmatic scrolling. It accepts a starting row, range size, and signed delta value. The algorithm operates on logical coordinates and uses `CopyRow` internally, which respects the circular mapping through the same modulo arithmetic used in `_getRow`.

Located at lines 945-1001 in [`textBuffer.cpp`](https://github.com/microsoft/terminal/blob/main/textBuffer.cpp), this function moves contiguous blocks of rows up or down without reallocating memory or shifting the entire buffer contents.

## Memory Optimization and Resizing

### Clearing Scrollback History

The `ClearScrollback` method compacts the circular buffer to reduce memory pressure. It:

1. Calculates the new logical offset by adding `_firstRow` to the desired viewport position.
2. Resets `_firstRow` to 0, effectively rebasing the circular buffer to the absolute start of the allocation.
3. De-commits unused trailing memory pages, returning physical RAM to the system without destroying the buffer structure.

This approach preserves the circular buffer semantics while allowing the operating system to reclaim memory from discarded scrollback history.

### Terminal Resizing

When the window dimensions change, `ResizeTraditional` creates a new `TextBuffer` instance with the target dimensions rather than modifying the existing circular buffer. It:

- Allocates fresh storage for the new size.
- Copies visible rows from the old buffer using the logical coordinate system.
- Swaps the internal buffer pointers, discarding the old circular mapping.

This clean-slate approach avoids the complexity of rearranging circular buffer indices during dimension changes and prevents fragmentation issues.

## Practical Implementation Examples

### Accessing Rows by Logical Offset

```cpp
// Retrieve the 5th visible row (0-indexed)
TextBuffer& buffer = GetTextBuffer();
til::CoordType screenY = 5;
const ROW& row = buffer.GetRowByOffset(screenY);
// Internal calculation: (_firstRow + 5) % _height

```

### Handling New Output Lines

```cpp
// Called when output pushes beyond viewport bottom
TextAttribute fillAttr = buffer.GetCurrentAttributes();
buffer.IncrementCircularBuffer(fillAttr);
// _firstRow now points to the next physical row
// The previous top row is recycled as the new bottom row

```

### Scrolling a Page

```cpp
// Scroll viewport up by 5 rows (user pressed Page Up)
til::CoordType firstRow = 0;
til::CoordType rowCount = buffer.TotalRowCount();
til::CoordType delta = -5;
buffer.ScrollRows(firstRow, rowCount, delta);
// Modulo arithmetic handles wrap-around automatically

```

### Compacting After Clear

```cpp
// Clear scrollback and optimize memory
til::CoordType newFirstRow = 0;
til::CoordType rowsToKeep = buffer.TotalRowCount();
buffer.ClearScrollback(newFirstRow, rowsToKeep);
// Buffer rebased to start of allocation, unused pages de-committed

```

## Summary

- **O(1) Scrolling**: The circular buffer achieves constant-time scroll operations by incrementing `_firstRow` rather than moving memory blocks.
- **Virtual Memory Efficiency**: Only active viewport rows plus one scratchpad are committed; remaining capacity stays reserved but uncommitted.
- **Modulo Mapping**: The `(_firstRow + y) % _height` formula translates logical screen coordinates to physical storage locations in constant time.
- **Non-Destructive Resizing**: `ResizeTraditional` creates new buffers rather than rearranging existing circular indices, simplifying dimension changes.

## Frequently Asked Questions

### How does Windows Terminal map logical rows to physical storage?

Windows Terminal uses the `_getRow` method in [`src/buffer/out/textBuffer.cpp`](https://github.com/microsoft/terminal/blob/main/src/buffer/out/textBuffer.cpp) to translate logical Y coordinates to physical offsets. It calculates `(_firstRow + y) % _height`, where `_firstRow` tracks the current top of the viewport and `_height` represents the total buffer capacity. This modulo operation enables the circular wrap-around behavior without data movement.

### What happens when the circular buffer fills up during scrolling?

When output exceeds the visible height, `IncrementCircularBuffer` recycles the oldest row by resetting its contents with the current text attributes and incrementing `_firstRow`. If `_firstRow` reaches `_height`, it wraps to 0, making the physical start of the array the new logical bottom of the viewport. This constant-time rotation prevents memory reallocation during continuous output.

### Why does clearing scrollback reset `_firstRow` to zero?

The `ClearScrollback` method rebases the circular buffer to the absolute start of the memory allocation by adding the current `_firstRow` offset to the new viewport position, then resetting `_firstRow` to 0. This alignment allows the terminal to de-commit unused trailing memory pages while maintaining valid row mappings, reducing physical memory usage without destroying the buffer structure.

### How does resizing affect the circular buffer implementation?

During resize operations, `ResizeTraditional` instantiates a new `TextBuffer` with the target dimensions, copies the visible content using logical coordinates, and swaps the internal storage pointers. This approach avoids the complexity of remapping circular indices when dimensions change, ensuring that the new buffer starts with `_firstRow` at 0 and a clean linear layout.