Skip to main content

Dirty Tracking Strategies

Terminal emulators need to track which parts of the screen have changed to optimize rendering. This page explains the two main approaches and why MonoTerm chose line-based tracking.

What is Dirty Tracking?

Dirty tracking answers: “What changed since last render?”
╔═══════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  DIRTY TRACKING PURPOSE                                                ║
║                                                                        ║
║                                                                        ║
║    Without Dirty Tracking:                                             ║
║                                                                        ║
║    Every frame:                                                        ║
║    ┌────────────────────────────────────────────┐                      ║
║    │  Re-render ENTIRE screen                   │                      ║
║    │  Even if only 1 character changed          │                      ║
║    │  120 cols x 40 rows = 4,800 cells          │                      ║
║    └────────────────────────────────────────────┘                      ║
║                                                                        ║
║    = Wasteful, slow                                                    ║
║                                                                        ║
║                                                                        ║
║    With Dirty Tracking:                                                ║
║                                                                        ║
║    Every frame:                                                        ║
║    ┌────────────────────────────────────────────┐                      ║
║    │  Check what changed                        │                      ║
║    │  Re-render ONLY changed parts              │                      ║
║    │  Maybe just 1 line or a few cells          │                      ║
║    └────────────────────────────────────────────┘                      ║
║                                                                        ║
║    = Efficient, fast                                                   ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Two Approaches: Epoch vs Content Comparison

Epoch (Sequence Number)

Epoch is a monotonically increasing counter that increments whenever a write happens.
╔═══════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  EPOCH / SEQUENCE NUMBER                                               ║
║                                                                        ║
║                                                                        ║
║    Concept:                                                            ║
║                                                                        ║
║    State changes ──▶ epoch++                                           ║
║                                                                        ║
║    ┌───────────────────┐      ┌───────────────────┐                    ║
║    │  State            │      │  State            │                    ║
║    │  content: "hello" │  ──▶ │  content: "hello" │                    ║
║    │  epoch: 5         │      │  epoch: 6         │                    ║
║    └───────────────────┘      └───────────────────┘                    ║
║           (before write)            (after write, same content)        ║
║                                                                        ║
║                                                                        ║
║    Renderer Logic:                                                     ║
║                                                                        ║
║    if (last_rendered_epoch < current_epoch) {                          ║
║        // Something changed, need to render                            ║
║        render();                                                       ║
║        last_rendered_epoch = current_epoch;                            ║
║    }                                                                   ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Content Comparison

Content comparison examines the actual data. Same content = no change detected.
╔═══════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  CONTENT-BASED TRACKING                                                ║
║                                                                        ║
║                                                                        ║
║    Concept:                                                            ║
║                                                                        ║
║    State changes ──▶ compare(old, new)                                 ║
║                                                                        ║
║    ┌───────────────────┐      ┌───────────────────┐                    ║
║    │  State            │      │  State            │                    ║
║    │  content: "hello" │  ──▶ │  content: "hello" │                    ║
║    └───────────────────┘      └───────────────────┘                    ║
║           (before write)            (after write, same content)        ║
║                                                                        ║
║                                 Content unchanged! (same content)      ║
║                                                                        ║
║                                                                        ║
║    Renderer Logic:                                                     ║
║                                                                        ║
║    if (old_content != new_content) {                                   ║
║        // Content actually changed, need to render                     ║
║        render();                                                       ║
║    }                                                                   ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

The Critical Difference

The key difference: what happens when the SAME content is written again.
╔═══════════════════════════════════════════════════════════════════════╗
║  EPOCH: SAME CONTENT WRITTEN AGAIN                                     ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Scenario: Line 2 written with same content                          ║
║                                                                        ║
║                                                                        ║
║       Line 0: "hello"                                                  ║
║       Line 1: "world"                                                  ║
║       Line 2: "foo"    <──  "foo" written again (same content!)        ║
║       Line 3: "test"                                                   ║
║       Line 4: "done"                                                   ║
║                                                                        ║
║                                                                        ║
║    Epoch Behavior:                                                     ║
║                                                                        ║
║       Line 2 epoch: 5 ──▶ 6  (incremented because write happened)      ║
║                                                                        ║
║       Renderer sees: "epoch changed (5->6), must re-render"            ║
║                                                                        ║
║       Result: RE-RENDER (even though content is identical)             ║
║                                                                        ║
║       X  Wasteful                                                      ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

