buildRemotePlan function
- CellBuffer prev,
- CellBuffer next, {
- required bool fullRepaint,
- TuiDirtyRows? dirtyRows,
Builds a RemotePlan from the rendered prev/next buffers — the
changed column ranges only, grouped into same-style runs with a
per-frame style table. On fullRepaint (or a size change) every row
is treated as changed.
When the frame is a beneficial upward scroll (the shared detector the
ANSI renderer uses), the plan carries scrollUpRows and the patches
cover only the residual rows — the client shifts its mirror up by that
amount before applying them, so a scrolling log ships one row instead
of the whole screen.
dirtyRows, when provided, must cover every cell that can differ
between prev and next — the same soundness contract as
AnsiRenderer.renderDiff's dirtyBounds. The plan is byte-identical
to the unbounded build; the hint only skips work that provably cannot
change it. On a plain diff the builder scans only the dirty rows (rows
outside sound damage are equal by definition). Scroll detection stays
exact: its whole-screen scans are skipped only when no shift could
pass the detector's entry guards, and a detected scroll falls back to
the unbounded residual walk, because the mirror shift redistributes
damage across rows the frame diff never touched (a clean static row
still needs re-patching once the mirror under it moves). Null means
"no damage hint": every row is scanned, exactly the prior behavior.
A debug oracle re-runs the unbounded build under assert and fails
loudly if a hint ever under-covers, so a broken damage producer is
caught in dev/CI instead of desyncing a live peer's mirror.
Implementation
RemotePlan buildRemotePlan(
CellBuffer prev,
CellBuffer next, {
required bool fullRepaint,
TuiDirtyRows? dirtyRows,
}) {
final full = fullRepaint || prev.size != next.size;
final rows = next.size.rows;
// First-line defense for a hint built against the wrong row count: an
// isFull hint whose single range stops short of next's rows would
// silently skip the tail while claiming full coverage. Unreachable from
// the runtime planner, which builds damage against frame.next.size.
assert(
dirtyRows == null ||
!dirtyRows.isFull ||
(dirtyRows.ranges.length == 1 &&
dirtyRows.ranges.single.startRow == 0 &&
dirtyRows.ranges.single.endRow >= rows),
'buildRemotePlan: an isFull dirtyRows hint does not cover every row of '
'next ($rows rows) — the damage was built against a different row '
'count.',
);
final styleIndices = <CellStyle, int>{};
final styleTable = <CellStyle>[];
int styleIndex(CellStyle s) => styleIndices.putIfAbsent(s, () {
styleTable.add(s);
return styleTable.length - 1;
});
// An empty (non-full) hint asserts zero dirty cells: no patch and no
// beneficial scroll is possible, so skip detection outright.
final emptyDamage = dirtyRows != null && dirtyRows.isEmpty;
// Detect a beneficial upward scroll on steady-state frames. The residual
// patches then compare against prev shifted up by `shift`. Under bounded
// damage, the whole-screen stats scan is skipped when the cheap
// candidate prefilter proves the detector could not fire — most
// steady-state frames. (A scroll can be beneficial under sound partial
// damage: rows a scrolling log leaves untouched — blank tails, repeated
// chrome — are often shift-invariant, so the detector's outcome must not
// depend on the hint.)
int? scrollUpRows;
if (!full &&
rows >= 2 &&
!emptyDamage &&
(dirtyRows == null ||
dirtyRows.isFull ||
_scrollCandidateExists(prev, next))) {
final shift = detectBeneficialScrollUp(
prev,
next,
screenDiffStats(prev, next),
);
if (shift != null && shift > 0) scrollUpRows = shift;
}
final shift = scrollUpRows ?? 0;
// The reference cell for (col, row): the cell `prev` holds at that
// position after the scroll shift. Off the top of `prev` reads empty.
Cell prevRef(int col, int row) {
final source = row + shift;
return source < rows ? prev.atColRow(col, source) : const Cell.empty();
}
final patches = _buildPatches(
next,
prevRef,
full,
styleIndex,
// Full frames must emit every row regardless of the hint, and a
// detected scroll compares residually against the SHIFTED prev — the
// shift moves damage onto rows the frame diff never marked (a clean
// row over new mirror content still needs its patch), so only a plain
// steady-state diff may ride the damage bound.
rowRanges: full || scrollUpRows != null ? null : dirtyRows?.ranges,
);
final plan = RemotePlan(
size: next.size,
fullRepaint: full,
scrollUpRows: scrollUpRows,
styleTable: styleTable,
patches: patches,
placements: _imagePlacements(next),
);
// Full frames ignore the hint entirely, so only steady-state bounded
// builds are oracle-checked.
assert(
full ||
dirtyRows == null ||
_boundedPlanOracleHolds(
prev,
next,
fullRepaint: fullRepaint,
bounded: plan,
),
'buildRemotePlan: the damage-bounded plan diverges from an unbounded '
'rebuild — the dirtyRows hint under-covers the true diff (soundness '
'contract violated). Shipping it would desync the peer\'s mirror until '
'the next full repaint.',
);
return plan;
}