Skip to main content

Grid System - Direct Buffer Injection

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)                                    │
│       │                                                             │
│       ▼                                                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  grid-buffer-injector.ts                                    │   │
│   │                                                             │   │
│   │  1. Validate epoch (reject stale updates)                   │   │
│   │  2. Sync scrollback position                                │   │
│   │  3. Inject viewport cells                                   │   │
│   │  4. Inject scrollback cells                                 │   │
│   │  5. Update cursor                                           │   │
│   │  6. Trigger WebGL render                                    │   │
│   │  7. Send ACK to backend                                     │   │
│   │                                                             │   │
│   └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ▼                                                             │
│   xterm.js WebGL Renderer                                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Why Direct Injection?

Standard xterm.js Path

// Standard path: term.write()
term.write(ansiString)


WriteBuffer.write()      // Enqueue


InputHandler.parse()     // VTE parser (slow) ❌ DUPLICATED!


BufferSet.update()       // State update


RenderService.refresh()  // Schedule render


WebGLRenderer.render()   // GPU

Grid Mode Path

// Grid Mode: direct injection
GridUpdate (already parsed by Rust)


grid-buffer-injector.ts

    ├── buffer.lines.get(row)._data   // Direct Uint32Array access
data[x*3] = content
data[x*3+1] = fg
data[x*3+2] = bg


term.refresh(0, rows-1)  // 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

const buffer = term._core._bufferService.buffer;
const line = buffer.lines.get(row);
const data = line._data as Uint32Array;

// Write cell at column 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

Implementation

GridBufferInjector Class

class GridBufferInjector {
    private sessions: Map<string, SessionState> = new Map();

    public handleGridUpdate(sessionId: string, update: GridUpdate) {
        const session = this.sessions.get(sessionId);
        if (!session) return;

        const term = session.terminal;

        // 1. Epoch validation
        if (update.epoch < session.currentEpoch) {
            invoke("grid_ack", { sessionId });
            return; // Stale update
        }

        // 2. Inject cells
        this.inject(session, term, update);

        // 3. Send ACK
        invoke("grid_ack", { sessionId });
    }

    private inject(session: SessionState, term: Terminal, update: GridUpdate) {
        const buffer = term._core._bufferService.buffer;
        const cols = term.cols;

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

        // Inject viewport
        for (let row = 0; row < update.viewport.length; row++) {
            const lineIndex = buffer.ybase + row;
            const line = buffer.lines.get(lineIndex);
            const data = line._data as Uint32Array;

            for (let col = 0; col < cols; col++) {
                const cell = update.viewport[row][col];
                const offset = col * 3;
                data[offset] = cell.content;
                data[offset + 1] = cell.fg;
                data[offset + 2] = cell.bg;
            }
        }

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

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

Epoch-Based Synchronization

Prevents race conditions during resize:
// Problem: Resize race condition
User resizes window (120x40 → 150x50)
Old GridUpdates with old size still in flight
Injecting into wrong buffer dimensionscorruption

// Solution: Epoch counter
class SessionState {
    currentEpoch: number = 0;
}

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

// On GridUpdate
if (update.epoch < session.currentEpoch) {
    console.log("Discarding stale update");
    invoke("grid_ack", { sessionId }); // Still ACK to prevent deadlock
    return;
}

ACK Flow Control

// After successful injection
invoke("grid_ack", { sessionId }).catch(() => {});

// Even on error, send ACK to prevent backend deadlock
if (!term) {
    invoke("grid_ack", { sessionId }).catch(() => {});
    return;
}

Dirty Line Optimization

Only inject changed lines:
private inject(session: SessionState, term: Terminal, update: GridUpdate) {
    if (update.is_full_update) {
        // Full update: inject all lines
        this.injectAllLines(session, term, update);
    } else {
        // Partial update: only dirty lines
        for (const row of update.dirty_lines) {
            this.injectLine(session, term, update, row);
        }
    }
}
Performance impact:
  • Full update: 120x40 = 4,800 cells
  • Typical dirty update: 2-5 lines = 240-600 cells
  • 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 Files

FileLinesPurpose
grid-buffer-injector.ts~726Main injection logic
cell_converter.rs~416Rust cell format

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. The injector preserves selection when possible:
private preserveSelection(term: Terminal, callback: () => void) {
    const selection = term.getSelection();
    const selectionPosition = term._core._selectionService.selectionStart;

    callback();

    if (selection && selectionPosition) {
        // Restore selection after injection
    }
}

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