╔═══════════════════════════════════════════════════════════════════════╗
║  CONTENT: SAME CONTENT WRITTEN AGAIN                                   ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Same Scenario: Line 2 written with same content                     ║
║                                                                        ║
║                                                                        ║
║       Line 0: "hello"                                                  ║
║       Line 1: "world"                                                  ║
║       Line 2: "foo"    <──  "foo" written ──▶ still "foo" (same!)      ║
║       Line 3: "test"                                                   ║
║       Line 4: "done"                                                   ║
║                                                                        ║
║                                                                        ║
║    Content Behavior:                                                   ║
║                                                                        ║
║       Line 2: "foo" ──▶ "foo"  (unchanged because content is same)     ║
║                                                                        ║
║       Renderer sees: "content unchanged, skip"                         ║
║                                                                        ║
║       Result: SKIP (correctly identifies no change)                    ║
║                                                                        ║
║       OK Efficient                                                     ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

The Fundamental Distinction: Actions vs Content

╔═══════════════════════════════════════════════════════════════════════╗
║  WHAT EPOCH VS CONTENT TRACKING ACTUALLY TRACKS                        ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    EPOCH                              CONTENT                          ║
║    =====                              =======                          ║
║                                                                        ║
║    Question asked:                    Question asked:                  ║
║    "Did a write happen?"              "Is the content different?"      ║
║                                                                        ║
║    On write():                        On write():                      ║
║    ──▶ epoch++                        ──▶ compare old vs new           ║
║                                                                        ║
║    Does it look at content?           Does it look at content?         ║
║    ──▶ NO                             ──▶ YES                          ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Why This Matters for AI CLI

AI CLI applications have a specific output pattern where content-based tracking excels.
╔═══════════════════════════════════════════════════════════════════════╗
║  AI CLI OUTPUT PATTERN                                                 ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Typical AI CLI (Claude Code, Aider, etc.):                          ║
║                                                                        ║
║    ┌─────────────────────────────────────────────┐                     ║
║    │ * Thinking...                               │  <── Rewritten often║
║    │ * Thinking...                               │  <── Same content!  ║
║    │ * Thinking...                               │  <── Same content!  ║
║    │                                             │                     ║
║    │ Here is my response...                      │  <── Actual new     ║
║    │ The answer to your question is...          │  <── Actual new     ║
║    └─────────────────────────────────────────────┘                     ║
║                                                                        ║
║                                                                        ║
║    "Thinking..." line is rewritten every ~100ms                        ║
║    But content is IDENTICAL each time                                  ║
║                                                                        ║
║                                                                        ║
║    ┌─────────────────────────────────────────────────────────────┐    ║
║    │  Tracking Method   │  "Thinking..." rewrite  │  Efficiency  │    ║
║    ├────────────────────┼─────────────────────────┼──────────────┤    ║
║    │  Epoch             │  Re-render every time   │  X  Wasteful │    ║
║    │  Content           │  Skip (same content)    │  OK Efficient│    ║
║    └─────────────────────────────────────────────────────────────┘    ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Partial Update: Natural with Content Comparison

Content comparison makes partial updates natural.
╔═══════════════════════════════════════════════════════════════════════╗
║  EPOCH: PARTIAL UPDATE IS DIFFICULT                                    ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Scenario: 5 lines, only Line 2 actually changed                     ║
║                                                                        ║
║                                                                        ║
║       Line 0: "hello"                                                  ║
║       Line 1: "world"                                                  ║
║       Line 2: "foo"    ──▶   "bar"   (content changed)                 ║
║       Line 3: "test"                                                   ║
║       Line 4: "done"                                                   ║
║                                                                        ║
║                                                                        ║
║    Single Epoch (whole state):                                         ║
║                                                                        ║
║       [Entire State] ─── epoch: 5 ──▶ 6                                ║
║                              │                                         ║
║                              └── "Something changed"                   ║
║                                                                        ║
║       Renderer knows: "epoch changed"                                  ║
║       Renderer does NOT know: "WHAT changed"                           ║
║       Renderer does NOT know: "WHERE changed"                          ║
║                                                                        ║
║       Options:                                                         ║
║       A) Re-render entire screen (wasteful)                            ║
║       B) Implement separate change tracking (complex)                  ║
║                                                                        ║
║       X  Partial update requires extra work                            ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

╔═══════════════════════════════════════════════════════════════════════╗
║  CONTENT: PARTIAL UPDATE IS NATURAL                                    ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Same Scenario: 5 lines, only Line 2 changed                         ║
║                                                                        ║
║                                                                        ║
║    Per-Line Comparison:                                                ║
║                                                                        ║
║       Line 0: "hello"  ──▶  "hello"  (same OK)                         ║
║       Line 1: "world"  ──▶  "world"  (same OK)                         ║
║       Line 2: "foo"    ──▶  "bar"    (DIFFERENT X)  <── Found it!      ║
║       Line 3: "test"   ──▶  "test"   (same OK)                         ║
║       Line 4: "done"   ──▶  "done"   (same OK)                         ║
║                                                                        ║
║                                                                        ║
║       Renderer immediately knows:                                      ║
║       - WHAT changed: Line 2                                           ║
║       - WHERE: Index 2                                                 ║
║                                                                        ║
║       ──▶ DiffHint::Partial { dirty_rows: [2] }                        ║
║       ──▶ Only Line 2 sent over IPC                                    ║
║       ──▶ Only Line 2 re-rendered                                      ║
║                                                                        ║
║       OK Partial update is built-in                                    ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Why Line-Level is the Sweet Spot

