Commit 9ed2a76
committed
Detect cap rotation via first-row hash to keep scrollback fresh
Follow-up correctness fix on top of the scrollback-in-buffer commit,
in a separate commit so it can be reverted independently.
When libghostty's scrollback hits its byte cap and starts evicting
the oldest rows in lockstep with new ones being pushed, the row at
scrollback index 0 changes underneath us. The normal delta-detection
in redraw() tracks `total_rows` deltas, but those don't capture
content rotation — and worse, the existing trim path (delta < 0)
removes our top rows under the assumption they match the rows
libghostty just evicted, which isn't true after rotation has shifted
the content.
User-visible symptom: after sustained streaming past the cap,
isearch / consult-line over the buffer's scrollback returns rows
that no longer exist in libghostty.
Fix:
- Add `wrote_since_redraw: bool` and `first_scrollback_row_hash: u64`
to the Terminal struct.
- vtWrite sets `wrote_since_redraw = true`. The end of redraw clears
it. resize() also clears `first_scrollback_row_hash` because reflow
invalidates the row content.
- Add `computeFirstScrollbackRowHash` helper: scrolls libghostty's
viewport to the top, reads the first row's first ~16 cells, mixes
them into an FNV-1a 64-bit hash, restores the viewport. Six
libghostty calls — cheap, gated to only run when rotation is
suspected (writes happened + we have scrollback + we have a stored
hash).
- At the start of redraw, before the existing delta sync, run the
rotation check. If the stored hash differs from the freshly
sampled hash, libghostty's scrollback has rotated underneath us:
eraseBuffer, set scrollback_in_buffer = 0, force a full redraw.
The delta-sync below will then see libghostty_sb - 0 = libghostty_sb
and refetch everything fresh via insertScrollbackRange.
- After the delta-sync, update the stored hash whenever we have
scrollback so the next redraw has a fresh baseline.
Why hash-the-row instead of comparing counts: libghostty's
total_rows is allowed to plateau OR shrink when the cap is hit
(it depends on page allocation and eviction semantics). Counter
comparison alone misses the case where total_rows is steady at the
cap with content rotating, and is wrong in the case where total_rows
shrinks while content has actually rotated. Sampling the first row's
content is the only signal that always tracks rotation correctly.
Test (test/ghostel-test.el):
- ghostel-test-scrollback-rotation-rebuild — write 5000 EARLY rows
into a tiny-cap terminal (libghostty saturates at ~920 rows) +
redraw, then write 5000 LATE rows without an intervening redraw,
then redraw. The second redraw must detect rotation and rebuild
so the buffer no longer contains any "early-" markers and shows
the most recent late- rows.
A previous version of this commit also included a "batched
multi-row insert" optimization in insertScrollbackRange that
collapsed N per-row env.insert calls into roughly N/page_rows
calls. Empirically it only bought ~2-8% on the streaming bench
because libghostty's vt_write parsing dominates redraw cost in
that workload, not Elisp FFI. The added complexity (~140 lines:
new RowMeta struct, new flushScrollbackChunk helper, cumulative
char_offset arithmetic, chunk overflow handling, oversized-row
fallback) wasn't worth the small gain. The batched-insert patch
is preserved at .claude/batched-insert.patch and can be re-applied
with `git apply .claude/batched-insert.patch` if the streaming
hot path becomes more important later.1 parent 34645e2 commit 9ed2a76
3 files changed
Lines changed: 160 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
399 | 449 | | |
400 | 450 | | |
401 | 451 | | |
| |||
671 | 721 | | |
672 | 722 | | |
673 | 723 | | |
674 | | - | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
675 | 727 | | |
676 | 728 | | |
677 | 729 | | |
| |||
693 | 745 | | |
694 | 746 | | |
695 | 747 | | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
696 | 779 | | |
697 | 780 | | |
698 | 781 | | |
| |||
1012 | 1095 | | |
1013 | 1096 | | |
1014 | 1097 | | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
1015 | 1111 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
39 | 52 | | |
40 | 53 | | |
41 | 54 | | |
| |||
187 | 200 | | |
188 | 201 | | |
189 | 202 | | |
| 203 | + | |
190 | 204 | | |
191 | 205 | | |
192 | 206 | | |
| |||
202 | 216 | | |
203 | 217 | | |
204 | 218 | | |
| 219 | + | |
205 | 220 | | |
206 | 221 | | |
207 | 222 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
266 | 314 | | |
267 | 315 | | |
268 | 316 | | |
| |||
0 commit comments