Skip to content

Module — Diff Renderer and Output Emitter

This module defines how framebuffer diffs are converted into terminal bytes and emitted to the platform backend.

Goals

  • Deterministic output for a given (prev, next, caps, initial_term_state).
  • Single flush per present: exactly one platform write call on success.
  • No partial effects: failures do not write partial output.

Components

Diff renderer (src/core/zr_diff.h)

zr_diff_render(prev, next, caps, initial_term_state, desired_cursor_state, lim, scratch_damage_rects, ..., out_buf, out_cap, ...) is a pure renderer:

  • It does not mutate the input framebuffers.
  • It writes out_len bytes to the caller-provided out_buf on success.
  • If the output does not fit in out_cap, it returns ZR_ERR_LIMIT and reports out_len = 0.
  • When scroll optimizations are enabled and caps.supports_scroll_region == 1, it may emit DECSTBM + SU/SD and redraw only newly exposed lines.

Terminal state validity flags (zr_term_state_t.flags)

zr_term_state_t.flags is a best-effort validity mask for the cached terminal state and baseline assumptions.

Why: The engine sometimes knows its terminal bookkeeping is desynced (startup/resize). These bits let the diff renderer force re-establishment of baseline state even when numeric fields happen to match.

Bits (see src/core/zr_diff.h):

  • STYLE_VALID: cached SGR style is known to match the terminal.
  • CURSOR_POS_VALID: cached cursor position is known to match the terminal.
  • CURSOR_VIS_VALID: cached cursor visibility is known to match the terminal.
  • CURSOR_SHAPE_VALID: cached cursor shape/blink is known to match the terminal.
  • SCREEN_VALID: terminal screen contents are known to match prev.

When SCREEN_VALID is not set, the renderer establishes a known blank baseline before applying sparse diffs.

