Skip to main content

Grid System - Direct Buffer Injection

Technical Reference: This document contains implementation details and code examples for developers working on Monolex internals. For conceptual understanding of how Monolex achieves fast rendering, see Why So Fast?.
The Grid System bypasses xterm.js’s internal parser to inject pre-parsed cells directly into the buffer, achieving significant CPU reduction.

Overview

┌─────────────────────────────────────────────────────────────────────┐
│               GRID BUFFER INJECTION                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   GridUpdate (from Rust backend)                                    │
│       │                                                             │
│       ▼                                                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Buffer Injector                                            │   │
│   │                                                             │   │
│   │  1. Validate epoch (reject stale updates)                   │   │
│   │  2. requestAnimationFrame callback fires                    │   │
│   │  3. Send ACK to backend  ◄── BEFORE inject!                 │   │
│   │  4. Sync scrollback position                                │   │
│   │  5. Inject viewport cells                                   │   │
│   │  6. Update cursor                                           │   │
│   │  7. Trigger WebGL render (next frame)                       │   │
│   │                                                             │   │
│   └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ▼                                                             │
│   xterm.js WebGL Renderer                                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Why Direct Injection?

Standard xterm.js Path vs Grid Mode Path

┌─────────────────────────────────────────────────────────────────┐
│  STANDARD XTERM.JS PATH                                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  term.write(ansiString)                                         │
│       │                                                         │
│       ▼                                                         │
│  WriteBuffer.write()       (Enqueue)                            │
│       │                                                         │
│       ▼                                                         │
│  InputHandler.parse()      (VTE parser) ❌ DUPLICATED!          │
│       │                                                         │
│       ▼                                                         │
│  BufferSet.update()        (State update)                       │
│       │                                                         │
│       ▼                                                         │
│  RenderService.refresh()   (Schedule render)                    │
│       │                                                         │
│       ▼                                                         │
│  WebGLRenderer.render()    (GPU)                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  GRID MODE PATH (Direct Injection)                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  GridUpdate (already parsed by native backend)                  │
│       │                                                         │
│       ▼                                                         │
│  Buffer Injector                                                │
│       │                                                         │
│       ├── Direct memory write to line data                      │
│       │   (Content, Foreground, Background slots)               │
│       │                                                         │
│       ▼                                                         │
│  term.refresh()            (Trigger WebGL)                      │
│       │                                                         │
│       ▼                                                         │
│  WebGLRenderer.render()    (GPU)                                │
│                                                                 │
│  ✅ No double parsing                                           │
│  ✅ Direct memory access                                        │
│  ✅ Minimal overhead                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

xterm.js Buffer Internals

BufferLine Structure

BufferLine._data = Uint32Array(cols × 3)

Cell 0         Cell 1         Cell 2         ...
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐
│ C │ F │ B │ │ C │ F │ B │ │ C │ F │ B │ ...
└───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘
[0] [1] [2]   [3] [4] [5]   [6] [7] [8]

C = Content slot (codepoint + width)
F = Foreground slot (color + flags)
B = Background slot (color + flags)

Access Pattern

┌─────────────────────────────────────────────────────────────────┐
│  BUFFER ACCESS PATTERN                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Step 1: Get buffer reference                                   │
│  Step 2: Get line at row index                                  │
│  Step 3: Get line data as typed array                           │
│                                                                 │
│  Step 4: Write cell at column X                                 │
│  ──────────────────────────────                                 │
│  offset = X × 3                                                 │
│  ├── Write content at offset + 0  (Slot 0)                      │
│  ├── Write foreground at offset + 1  (Slot 1)                   │
│  └── Write background at offset + 2  (Slot 2)                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Implementation

Buffer Injector Component

┌─────────────────────────────────────────────────────────────────┐
│  BUFFER INJECTOR - handleGridUpdate() FLOW                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  GridUpdate received                                            │
│       │                                                         │
│       ▼                                                         │
│  Step 1: Get session state                                      │
│       │                                                         │
│       ▼                                                         │
│  Step 2: Epoch validation                                       │
│       │ Is update.epoch ≥ currentEpoch?                         │
│       │                                                         │
│       ├── NO → Send ACK, discard stale update                   │
│       │                                                         │
│       └── YES → Continue                                        │
│            │                                                    │
│            ▼                                                    │
│  Step 3: Schedule RAF callback                                  │
│       │                                                         │
│       ▼                                                         │
│  Step 4: RAF fires → Send ACK BEFORE inject                     │
│       │                                                         │
│       ▼                                                         │
│  Step 5: Call inject()                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  BUFFER INJECTOR - inject() FLOW                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Step 1: Sync scrollback position                               │
│          Set ybase to match backend scrollback count            │
│                                                                 │
│  Step 2: Inject viewport cells                                  │
│          For each row in viewport:                              │
│            For each column:                                     │
│              Write content, foreground, background              │
│                                                                 │
│  Step 3: Update cursor position                                 │
│          Set cursor X and Y from GridUpdate                     │
│                                                                 │
│  Step 4: Trigger WebGL render                                   │
│          Call refresh(0, rows-1)                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Epoch-Based Synchronization

