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
Copy
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
Copy
╔═════════════════════════════════════════════════════════════════════╗
║ 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:Copy
╔═════════════════════════════════════════════════════════════════════╗
║ 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
| Property | Description |
|---|---|
| Consumer-driven | RAF callback start triggers next emit (not render completion!) |
| No data loss | Grid processes ALL data immediately, only emit is controlled |
| 10s fallback | Prevents deadlock if ACK is lost |
| O(1) memory | Uses 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
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 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
Copy
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
Copy
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 usingterm.write(), we inject directly into xterm’s internal buffer:
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 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()?
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 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:Copy
╔═════════════════════════════════════════════════════════════════╗
║ 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
| Operation | Standard xterm | Grid Mode v2 |
|---|---|---|
| VTE Parsing | JavaScript (slow) | Native Rust (fast) |
| Cell Conversion | N/A | Direct format conversion |
| Buffer Update | JS internal | Direct injection |
| WebGL Render | GPU-accelerated | GPU-accelerated |
| Overall | High CPU usage | Lower CPU usage |
Key Components
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 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) ║
║ ║
╚═════════════════════════════════════════════════════════════════╝