Terminal dirty tracking could be done at different granularities.
╔═══════════════════════════════════════════════════════════════════════╗
║  GRANULARITY OPTIONS                                                   ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Screen Size: 120 cols x 40 rows = 4,800 cells                       ║
║                                                                        ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────────────────┐  ║
║    │                                                                │  ║
║    │  CELL-LEVEL TRACKING                                           │  ║
║    │                                                                │  ║
║    │  Track each cell individually:                                 │  ║
║    │                                                                │  ║
║    │    4,800 comparisons to make                                   │  ║
║    │                                                                │  ║
║    │    Problem 1: Tracking overhead is massive                     │  ║
║    │    Problem 2: GPU doesn't benefit (see below)                  │  ║
║    │                                                                │  ║
║    │    X  Too granular                                             │  ║
║    │                                                                │  ║
║    └────────────────────────────────────────────────────────────────┘  ║
║                                                                        ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────────────────┐  ║
║    │                                                                │  ║
║    │  SCREEN-LEVEL TRACKING                                         │  ║
║    │                                                                │  ║
║    │  Track entire screen as one unit:                              │  ║
║    │                                                                │  ║
║    │    One comparison for whole screen                             │  ║
║    │                                                                │  ║
║    │    Problem: Any change ──▶ re-render everything                │  ║
║    │    Typing one character ──▶ 4,800 cells re-rendered            │  ║
║    │                                                                │  ║
║    │    X  Too coarse                                               │  ║
║    │                                                                │  ║
║    └────────────────────────────────────────────────────────────────┘  ║
║                                                                        ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────────────────┐  ║
║    │                                                                │  ║
║    │  LINE-LEVEL TRACKING  <── SWEET SPOT                           │  ║
║    │                                                                │  ║
║    │  Track each line:                                              │  ║
║    │                                                                │  ║
║    │    40 lines = 40 comparisons to manage                         │  ║
║    │    Reasonable overhead                                         │  ║
║    │                                                                │  ║
║    │    Typing on line 5 ──▶ only line 5 re-rendered                │  ║
║    │    120 cells instead of 4,800                                  │  ║
║    │                                                                │  ║
║    │    OK Optimal balance                                          │  ║
║    │                                                                │  ║
║    └────────────────────────────────────────────────────────────────┘  ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Why Line-Level Matches GPU Rendering

╔═══════════════════════════════════════════════════════════════════════╗
║  GPU RENDERING AND LINE-LEVEL DIRTY TRACKING                           ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    Terminal screen layout:                                             ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────┐             ║
║    │ Line 0: $ ls -la                                   │             ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ Line 1: total 48                                   │             ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ Line 2: drwxr-xr-x  5 user staff  160 Jan 15 10:00 │             ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ Line 3: -rw-r--r--  1 user staff  1234 Jan 15 10:01│             ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ ...                                                 │             ║
║    └────────────────────────────────────────────────────┘             ║
║                                                                        ║
║                                                                        ║
║    GPU draws in ROWS:                                                  ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────┐             ║
║    │ [Row 0 texture] [Row 0 texture] [Row 0 texture]    │ <─ 1 draw   ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ [Row 1 texture] [Row 1 texture] [Row 1 texture]    │ <─ 1 draw   ║
║    ├────────────────────────────────────────────────────┤             ║
║    │ ...                                                 │             ║
║    └────────────────────────────────────────────────────┘             ║
║                                                                        ║
║                                                                        ║
║    Perfect Alignment:                                                  ║
║                                                                        ║
║    - Line dirty ──▶ row needs redraw                                   ║
║    - Line clean ──▶ row skip                                           ║
║    - 1:1 mapping between dirty tracking and GPU units                  ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

MonoTerm’s DiffHint System

