Circular Buffer Implementation in Windows Terminal: Low-Memory Scrolling with TextBuffer
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. 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, 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:
- Logical to Physical Translation: The function adds the requested Y coordinate to
_firstRowand applies modulo_heightto wrap around the buffer boundary. - Constant Time Access: Row lookup requires only integer addition and modulo operations, ensuring O(1) performance regardless of buffer size.
- Wrap-Around Handling: When
_firstRow + yexceeds 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
_firstRowand wraps to 0 if it reaches_height.
This operation appears in 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, 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:
- Calculates the new logical offset by adding
_firstRowto the desired viewport position. - Resets
_firstRowto 0, effectively rebasing the circular buffer to the absolute start of the allocation. - 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
// 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
// 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
// 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
// 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
_firstRowrather 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) % _heightformula translates logical screen coordinates to physical storage locations in constant time. - Non-Destructive Resizing:
ResizeTraditionalcreates 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →