Skip to main content

Grid Mode v2: 5-Tier Architecture

Grid Mode v2 combines native VTE parsing with xterm.js WebGL rendering to achieve significant CPU reduction compared to standard xterm.js.

Performance Comparison

Standard xterm.js:          │  Grid Mode v2:
PTY → JS VTE Parser (slow)  │  PTY → Native VTE (fast)
    → DOM Manipulation      │      → Cell Conversion
    → WebGL                 │      → Buffer Injection
                            │      → WebGL
Result: High CPU usage      │  Result: Lower CPU usage

The 5-Tier Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                         5-TIER ARCHITECTURE                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  TIER 0: PTY Data Stream                                            │
│  ────────────────────────                                           │
│  Unix socket, 4KB chunks, binary + ANSI sequences                   │
│                                                                     │
│  TIER 1: Native VTE Parser + AtomicParser                           │
│  ────────────────────────────────────────────                       │
│  atomic_parser.rs: BSU/ESU detection, metadata extraction           │
│  Native Rust: VTE parsing (fast native)                             │
│                                                                     │
│  TIER 2: ACK-Based Flow Control                                     │
│  ───────────────────────────────                                    │
│  Consumer-driven backpressure (waiting_for_ack, 10s fallback)       │
│  → 100 PTY chunks → Grid updated 100x → emit 5-10x (ACK-paced)      │
│                                                                     │
│  TIER 3: Cell Converter (Rust)                                      │
│  ─────────────────────────────                                      │
│  Native Cell → xterm.js XtermCell (3×u32 per cell)                  │
│                                                                     │
│  TIER 4: Direct Buffer Injection                                    │
│  ────────────────────────────────                                   │
│  GridUpdate → xterm._core._bufferService.buffer                     │
│                                                                     │
│  TIER 5: xterm.js WebGL Rendering                                   │
│  ────────────────────────────────                                   │
│  GPU-accelerated glyph rendering                                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Tier 2: ACK-Based Flow Control

The key innovation that prevents crashes during high-output scenarios (LLM streaming, cat large_file).

The Problem

Without flow control:
  • LLM streaming outputs bulk data rapidly
  • Each line triggers DOM reflow
  • Browser crashes or freezes

The Solution

Consumer-driven backpressure:
┌─────────────────────────────────────────────────────────────────────┐
│  ACK-BASED FLOW CONTROL                                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  let mut waiting_for_ack = false;                                   │
│  let mut has_pending_data = false;                                  │
│  const ACK_TIMEOUT_SECS: u64 = 10;  // Fallback only                │
│                                                                     │
│  PTY Data arrives                                                   │
│       ↓                                                             │
│  renderer.process(&data)  // Grid state updated immediately!        │
│       ↓                                                             │
│  [waiting_for_ack?]                                                 │
│       │                                                             │
│       ├─ YES → has_pending_data = true; continue;                   │
│       │        (skip emit → no Frontend burden)                     │
│       │                                                             │
│       └─ NO → emit(grid_update); waiting_for_ack = true;            │
│                                                                     │
│  Frontend rendering complete                                        │
│       ↓                                                             │
│  grid_ack() → Backend                                               │
│       ↓                                                             │
│  waiting_for_ack = false;                                           │
│  if has_pending_data → request_full_update() → emit                 │
│                                                                     │
│  Result: 100 chunks → 5-10 emits (stable UI)                        │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Key Properties

PropertyDescription
Consumer-drivenFrontend rendering completion triggers next emit
No data lossGrid processes ALL data immediately, only emit is controlled
10s fallbackPrevents deadlock if ACK is lost
O(1) memoryUses boolean flags only, no frame queue

Tier 3: Cell Format Conversion

Grid Mode v2 converts between two different cell representations:

Native Cell Structure

pub struct Cell {
    pub c: char,           // Unicode code point (4 bytes)
    pub fg: Color,         // Foreground color
    pub bg: Color,         // Background color
    pub flags: Flags,      // Cell attributes (bitflags)
    pub extra: Option<Arc<CellExtra>>,  // Wide chars, hyperlinks
}

pub enum Color {
    Named(NamedColor),     // 0-15 (black, red, green, etc.)
    Indexed(u8),           // 16-255 (256-color palette)
    Rgb(Rgb),              // True color (24-bit)
}

xterm.js BufferLine Format

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 (codepoint + width)
F = Foreground (color + flags)
B = Background (color + flags)

Bit-Packing Format

Slot 0 - CONTENT:
┌─────────────────────────────────┐
│ codepoint[0:20] │combined│width │
│   21 bits       │  1 bit │2 bits│
└─────────────────────────────────┘

Slot 1 - FG (Foreground):
┌─────────────────────────────────┐
│  color[0:23]  │CM[24:25]│flags  │
│   24 bits     │ 2 bits  │6 bits │
└─────────────────────────────────┘
Flags: INVERSE, BOLD, UNDERLINE, BLINK, INVISIBLE, STRIKETHROUGH

Slot 2 - BG (Background):
Same structure, BG flags: ITALIC, DIM, HAS_EXTENDED, PROTECTED, OVERLINE

Tier 4: Direct Buffer Injection

Instead of using term.write(), we inject directly into xterm’s internal buffer:
private inject(session, term, update: GridUpdate) {
    const buffer = term._core._bufferService.buffer;

    // 1. Sync scrollback position
    buffer.ybase = update.scrollback_lines;

    // 2. Inject cells directly
    for (let i = 0; i < update.viewport.length; i++) {
        const data = buffer.lines.get(i)._data as Uint32Array;
        for (let x = 0; x < cols; x++) {
            const offset = x * 3;
            data[offset] = cell.content;      // Slot 0
            data[offset + 1] = cell.fg;       // Slot 1
            data[offset + 2] = cell.bg;       // Slot 2
        }
    }

    // 3. Update cursor
    buffer.x = update.cursor_x;
    buffer.y = update.cursor_y;

    // 4. Trigger WebGL render
    term.refresh(0, term.rows - 1);
}

Why Not term.write()?

Standard xterm.js path:              Grid Mode v2 path:
─────────────────────               ──────────────────
term.write(ansiString)              GridUpdate (from Rust)
    │                                   │
    ▼                                   ▼
WriteBuffer.write()                 grid-buffer-injector.ts
    │                                   │
    ▼                                   ├── buffer.lines.get(row)._data
InputHandler.parse() ❌ DUPLICATE       │   data[x*3] = content
    │                                   │   data[x*3+1] = fg
    ▼                                   │   data[x*3+2] = bg
BufferSet.update()                      │
    │                                   ▼
    ▼                               term.refresh()
RenderService.refresh()                 │
    │                                   ▼
    ▼                               WebGLRenderer.render()
WebGLRenderer.render()

✅ VTE parsing already done in Rust
✅ No double parsing
✅ Direct memory access

Epoch-Based Size Synchronization

Prevents race conditions during resize:
// Problem: Resize race condition
User resizes (120x40 → 150x50)
Old GridUpdates arrive with old size
Injection corruption

// Solution: Epoch counter
session.currentEpoch++;
await invoke("resize_session", { sessionId, cols, rows, epoch });

// Injection validates epoch
if (update.epoch < session.currentEpoch) {
    return; // STALE, discard
}

Architecture Benefits

OperationStandard xtermGrid Mode v2
VTE ParsingJavaScript (slow)Native Rust (fast)
Cell ConversionN/ADirect format conversion
Buffer UpdateJS internalDirect injection
WebGL RenderGPU-acceleratedGPU-accelerated
OverallHigh CPU usageLower CPU usage
The key advantage is bypassing the JavaScript VTE parser entirely, using a native Rust implementation instead.

Key Files

ComponentFileLines
AtomicParseratomic_parser.rs~764
Cell Convertercell_converter.rs~416
Buffer Injectorgrid-buffer-injector.ts~726
Terminal Parseralacritty_renderer.rs~599