grid: a scroll over a dirty region, and DL/IL, ship rows instead of a Scrolled directive #447
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#447
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Split from #422 (its scroll half was declined until this lands).
Problem
The daemon ships
GridMsg::Scrolledonly for a pure scroll:scroll_region_up/scroll_region_downincrates/felis-grid/src/editing.rscheckregion_has_dirty_rowsand, if any row of the region is already dirty, mark the whole region and ship it asRowDeltarows. The per-subscriber gateaccept_scroll(crates/felis-daemon/src/serve/streaming.rs) does the same when a slow subscriber still owes rows in the region. DL / IL (editing.rsdl/il) never produce a directive at all: they rotate the band[cursor row, margins.bottom]and thenmark_all(), so even the rows outside the margins are replayed.So in practice the client almost never receives one. Measured for #422 (Linux, NVIDIA, release, 212x93,
43ac9b14, instrumented client):Scrolledreceivedseq -wflood, 6 runsprintf "\n..."), 3 runsless,jevery 30 msvim -u NONE,^Eevery 30 ms, 2 runsThree causes:
lessrewrites its prompt on the bottom row before it scrolls: same gate.ESC[1;28r ESC[H ESC[M ESC[r, captured from a pty). DL does not go throughscroll_region_up.Every such frame rebuilds and uploads the whole region on the client (#424's row cache marks every row a
RowDeltatouches), and ships every row of it on the wire.docs/explanation/rendering/pipeline.mdalready describes the intended behavior ("everything accumulated since the last emission coalesces into oneScrolledop plus a bottom-row batch"); the grid does not do that today.Proposal
No wire change.
GridScrolled { region_top, region_bottom, n_rows, direction }already names an arbitrary row band, and the ordering contract (docs/reference/ipc.md: queued directives first, then oneRowDeltawhose entries override) already lets a cycle carry a directive plus rows. What changes is when the daemon emits it and which rows it marks:rbecomes dirty atr - n(up) orr + n(down), bits pushed out of the region drop, and the rotated-in band is marked as today. Then emit the directive regardless of prior dirt.accept_scrolldoes the same rotation on the subscriber's ownDamageinstead of falling back to a region replay. The cycle then shipsScrolled+ only the rows whose final content differs from the shifted mirror.CSI n Mat cursor rowrinside the margins is a scroll up of[r, margins.bottom]bynwith no scrollback push;CSI n Lis a scroll down of the same band. EmitScrolled { region_top: r, region_bottom: margins.bottom, ... }for them, under the same rotation rule.n_rows; once the accumulated shift reaches the band height, drop the directives and mark the band (the directive then saves nothing). This answers the cost argument in theediting.rscomment ("a directive on top doubles the per-frame cost"): that holds only while the region is also replayed in full, which rotation removes.Sketch of a mixed tick (region 0..=92, print on 92, LF, print on 92):
Compatibility
felis.protois untouched, sotools/proto/compat.pyhas nothing to gate. NoHelloflag:docs/explanation/architecture/ipc.mdalready rejects a capability bit for scroll directives.ipc.mdstatesScrolledis "emitted only when the scrolled region is otherwise clean". A released client applies the shift then theRowDeltaoverrides, so it converges as long as the daemon marks the right rows; nothing in the client assumes the band is clean afterwards. This needs confirming against the v0.1.0 shadow (apply_scroll_directiveand the pull/cycle boundary) before the reference text is changed.region_top <= region_bottominside the announced grid and1 <= n_rows <= height.What it unlocks
#422 part 2: rotate per-slot y offsets on the client (a uniform; storage buffers are not guaranteed in vertex shaders on downlevel adapters), rotate the shadow's damage bits instead of marking the region, and rotate the row cache and
ShapeFrameslots. On the frames above that would replace roughly the whole 430-2300 us build+upload with one or two rebuilt rows. This issue is worth doing on its own for wire bytes even before the client side lands.Open questions
Damageto learn a band shift, or canrotate_regioncarry it? Either waycrates/felis-grid/tests/damage_correctness.rs(no underdraw) must hold with a directive and rows in the same cycle, and therow_cache.rsproperty test with directives on a dirty band.ScrolledIntoScrollback; DL/IL directives must not push into scrollback or shift placements differently from today.Scrolledcount per run and rows marked per frame should drop to the rotated-in band plus the rows actually written.Refs #422, #424.