ALT mode baseline:

  • reset scroll region (ESC[r, homes cursor)
  • emit a deterministic baseline style (absolute SGR)
  • clear screen (ESC[2J)

INLINE mode baseline (see "Inline screen mode emission" below):

  • re-anchor to the region top with relative motion (CR, then CUU when rows are claimed)
  • emit a deterministic baseline style (absolute SGR)
  • erase below only (ESC[J), never ESC[2J and never DECSTBM
  • reset the claimed-row extent to 1

This prevents stale glyph artifacts in terminals that preserve screen contents across resizes.

Inline screen mode emission (zr_term_state_t.screen_mode == ZR_SCREEN_MODE_INLINE)

Inline mode renders a bounded region on the primary screen with the session's scrollback above it. The primary-screen scroll offset is unknown by design (no CPR queries), so the renderer never emits absolute-row sequences:

  • Positioning uses relative motion only: CR (column 0), CUU/CUD (vertical), CHA (absolute column — columns are always safe).
  • zr_term_state_t.cursor_x/cursor_y are viewport-relative (row 0 == region top), and zr_term_state_t.inline_rows_claimed tracks how many viewport rows are already materialized on the terminal.
  • Rows at or beyond the claimed extent are materialized with LF ("claims"). CUD clamps at the physical screen bottom while LF scrolls there; a scroll shifts the whole region (including its top row) up as one unit, which keeps viewport-relative coordinates self-consistent without an absolute anchor.
  • Before LF claims, the renderer emits the absolute baseline SGR. Terminals fill scrolled-in lines with the current background (BCE), and the blank-baseline cell model requires that fill to match the baseline style.
  • DECSTBM scroll optimization and ESC[2J are never emitted in inline mode (the engine additionally suppresses the scroll-region capability).
  • When the tracked cursor position is invalid (e.g. glyph width-model drift), recovery emits CR to re-anchor the column and keeps the last assumed row; residual vertical drift is bounded to deferred-autowrap edge cases and is repaired by subsequent row repaints.

Damage rectangles

The diff renderer computes a bounded, coalesced set of damage rectangles (cell-space) for the frame:

  • Rectangles are derived from prev → next cell differences.
  • The renderer emits bytes only for cells within the damaged rectangles.
  • Wide-glyph safety is preserved: damaged regions are expanded as needed so the renderer never emits only half of a wide glyph.
  • The rectangle list is cap-bounded by zr_limits_t.diff_max_damage_rects. When exceeded, the renderer falls back to a “full damage” mode for that frame and reports it via metrics (damage_full_frame = 1).

Optional row-cache scratch

zr_diff_render_ex(...) accepts optional caller-owned row scratch (zr_diff_scratch_t):

  • prev_row_hashes[], next_row_hashes[], dirty_rows[] are sized to at least framebuffer row count.
  • prev_hashes_valid lets callers reuse previously computed prev_row_hashes[] across frames.
  • The renderer precomputes per-row fingerprints and dirty hints once per frame (or reuses valid previous hashes).
  • If scratch is not supplied, rendering remains correct and deterministic; only the optimization path is disabled.

Adaptive render path

When row-cache data is available, the renderer can choose between:

  • sparse damage-rectangle rendering (default path for low dirty density)
  • per-row sweep rendering (selected when dirty-row density crosses an adaptive threshold)

Adaptive threshold policy is intentionally simple:

  • starts from a fixed base dirty-row percentage
  • applies bounded adjustments for very small frames, wide frames, and very-dirty frames
  • remains deterministic for identical inputs

When rendering through damage rectangles, coalescing uses a row-indexed active-rectangle walk to avoid the legacy rows * rect_count full scan while preserving deterministic interval ordering.

Both paths preserve:

  • grapheme/wide-cell safety
  • single-output-buffer semantics (ZR_ERR_LIMIT on truncation, out_len = 0 on failure)
  • deterministic output for the same (prev, next, caps, initial_term_state)
  • no per-frame heap churn

Internal telemetry counters

The diff renderer records internal counters for diagnostics/debug trace plumbing:

  • sweep path usage vs damage path usage
  • scroll optimization attempted/hit flags
  • hash collision-guard hits (equal-hash rows that required exact byte compare fallback)

SGR emission policy

Style changes use a deterministic compatibility-first policy:

  • every style transition emits an absolute reset-based SGR (0;...m) that fully re-establishes the desired style
  • this avoids renderer-specific retained-state drift observed in stricter terminals
  • no partial SGR sequences are emitted; each emitted CSI is syntactically complete

Extended style behavior:

  • Underline variants use colon subparameters when supported:
  • straight 4:1, double 4:2, curly 4:3, dotted 4:4, dashed 4:5
  • when underline is on and variant is 0, renderer emits legacy 4 for compatibility
  • Colored underline uses SGR 58:
  • RGB mode: 58;2;R;G;B
  • 256 mode: 58;5;idx (deterministic RGB->xterm256 mapping)
  • reset on transition to uncolored underline: 59
  • All underline extensions are capability-gated:
  • caps.supports_underline_styles == 0 -> variant bits are ignored, plain underline behavior only
  • caps.supports_colored_underlines == 0 -> underline color is omitted

Hyperlinks are tracked independently from SGR style:

  • hyperlink state is cell-scoped through framebuffer style.link_ref values
  • renderer emits OSC 8 transitions when link_ref changes between rendered cells:
  • open: ESC ] 8 ; params ; URI ESC \
  • close: ESC ] 8 ; ; ESC \
  • transitions are ordered as close-then-open when switching links
  • redundant transitions are skipped when adjacent cells share the same link
  • renderer emits a final close at end-of-frame if a link is still open

Capability gate:

  • caps.supports_hyperlinks == 0 forces effective link_ref = 0, so text renders normally with no OSC bytes.

Scrollback commits (inline mode)

engine_commit_scrollback() stages drawlist-rendered rows that the next successful present emits before the live-region diff, inside the same single flush and synchronized-update wrap:

  • Each staged block is a normal drawlist executed at submit time into an isolated framebuffer (viewport cols x rows); drawlist resources act on a transient snapshot and do not persist.
  • Emission re-anchors at the region top, paints each committed row with a baseline-SGR tail erase (EL) and claims lines with LF, so earlier lines scroll into terminal history; a final claimed line becomes the new region top.
  • The returned terminal state clears SCREEN_VALID, so the live-region diff that follows re-establishes the inline baseline (erase below + full repaint) under the committed block.
  • Staged blocks are FIFO, bounded (ZR_COMMIT_ROWS_MAX per call, ZR_COMMIT_PENDING_ROWS_MAX total), survive failed presents, and are released only after a successful flush.

Cursor control

Cursor control is applied as part of output emission:

  • The diff renderer tracks terminal state beyond (cursor_x, cursor_y) to avoid redundant sequences.
  • After diff bytes are produced, the present path may append cursor-control sequences (visibility + shape + blink and an optional final cursor position) based on the engine-owned desired cursor state.

Output emitter (engine_present, src/core/zr_engine.h)

engine_present() owns the platform flush policy:

  • The engine allocates an engine-owned output buffer sized by zr_limits_t.out_max_bytes_per_frame.
  • On success, engine_present() MUST call plat_write_output() exactly once with the full diff output bytes.
  • On ZR_ERR_LIMIT (or any other failure), engine_present() MUST perform zero platform writes (no partial flush).
  • Framebuffer swap (prev ← next) occurs only on success.
  • When caps.supports_sync_update == 1, engine_present() may wrap the frame output in ESC[?2026h / ESC[?2026l.

Image sideband emission (DRAW_IMAGE in v1)

When drawlist v1 image commands are present, engine_present() appends protocol bytes to the same output buffer after diff rendering:

  1. zr_diff_render_ex(...) emits framebuffer diff bytes.
  2. zr_image_emit_frame(...) appends image protocol sideband bytes (Kitty/Sixel/iTerm2), using staged image commands.
  3. Optional sync-wrap bytes are applied around the combined output.

Invariants:

  • still exactly one platform write on success
  • any image-emission failure returns ZR_ERR_* and performs zero writes
  • image cache state is staged during render and committed only after successful write, preserving no-partial-effects
  • image emission uses deterministic profile/cell-metric inputs (zr_terminal_profile_t) and bounded builders/arena

Optional backpressure (wait-for-output-drain)

When enabled by config, engine_present() performs a bounded wait for output to become writable before emitting bytes:

  • The engine calls plat_wait_output_writable(plat, timeout_ms) once per present.
  • The default timeout is derived from target_fps (one frame budget in milliseconds, clamped to at least 1ms).
  • On timeout, engine_present() returns ZR_ERR_LIMIT and performs zero platform writes.