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                           ║
║  ────────────────────────────────────────────                       ║
║  AtomicParser: 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;            ║
║                                                                     ║
║  RAF callback fires (before inject!)                                ║
║       ↓                                                             ║
║  grid_ack() → Backend  ◀── ACK sent before inject                   ║
║       ↓                                                             ║
║  waiting_for_ack = false;                                           ║
║  if has_pending_data → request_full_update() → emit                 ║
║                                                                     ║
║  Result: 100 chunks → 5-10 emits (stable UI)                        ║
║                                                                     ║
╚═════════════════════════════════════════════════════════════════════╝

Key Properties

PropertyDescription
Consumer-drivenRAF callback start triggers next emit (not render completion!)
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
Note: ACK is sent at RAF callback start, BEFORE inject(). This provides processing-slot backpressure, not frame-level render synchronization. The ~15ms gap between ACK and actual render is a deliberate trade-off to prevent GridWorker blocking.

Tier 3: Cell Format Conversion

Grid Mode v2 converts between two different cell representations:

Native Cell Structure

╔═════════════════════════════════════════════════════════════════╗
║  NATIVE CELL STRUCTURE                                          ║
╠═════════════════════════════════════════════════════════════════╣
║                                                                 ║
║  Cell:                                                          ║
║  ├── Character      Unicode code point (4 bytes)                ║
║  ├── Foreground     Color value                                 ║
║  ├── Background     Color value                                 ║
║  ├── Flags          Cell attributes (bold, italic, etc.)        ║
║  └── Extra          Wide characters, hyperlinks                 ║
║                                                                 ║
║  Color Types:                                                   ║
║  ├── Named          0-15 (black, red, green, etc.)              ║
║  ├── Indexed        16-255 (256-color palette)                  ║
║  └── 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 │ ...
└───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘
[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:
╔═════════════════════════════════════════════════════════════════╗
║  DIRECT BUFFER INJECTION STEPS                                  ║
╠═════════════════════════════════════════════════════════════════╣
║                                                                 ║
║  Step 1: Sync Scrollback Position                               ║
║  ────────────────────────────────                               ║
║  Set ybase to match backend scrollback count                    ║
║                                                                 ║
║  Step 2: Inject Cells Directly                                  ║
║  ──────────────────────────────                                 ║
║  For each viewport row:                                         ║
║    For each column:                                             ║
║      ├── Write content (Slot 0)                                 ║
║      ├── Write foreground (Slot 1)                              ║
║      └── Write background (Slot 2)                              ║
║                                                                 ║
║  Step 3: Update Cursor                                          ║
║  ───────────────────────                                        ║
║  Set cursor X and Y from GridUpdate                             ║
║                                                                 ║
║  Step 4: Trigger WebGL Render                                   ║
║  ───────────────────────────────                                ║
║  Call refresh(0, rows-1) to repaint all rows                    ║
║                                                                 ║
╚═════════════════════════════════════════════════════════════════╝

Why Not term.write()?

╔═════════════════════════════════════════════════════════════════╗
║  PATH COMPARISON                                                ║
╠═════════════════════════════════════════════════════════════════╣
║                                                                 ║
║  Standard xterm.js:           Grid Mode v2:                     ║
║  ─────────────────            ─────────────                     ║
║  term.write(ansiString)       GridUpdate (from Rust)            ║
║       │                            │                            ║
║       ▼                            ▼                            ║
║  WriteBuffer                   Buffer Injector                  ║
║       │                            │                            ║
║       ▼                            ├── Direct memory write      ║
║  InputHandler.parse()              │   (Content, FG, BG slots)  ║
║  ❌ DUPLICATE PARSING!             │                            ║
║       │                            ▼                            ║
║       ▼                        term.refresh()                   ║
║  BufferSet.update()                │                            ║
║       │                            ▼                            ║
║       ▼                        WebGL Render                     ║
║  RenderService.refresh()                                        ║
║       │                                                         ║
║       ▼                                                         ║
║  WebGL Render                                                   ║
║                                                                 ║
║  ✅ VTE parsing already done in native backend                  ║
║  ✅ No double parsing                                           ║
║  ✅ Direct memory access                                        ║
║                                                                 ║
╚═════════════════════════════════════════════════════════════════╝

Epoch-Based Size Synchronization

Prevents race conditions during resize:
╔═════════════════════════════════════════════════════════════════╗
║  EPOCH-BASED SIZE SYNCHRONIZATION                               ║
╠═════════════════════════════════════════════════════════════════╣
║                                                                 ║
║  PROBLEM: Resize Race Condition                                 ║
║  ──────────────────────────────                                 ║
║  User resizes (120×40 → 150×50)                                 ║
║       ↓                                                         ║
║  Old GridUpdates arrive with old size                           ║
║       ↓                                                         ║
║  Injection corruption (data/size mismatch)                      ║
║                                                                 ║
║  SOLUTION: Epoch Counter                                        ║
║  ─────────────────────────                                      ║
║  1. Frontend increments epoch on resize                         ║
║       ↓                                                         ║
║  2. Backend stores epoch with each GridUpdate                   ║
║       ↓                                                         ║
║  3. Injection validates: update.epoch ≥ currentEpoch?           ║
║       │                                                         ║
║       ├── YES → Inject normally                                 ║
║       └── NO → Discard as STALE                                 ║
║                                                                 ║
╚═════════════════════════════════════════════════════════════════╝

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 Components

╔═════════════════════════════════════════════════════════════════╗
║  GRID MODE v2 COMPONENTS                                        ║
╠═════════════════════════════════════════════════════════════════╣
║                                                                 ║
║  Component          Purpose                                     ║
║  ─────────────────  ────────────────────────────────────────    ║
║  AtomicParser       BSU/ESU detection, metadata extraction      ║
║  Cell Converter     Native cell → xterm.js format conversion    ║
║  Buffer Injector    Direct xterm.js buffer manipulation         ║
║  VTE Parser         ANSI/escape sequence parsing (Alacritty)    ║
║                                                                 ║
╚═════════════════════════════════════════════════════════════════╝