MonoTerm uses DiffHint to communicate what changed.
╔═══════════════════════════════════════════════════════════════════════╗
║  DIFFHINT: COMMUNICATING WHAT CHANGED                                  ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    DiffHint enum:                                                      ║
║                                                                        ║
║    ┌────────────────────────────────────────────────────────────────┐ ║
║    │                                                                │ ║
║    │  Full                                                          │ ║
║    │  ────                                                          │ ║
║    │  All lines need to be rendered.                                │ ║
║    │  Used for: Initial render, resize, scroll                      │ ║
║    │                                                                │ ║
║    ├────────────────────────────────────────────────────────────────┤ ║
║    │                                                                │ ║
║    │  Partial { dirty_rows: [2, 5, 7] }                             │ ║
║    │  ──────────────────────────────────                            │ ║
║    │  Only specified lines changed.                                 │ ║
║    │  Used for: Normal typing, single-line updates                  │ ║
║    │                                                                │ ║
║    ├────────────────────────────────────────────────────────────────┤ ║
║    │                                                                │ ║
║    │  ScrollOnly { delta: -3 }                                      │ ║
║    │  ─────────────────────────                                     │ ║
║    │  Screen scrolled, no content changed.                          │ ║
║    │  Used for: Pure scroll events                                  │ ║
║    │                                                                │ ║
║    ├────────────────────────────────────────────────────────────────┤ ║
║    │                                                                │ ║
║    │  None                                                          │ ║
║    │  ────                                                          │ ║
║    │  Nothing changed (cursor only maybe).                          │ ║
║    │  Used for: Cursor blink, no-op updates                         │ ║
║    │                                                                │ ║
║    └────────────────────────────────────────────────────────────────┘ ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Comparison Summary

╔═══════════════════════════════════════════════════════════════════════╗
║  TRACKING METHOD COMPARISON                                            ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    ┌────────────────────────────────────────────────────────────────┐ ║
║    │  Aspect              │  Epoch           │  Content             │ ║
║    ├──────────────────────┼──────────────────┼──────────────────────┤ ║
║    │  Tracks              │  "Write event"   │  "Content different" │ ║
║    ├──────────────────────┼──────────────────┼──────────────────────┤ ║
║    │  Same content        │  Re-render X     │  Skip OK             │ ║
║    │  rewritten           │                  │                      │ ║
║    ├──────────────────────┼──────────────────┼──────────────────────┤ ║
║    │  Computation cost    │  None (++)       │  Compare operation   │ ║
║    ├──────────────────────┼──────────────────┼──────────────────────┤ ║
║    │  Partial update      │  Extra tracking  │  Built-in            │ ║
║    │                      │  needed          │                      │ ║
║    └────────────────────────────────────────────────────────────────┘ ║
║                                                                        ║
║                                                                        ║
║    When to use Epoch:                                                  ║
║    - Same-process multi-renderer coordination                          ║
║    - Version tracking (e.g., resize synchronization)                   ║
║    - When write always means content changed                           ║
║                                                                        ║
║    When to use Content Comparison:                                     ║
║    - Cross-boundary (IPC) dirty tracking                               ║
║    - When same content may be rewritten                                ║
║    - When partial updates are desired                                  ║
║    - AI CLI workloads (frequent rewrites)                              ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝

Summary

╔═══════════════════════════════════════════════════════════════════════╗
║  DIRTY TRACKING SUMMARY                                                ║
╠═══════════════════════════════════════════════════════════════════════╣
║                                                                        ║
║    ┌───────────────────────────────────────────────────────────────┐  ║
║    │                                                               │  ║
║    │  MonoTerm uses LINE-LEVEL CONTENT COMPARISON because:         │  ║
║    │                                                               │  ║
║    │  1. AI CLIs frequently rewrite same content                   │  ║
║    │     ──▶ Content comparison skips redundant work               │  ║
║    │                                                               │  ║
║    │  2. GPU renders in rows                                       │  ║
║    │     ──▶ Line-level matches GPU rendering units                │  ║
║    │                                                               │  ║
║    │  3. Partial updates reduce IPC traffic                        │  ║
║    │     ──▶ Only send changed lines                               │  ║
║    │                                                               │  ║
║    │  4. 40 lines is manageable                                    │  ║
║    │     ──▶ Not too granular, not too coarse                      │  ║
║    │                                                               │  ║
║    └───────────────────────────────────────────────────────────────┘  ║
║                                                                        ║
║                                                                        ║
║    Result:                                                             ║
║                                                                        ║
║    ┌───────────────────────────────────────────────────────────────┐  ║
║    │                                                               │  ║
║    │  Typical typing:                                              │  ║
║    │    Only 1 line dirty                                          │  ║
║    │    ──▶ 1 line sent over IPC                                   │  ║
║    │    ──▶ 1 line re-rendered by GPU                              │  ║
║    │                                                               │  ║
║    │  AI "Thinking..." rewrite:                                    │  ║
║    │    Same content detected                                      │  ║
║    │    ──▶ 0 lines sent over IPC                                  │  ║
║    │    ──▶ 0 lines re-rendered by GPU                             │  ║
║    │                                                               │  ║
║    └───────────────────────────────────────────────────────────────┘  ║
║                                                                        ║
╚═══════════════════════════════════════════════════════════════════════╝