pmacs/docs/pmacs-gpu-perline-reshape-f...

3.8 KiB

pmacs-gpu per-line incremental reshape — framing pass

Date: 2026-06-10. The per-keystroke latency floor after the optimistic-typing arc: every keystroke runs reshape() — full visible-slice chunk rebuild + set_rich_text (resets every BufferLine's shape cache) + shape_until_scroll (re-shapes every visible line with Shaping::Advanced).

Verified cosmic-text facts (0.18.2, vendored source)

  • Buffer::lines is pub Vec<BufferLine>; replacing one element with BufferLine::new(text, ending, attrs_list, shaping) leaves the other lines' shape caches intact, and shape_until_scroll re-shapes only the fresh line (shape_opt: Cached::Empty).
  • set_rich_text splits lines via BidiParagraphs, which strips the trailing paragraph separator from every yielded line in BOTH the ASCII fast path and the BidiInfo general path, yields no trailing empty line for text ending in \n, and assigns LineEnding::default() (= Lf) to every line including the last. Attr spans are added only when they differ from the defaults.
  • Parity hazard: separators other than \n (\r, U+0085, U+2028, U+2029) split lines in the full path; a surgically built line containing one would diverge.

Q#R1 — surgery vs full rebuild

Stance: per-line surgery for the single-line edit (the keystroke case), full reshape() for everything else. Fallback conditions (any ⇒ full): slice origin moved; line count changed (Enter, multi-line delete); inserted text contains \n; edited line outside the shaped slice (except: an edit entirely past the slice end changes no visible line — update view_range and redraw only); the rebuilt line's projected text contains a non-\n paragraph separator; more than one edit in the batch.

Equivalence invariant: the surgically built line's (text, attr spans) must equal what a full set_rich_text over the slice would produce for that line. Achieved by deriving both from one shared chunk function (clipped_chunks_for_range) — the full path calls it with the slice range, surgery with the line's content range — and pinned by a pure unit test (full-walk output split at line boundaries == concatenated per-line walks).

Q#R2 — the M-2 hit map under surgery

The pointer hit map (projected runs + projected line starts) is derived from the full chunk walk. Stance: make it lazy — surgery marks it dirty; hit_test_source_byte rebuilds it on demand from the same shared chunk function. Clicks are rare relative to keystrokes, and the rebuild is an O(slice) byte walk (no shaping), microseconds. Deriving from the same inputs keeps the map consistent with the shaped buffer by construction.

Q#R3 — scope

Only the local text-delta apply path (apply_loro_text_delta_batches callers: optimistic edits + incoming CrdtOps) takes the surgery path. StyleSpans/Decorations/adornment arrivals, scroll, resize, snapshot keep full reshape — they change content across the slice and arrive at far lower cadence (parse-settle / server-response rate).

Predicted findings (categorical bets)

  1. Parity miss in some attrs/boundary case the unit test doesn't cover (most likely: adornment anchored exactly at a line edge) — surfaces as one mis-styled line until the next full reshape.
  2. Staleness leak: some consumer besides the hit map silently depended on full-reshape side effects (view_range freshness, redraw requests) — surfaces as a paint lag.
  3. The win is real but the remaining per-keystroke cost shifts to glyphon prepare (full-buffer glyph pass per redraw), capping the perceived improvement.

Session plan

Single session: shared chunk fn refactor → surgery + fallbacks → lazy hit map → pure parity tests. Manual validation: type mid-file (fast burst), Enter (fallback), type on a line with inlay hints, click after typing (lazy map), peer-edit while typing.