Prevents race conditions during resize:
┌─────────────────────────────────────────────────────────────────┐
│  EPOCH-BASED SIZE SYNCHRONIZATION                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  PROBLEM: Resize Race Condition                                 │
│  ──────────────────────────────                                 │
│  User resizes (120×40 → 150×50)                                 │
│       ↓                                                         │
│  Old GridUpdates with old size still in flight                  │
│       ↓                                                         │
│  Injecting into wrong dimensions → corruption                   │
│                                                                 │
│  SOLUTION: Epoch Counter                                        │
│  ─────────────────────────                                      │
│  On Resize:                                                     │
│    1. Increment currentEpoch                                    │
│    2. Send resize command with new epoch                        │
│                                                                 │
│  On GridUpdate:                                                 │
│    Check: update.epoch ≥ currentEpoch?                          │
│       │                                                         │
│       ├── NO → Log "stale", send ACK, discard                   │
│       │        (ACK prevents backend deadlock)                  │
│       │                                                         │
│       └── YES → Process normally                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ACK Flow Control

Note: ACK is sent at RAF callback start, BEFORE inject(). This provides processing-slot backpressure, not frame-level render sync.
┌─────────────────────────────────────────────────────────────────┐
│  ACK FLOW CONTROL                                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  RAF callback fires                                             │
│       │                                                         │
│       ▼                                                         │
│  Step 1: Send ACK immediately ◄── BEFORE inject!                │
│       │  (Unlocks backend for next emit)                        │
│       │                                                         │
│       ▼                                                         │
│  Step 2: Inject cells into buffer                               │
│       │                                                         │
│       ▼                                                         │
│  Step 3: Trigger WebGL render                                   │
│                                                                 │
│  ERROR HANDLING:                                                │
│  ───────────────                                                │
│  If terminal not found:                                         │
│    → Send ACK anyway (prevents backend deadlock)                │
│    → Return early                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Dirty Line Optimization

Only inject changed lines:
┌─────────────────────────────────────────────────────────────────┐
│  DIRTY LINE OPTIMIZATION                                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  On inject():                                                   │
│       │                                                         │
│       ▼                                                         │
│  Check: is_full_update?                                         │
│       │                                                         │
│       ├── YES → Inject all lines (full viewport)                │
│       │                                                         │
│       └── NO → Inject only dirty lines                          │
│                (lines that changed since last update)           │
│                                                                 │
│  PERFORMANCE IMPACT:                                            │
│  ───────────────────                                            │
│  Full update:    120×40 = 4,800 cells                           │
│  Dirty update:   2-5 lines = 240-600 cells                      │
│                                                                 │
│  Result: Significant reduction in data transferred              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Scrollback Handling

┌─────────────────────────────────────────────────────────────────────┐
│               BUFFER STRUCTURE                                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   buffer.lines (CircularList):                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │ Line 0     ← Oldest scrollback                              │   │
│   │ Line 1                                                      │   │
│   │ ...                                                         │   │
│   │ Line N     ← buffer.ybase (scrollback end)                  │   │
│   │ Line N+1   ← Viewport start                                 │   │
│   │ Line N+2                                                    │   │
│   │ ...                                                         │   │
│   │ Line N+40  ← Viewport end (if rows=40)                      │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│   buffer.ybase = scrollback line count                              │
│   buffer.ydisp = current scroll position                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Architecture Benefits

MetricStandard xtermGrid Injection
Parse timeSlow (JS)None (pre-parsed)
Injection timeN/AFast
OverallHigh CPULower CPU
The key benefit is bypassing JavaScript parsing entirely - cells arrive pre-parsed from Rust.

Key Components

┌─────────────────────────────────────────────────────────────────┐
│  GRID SYSTEM COMPONENTS                                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Component          Purpose                                     │
│  ─────────────────  ────────────────────────────────────────    │
│  Buffer Injector    Main injection logic                        │
│  Cell Converter     Native cell → xterm.js format conversion    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Limitations

Internal API Usage

Grid injection uses xterm.js internal APIs:
  • term._core._bufferService.buffer
  • line._data
These may change between xterm.js versions. Monolex pins to tested versions.

Selection State

Direct injection may interfere with selection state:
┌─────────────────────────────────────────────────────────────────┐
│  SELECTION PRESERVATION                                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Step 1: Save current selection                                 │
│          ├── Selection text                                     │
│          └── Selection position                                 │
│                                                                 │
│  Step 2: Perform injection                                      │
│                                                                 │
│  Step 3: Restore selection if it existed                        │
│                                                                 │
│  Note: Selection may be invalidated if underlying               │
│        buffer content changes significantly                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

SMPC/OFAC Applied

PrincipleApplication
SMPCSingle purpose: inject cells
Clean interface: handleGridUpdate()
No parsing logic (Rust does that)
OFACEpoch system emerged from resize races
ACK pattern emerged from flow control need
Dirty tracking emerged from performance need