@@ -464,21 +464,23 @@ pub fn isRowEmptyAt(term: *Terminal, cy: u16) bool {
464464 return true ;
465465}
466466
467- /// Hash the first ~16 cells of libghostty's first scrollback row using
468- /// FNV-1a. Returns 0 if there is no scrollback or if anything fails.
467+ /// Read the first scrollback row's codepoints into `out` (one per cell,
468+ /// up to 512 entries). Returns true on success, false if there is no
469+ /// scrollback or anything fails. For terminals wider than 512 columns
470+ /// only the first 512 cells are compared — a practical limit since
471+ /// 512 columns exceeds any standard display.
469472///
470473/// Used to detect rotation: when libghostty's scrollback is plateaued at
471474/// its byte cap, sustained writes evict the oldest row in lockstep with
472475/// new rows being pushed, so `total_rows` doesn't change and the normal
473- /// delta-detection sees no work to do. Sampling the first scrollback
474- /// row's content lets us detect that the row at index 0 has changed
475- /// underneath us.
476+ /// delta-detection sees no work to do. Comparing the full row lets us
477+ /// detect that the row at index 0 has changed underneath us.
476478///
477479/// Scrolls libghostty's viewport to the top to read the row, then
478- /// restores the previous viewport offset. Cheap (~6 libghostty calls);
479- /// gated by the caller to only run when rotation is suspected.
480- fn computeFirstScrollbackRowHash (term : * Terminal ) u64 {
481- const sb = term .getScrollbar () orelse return 0 ;
480+ /// restores the previous viewport offset. Gated by the caller to only
481+ /// run when rotation is suspected.
482+ fn readFirstScrollbackRow (term : * Terminal , out : * [ 512 ] u32 ) bool {
483+ const sb = term .getScrollbar () orelse return false ;
482484 const saved_offset = sb .offset ;
483485
484486 term .scrollViewport (gt .SCROLL_TOP , 0 );
@@ -489,29 +491,23 @@ fn computeFirstScrollbackRowHash(term: *Terminal) u64 {
489491 }
490492 }
491493
492- if (gt .c .ghostty_render_state_update (term .render_state , term .terminal ) != gt .SUCCESS ) return 0 ;
493- if (gt .c .ghostty_render_state_get (term .render_state , gt .RS_DATA_ROW_ITERATOR , @ptrCast (& term .row_iterator )) != gt .SUCCESS ) return 0 ;
494- if (! gt .c .ghostty_render_state_row_iterator_next (term .row_iterator )) return 0 ;
495- if (gt .c .ghostty_render_state_row_get (term .row_iterator , gt .RS_ROW_DATA_CELLS , @ptrCast (& term .row_cells )) != gt .SUCCESS ) return 0 ;
494+ if (gt .c .ghostty_render_state_update (term .render_state , term .terminal ) != gt .SUCCESS ) return false ;
495+ if (gt .c .ghostty_render_state_get (term .render_state , gt .RS_DATA_ROW_ITERATOR , @ptrCast (& term .row_iterator )) != gt .SUCCESS ) return false ;
496+ if (! gt .c .ghostty_render_state_row_iterator_next (term .row_iterator )) return false ;
497+ if (gt .c .ghostty_render_state_row_get (term .row_iterator , gt .RS_ROW_DATA_CELLS , @ptrCast (& term .row_cells )) != gt .SUCCESS ) return false ;
496498
497- // FNV-1a 64-bit hash. We mix in the first ~16 cells' first
498- // codepoints (or a space for empty cells) — enough entropy to
499- // distinguish rotation states without scanning the whole row.
500- const fnv_prime : u64 = 0x100000001b3 ;
501- var hash : u64 = 0xcbf29ce484222325 ;
499+ @memset (out , 0 );
502500 var i : usize = 0 ;
503- while (i < 16 and gt .c .ghostty_render_state_row_cells_next (term .row_cells )) : (i += 1 ) {
501+ const cols = @min (term .cols , 512 );
502+ while (i < cols and gt .c .ghostty_render_state_row_cells_next (term .row_cells )) : (i += 1 ) {
504503 var graphemes_len : u32 = 0 ;
505504 if (gt .c .ghostty_render_state_row_cells_get (term .row_cells , gt .RS_CELLS_DATA_GRAPHEMES_LEN , @ptrCast (& graphemes_len )) != gt .SUCCESS ) continue ;
506- if (graphemes_len == 0 ) {
507- hash = (hash ^ ' ' ) *% fnv_prime ;
508- continue ;
509- }
505+ if (graphemes_len == 0 ) continue ;
510506 var codepoints : [4 ]u32 = undefined ;
511507 if (gt .c .ghostty_render_state_row_cells_get (term .row_cells , gt .RS_CELLS_DATA_GRAPHEMES_BUF , @ptrCast (& codepoints )) != gt .SUCCESS ) continue ;
512- hash = ( hash ^ codepoints [0 ]) *% fnv_prime ;
508+ out [ i ] = codepoints [0 ];
513509 }
514- return hash ;
510+ return true ;
515511}
516512
517513/// Result from buildRowContent: byte length for make_string, char count for properties.
@@ -976,20 +972,22 @@ pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
976972 // that sampling requires is wasted work if we are about to erase anyway.
977973 // On a hash match, stash the value for reuse at end-of-redraw (promotion
978974 // and insert-at-tail don't shift row 0, so it stays valid).
979- var cached_row0_hash : ? u64 = null ;
975+ var cached_row0_valid = false ;
980976 if (! scrollback_stale and
981977 term .wrote_since_redraw and
982978 term .scrollback_in_buffer > 0 and
983- term .first_scrollback_row_hash != 0 )
979+ term .first_scrollback_row_valid )
984980 {
985- const new_hash = computeFirstScrollbackRowHash (term );
986- // computeFirstScrollbackRowHash scrolled libghostty's viewport to
981+ var new_row : [512 ]u32 = undefined ;
982+ const read_ok = readFirstScrollbackRow (term , & new_row );
983+ // readFirstScrollbackRow scrolled libghostty's viewport to
987984 // sample row 0; the render state is now stale — refresh it.
988985 if (gt .c .ghostty_render_state_update (term .render_state , term .terminal ) != gt .SUCCESS ) return ;
989- if (new_hash != term .first_scrollback_row_hash ) {
986+ const compare_cols = @min (term .cols , 512 );
987+ if (read_ok and ! std .mem .eql (u32 , new_row [0.. compare_cols ], term .first_scrollback_row [0.. compare_cols ])) {
990988 scrollback_stale = true ;
991- } else {
992- cached_row0_hash = new_hash ;
989+ } else if ( read_ok ) {
990+ cached_row0_valid = true ;
993991 }
994992 }
995993
@@ -1004,7 +1002,7 @@ pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
10041002 if (scrollback_stale ) {
10051003 env .eraseBuffer ();
10061004 term .scrollback_in_buffer = 0 ;
1007- term .first_scrollback_row_hash = 0 ;
1005+ term .first_scrollback_row_valid = false ;
10081006 force_full = true ;
10091007 }
10101008
@@ -1331,24 +1329,21 @@ pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
13311329 _ = env .call1 (emacs .sym .@"ghostel--update-directory" , env .makeString (pwd ));
13321330 }
13331331
1334- // Update the cached first-scrollback-row hash for the next redraw's
1335- // rotation check. Reuse the start-of-redraw hash when it's still
1336- // valid (no rotation, no trim) — avoids a second scroll-to-top +
1337- // render_state_update round trip per redraw.
1338- //
1339- // If nothing was written AND we didn't populate `cached_row0_hash`
1340- // at the top, row 0 cannot have moved since the previous redraw
1341- // set `first_scrollback_row_hash`, so skip the compute entirely.
1342- // This covers cursor-only redraws and idle-timer fires.
1332+ // Update the cached first-scrollback-row snapshot for the next redraw's
1333+ // rotation check. When the start-of-redraw check found no rotation,
1334+ // `term.first_scrollback_row` already holds the current value — skip
1335+ // the second round trip. If nothing was written at all, row 0 cannot
1336+ // have moved, so skip entirely. This covers cursor-only redraws and
1337+ // idle-timer fires.
13431338 if (term .scrollback_in_buffer > 0 ) {
1344- if (cached_row0_hash ) | h | {
1345- term .first_scrollback_row_hash = h ;
1339+ if (cached_row0_valid ) {
1340+ // term.first_scrollback_row is already current; nothing to do.
13461341 } else if (term .wrote_since_redraw ) {
1347- term .first_scrollback_row_hash = computeFirstScrollbackRowHash (term );
1342+ term .first_scrollback_row_valid = readFirstScrollbackRow (term , & term . first_scrollback_row );
13481343 }
1349- // else: no writes, no cached hash → existing value is still current.
1344+ // else: no writes, no cached row → existing value is still current.
13501345 } else {
1351- term .first_scrollback_row_hash = 0 ;
1346+ term .first_scrollback_row_valid = false ;
13521347 }
13531348
13541349 // Clear the write flag so the next redraw can detect "writes happened
0 commit comments