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?.
Overview
Copy
┌─────────────────────────────────────────────────────────────────────┐
│ 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
Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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
Copy
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
Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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
Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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:Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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.
Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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:Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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
Copy
┌─────────────────────────────────────────────────────────────────────┐
│ 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
| Metric | Standard xterm | Grid Injection |
|---|---|---|
| Parse time | Slow (JS) | None (pre-parsed) |
| Injection time | N/A | Fast |
| Overall | High CPU | Lower CPU |
Key Components
Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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.bufferline._data
Selection State
Direct injection may interfere with selection state:Copy
┌─────────────────────────────────────────────────────────────────┐
│ 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
| Principle | Application |
|---|---|
| SMPC | Single purpose: inject cells |
| Clean interface: handleGridUpdate() | |
| No parsing logic (Rust does that) | |
| OFAC | Epoch system emerged from resize races |
| ACK pattern emerged from flow control need | |
| Dirty tracking emerged from performance need |