Per-window render state for incremental rendering.
Tracks which buffer lines need re-rendering and stores last-frame comparison
values so the render pipeline can detect when a full redraw is needed. The
semantic RenderModel.Window.Builder reuses retained composed rows (see the
retained_rows/retained_wrap_lines fields) for unchanged lines.
Dirty-line tracking
The dirty set uses two representations:
:allmeans every line needs re-rendering (scroll, resize, theme change, highlight update, fold toggle, or any other wholesale invalidation)- A map of specific buffer line numbers (
%{line => true}) for targeted invalidation (edits that touch a few lines)
Tracking fields
last_viewport_top, last_viewport_cache_key, last_gutter_w,
last_line_count, last_cursor_line, and last_buf_version store values
from the previous frame. The Scroll stage compares current values against
these to detect full-invalidation triggers. last_context_fingerprint
captures all per-frame render context inputs (visual selection, search
matches, syntax highlights, signs, etc.) so context changes trigger full
redraws.
Summary
Types
Context fingerprint: a term derived from the render context that captures all per-frame inputs affecting every visible line. When the fingerprint changes between frames, all lines are re-rendered.
A retained composed row plus the cheap input fingerprint that produced it.
A retained wrapped logical line: the cheap input fingerprint that produced its visual rows plus the full visual-row entry list (Rows and their wrap metadata).
Functions
Returns the sequence through which the line identity has been applied.
Applies renderer-consumed deltas and retains their atomic changed snapshot.
Returns a cached wrapped visual row total when the key matches.
Returns the atomic bounded snapshot for pending resident deltas.
Returns the durable content epoch.
Compares the current render context fingerprint against the last frame's.
Checks current frame parameters against last-frame tracking fields and marks all lines dirty if anything requiring a full redraw has changed.
Returns true if the given buffer line needs re-rendering.
Returns the renderer-consumed version required by bounded fetches.
Returns the explicit reason for the next full resident hydration.
Returns the durable logical-line identity sequence.
Marks specific buffer lines as needing re-render.
Forces the next hydration to establish a fresh durable content epoch.
Marks the next retained GUI frame as a frontend-state reset.
Returns renderer-consumed deltas pending resident composition.
Prepares the retained GUI content epoch for the current frame.
Overlays renderer-owned committed lineage onto a stale editor snapshot.
Replaces the persistent residence build state captured by the last frame.
Replaces the retained-row map with the current frame's composed rows.
Replaces the retained wrapped-line map with the current frame's logical lines.
Stores the producer row-slot allocator after a successful content build.
Stores the wrapped visual row total for the current cache key.
Invalidates retained composition for full hydration while preserving residence promotion.
Returns a fresh cache with all lines dirty.
Returns a fresh cache invalidation while preserving retained-render epoch state.
Explicitly rebuilds durable content identity in a fresh epoch.
Returns whether residence was armed by the previous eligible frame (#2679).
Returns the residence flag captured by the last rendered frame.
Returns the persistent full-document residence build state, or nil when the
window has not built on the residence path since the last reset. Carries the
resident entry list and its incremental content digest across frames.
Returns the {row_id => {input_hash, Row.t()}} map captured by the last
semantic content build. The Builder consults it to skip recomposing rows
whose cheap input fingerprint is unchanged.
Returns the {durable_row_id => {input_hash, entries}} map captured by the last
semantic content build for wrapped windows. The Builder consults it to skip
recomposing and re-wrapping logical lines whose input fingerprint is unchanged.
Returns the persistent producer row-slot allocator.
Returns the monotonic scroll-authority sequence (#2661).
Arms (or disarms) full-document residence for the next frame (#2679).
Records the render pipeline's full-document residence decision for the frame.
Advances the scroll-authority sequence when this frame made a genuine BEAM-initiated scroll, and records the new baselines.
Snapshots tracking fields after a successful render pass.
Reconciles durable logical-line identity from one atomic buffer snapshot.
Pins bounded line fetches to the version atomically consumed by the renderer.
Types
@type context_fingerprint() :: term()
Context fingerprint: a term derived from the render context that captures all per-frame inputs affecting every visible line. When the fingerprint changes between frames, all lines are re-rendered.
Built from: visual selection, search matches, highlight version, diagnostic signs, git signs, viewport left scroll, active status, and theme color structs.
@type retained_row() :: {input_hash :: non_neg_integer(), Minga.RenderModel.Window.Row.t()}
A retained composed row plus the cheap input fingerprint that produced it.
The Content stage (semantic Builder) keys these by row_id. On the next
frame it recomputes only the input fingerprint per row; when the fingerprint
matches, it reuses the cached Row.t() verbatim instead of recomposing the
text and spans. See MingaEditor.RenderModel.Window.Builder (#2287).
@type retained_wrap_line() :: {input_hash :: non_neg_integer(), entries :: [map()]}
A retained wrapped logical line: the cheap input fingerprint that produced its visual rows plus the full visual-row entry list (Rows and their wrap metadata).
The Content stage keys these by the first visual row's durable row id. On the next
frame, when the logical line's fingerprint (line text, highlight segments,
compose context, and content width) is unchanged, the Builder reuses the
entire visual-row set verbatim instead of recomposing the line and recomputing
its wrap points. See MingaEditor.RenderModel.Window.Builder (#2287).
@type t() :: %MingaEditor.Renderer.WindowCache{ applied_change_sequence: non_neg_integer(), changed_snapshot: Minga.Buffer.RenderSnapshot.t() | nil, content_epoch: non_neg_integer(), dirty_lines: :all | %{optional(non_neg_integer()) => true}, fetch_version: non_neg_integer() | nil, hydration_reason: atom() | nil, identity_buffer: pid() | nil, last_buf_version: integer(), last_context_fingerprint: context_fingerprint(), last_cursor_line: integer(), last_gutter_w: integer(), last_line_count: integer(), last_reset_fingerprint: term(), last_viewport_cache_key: integer(), last_viewport_top: integer(), line_identity: Minga.RenderModel.Window.LineIdentity.t() | nil, pending_edit_deltas: [Minga.Buffer.EditDelta.t()], reset_pending: boolean(), residence_armed: boolean(), resident: boolean(), resident_build: MingaEditor.RenderModel.Window.ResidentBuild.t() | nil, retained_rows: %{optional(non_neg_integer()) => retained_row()}, retained_wrap_lines: %{optional(non_neg_integer()) => retained_wrap_line()}, row_slot_allocator: Minga.RenderModel.Window.RowSlotAllocator.t(), scroll_seq: non_neg_integer(), scroll_seq_last_authoritative: non_neg_integer(), scroll_seq_last_top: integer() | nil, total_visual_rows_cache: {term(), non_neg_integer()} | nil }
Functions
@spec applied_change_sequence(t()) :: non_neg_integer()
Returns the sequence through which the line identity has been applied.
@spec apply_edit_deltas( t(), pid(), [Minga.Buffer.EditDelta.t()], Minga.Buffer.RenderSnapshot.t() | nil ) :: t()
Applies renderer-consumed deltas and retains their atomic changed snapshot.
@spec cached_total_visual_rows(t(), term()) :: non_neg_integer() | nil
Returns a cached wrapped visual row total when the key matches.
@spec changed_snapshot(t()) :: Minga.Buffer.RenderSnapshot.t() | nil
Returns the atomic bounded snapshot for pending resident deltas.
@spec content_epoch(t()) :: non_neg_integer()
Returns the durable content epoch.
@spec detect_context_change(t(), context_fingerprint()) :: t()
Compares the current render context fingerprint against the last frame's.
If the fingerprint changed, marks all lines dirty. Catches changes to visual selection, search matches, syntax highlights, diagnostic signs, git signs, horizontal scroll, active/inactive status, and theme colors.
@spec detect_invalidation( t(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: t()
Checks current frame parameters against last-frame tracking fields and marks all lines dirty if anything requiring a full redraw has changed.
Structural redraw triggers: viewport scroll, gutter width, line count, buffer version, first frame (sentinel values). Only first frame and gutter-width geometry changes request a retained-GUI epoch reset; ordinary text edits and line-count changes use row hashes without bumping the content epoch.
@spec detect_invalidation( t(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: t()
@spec detect_invalidation( t(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: t()
@spec dirty?(t(), non_neg_integer()) :: boolean()
Returns true if the given buffer line needs re-rendering.
@spec fetch_version(t()) :: non_neg_integer() | nil
Returns the renderer-consumed version required by bounded fetches.
Returns the explicit reason for the next full resident hydration.
@spec line_identity(t()) :: Minga.RenderModel.Window.LineIdentity.t() | nil
Returns the durable logical-line identity sequence.
@spec mark_dirty(t(), [non_neg_integer()] | :all) :: t()
Marks specific buffer lines as needing re-render.
Pass :all to force a complete redraw. Pass a list of buffer line
numbers for targeted invalidation. If already fully dirty, adding
specific lines is a no-op.
Forces the next hydration to establish a fresh durable content epoch.
Marks the next retained GUI frame as a frontend-state reset.
@spec pending_edit_deltas(t()) :: [Minga.Buffer.EditDelta.t()]
Returns renderer-consumed deltas pending resident composition.
@spec prepare_epoch(t(), term()) :: {t(), non_neg_integer(), boolean()}
Prepares the retained GUI content epoch for the current frame.
@spec put_lineage( t(), pid(), Minga.RenderModel.Window.LineIdentity.t(), non_neg_integer() ) :: t()
Overlays renderer-owned committed lineage onto a stale editor snapshot.
@spec put_resident_build(t(), MingaEditor.RenderModel.Window.ResidentBuild.t() | nil) :: t()
Replaces the persistent residence build state captured by the last frame.
@spec put_retained_rows(t(), %{optional(non_neg_integer()) => retained_row()}) :: t()
Replaces the retained-row map with the current frame's composed rows.
Stored wholesale (not merged) so the map stays bounded to the visible row set and never accumulates rows that scrolled out of view.
@spec put_retained_wrap_lines(t(), %{ optional(non_neg_integer()) => retained_wrap_line() }) :: t()
Replaces the retained wrapped-line map with the current frame's logical lines.
Stored wholesale (not merged) so the map stays bounded to the visible logical lines and never accumulates lines that scrolled out of view.
@spec put_row_slot_allocator(t(), Minga.RenderModel.Window.RowSlotAllocator.t()) :: t()
Stores the producer row-slot allocator after a successful content build.
@spec put_total_visual_rows(t(), term(), non_neg_integer()) :: t()
Stores the wrapped visual row total for the current cache key.
Invalidates retained composition for full hydration while preserving residence promotion.
@spec reset() :: t()
Returns a fresh cache with all lines dirty.
Use after any event that invalidates render state: buffer switch, resize, theme change, etc.
Returns a fresh cache invalidation while preserving retained-render epoch state.
@spec reset_content_identity(t(), pid(), Minga.Buffer.RenderSnapshot.t()) :: t()
Explicitly rebuilds durable content identity in a fresh epoch.
Returns whether residence was armed by the previous eligible frame (#2679).
Returns the residence flag captured by the last rendered frame.
@spec resident_build(t()) :: MingaEditor.RenderModel.Window.ResidentBuild.t() | nil
Returns the persistent full-document residence build state, or nil when the
window has not built on the residence path since the last reset. Carries the
resident entry list and its incremental content digest across frames.
@spec retained_rows(t()) :: %{optional(non_neg_integer()) => retained_row()}
Returns the {row_id => {input_hash, Row.t()}} map captured by the last
semantic content build. The Builder consults it to skip recomposing rows
whose cheap input fingerprint is unchanged.
@spec retained_wrap_lines(t()) :: %{ optional(non_neg_integer()) => retained_wrap_line() }
Returns the {durable_row_id => {input_hash, entries}} map captured by the last
semantic content build for wrapped windows. The Builder consults it to skip
recomposing and re-wrapping logical lines whose input fingerprint is unchanged.
@spec row_slot_allocator(t()) :: Minga.RenderModel.Window.RowSlotAllocator.t()
Returns the persistent producer row-slot allocator.
@spec scroll_seq(t()) :: non_neg_integer()
Returns the monotonic scroll-authority sequence (#2661).
Arms (or disarms) full-document residence for the next frame (#2679).
First-paint-then-promote: a resident-eligible window renders viewport-windowed
on its first frame after becoming eligible, arming promotion; the next frame
sees residence_armed?/1 true and emits full residence. This keeps the
expensive O(document) first build off file-open first paint. The flag is reset
by reset/1 so a layout_generation rebuild (resize, font, wrap) re-defers.
Records the render pipeline's full-document residence decision for the frame.
Lives in the render cache (not a top-level Window field) because the render
cache is the only per-window struct the async render writeback copies back to
the live window (MingaEditor.State.merge_renderer_window/2). The input layer
reads it via resident?/1.
@spec settle_scroll_seq(t(), integer(), integer() | nil, non_neg_integer()) :: t()
Advances the scroll-authority sequence when this frame made a genuine BEAM-initiated scroll, and records the new baselines.
scroll_seq bumps when EITHER of two independent signals fires:
Authoritative marker (#2652):
authoritative_seq(the editor-ownedWindow.authoritative_scroll_seqrequest counter) moved past the last count this cache settled against. Command handlers increment it for the commands that must discard a frontend offset — at dispatch for always-authoritative viewport commands, and in the success branch of failable jumps (theMingaEditor.Commands@authoritative_scroll_commandscomment is the source of truth for the set). This is the only signal that catches a jump landing exactly on the previous or echoed top (zzwhile already centered, a search hit already on screen), which the top comparison below cannot see.Top comparison:
topdiffers from both the previous committed top (scroll_seq_last_top) and the frontend-reported free-scroll top (echo_top). A move to the echoed top is the frontend's own report reflected back (a wheel/trackpad free scroll, always viewport-only), so it does not advance the sequence. The first settle after a fresh cache (nil baseline) does not bump via this path (it only records the baseline).
The two signals are OR-combined into one bump, so a jump that both marks the
counter and moves the top advances scroll_seq exactly once, never twice.
The marker cannot latch: the render cache overwrites scroll_seq_last_authoritative
to the observed counter on every settle rather than the counter being cleared on
the live window, so a single increment causes at most one bump per rendered
lineage. Because the counter (scroll_seq) and both baselines live here and the
render cache is written back to the live window, the sequence is monotonic per
rendered lineage; overlapped in-flight frames can emit duplicate or regressing
values, which the frontend tolerates (strict greater-than check, with anchor-key
and reset_required covering discards).
@spec snapshot( t(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), context_fingerprint() ) :: t()
Snapshots tracking fields after a successful render pass.
Clears the dirty set and records the current frame's parameters so the next frame can detect what changed.
@spec snapshot( t(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), context_fingerprint() ) :: t()
@spec sync_line_identity(t(), pid(), Minga.Buffer.RenderSnapshot.t()) :: t()
Reconciles durable logical-line identity from one atomic buffer snapshot.
@spec with_fetch_version(t(), non_neg_integer() | nil) :: t()
Pins bounded line fetches to the version atomically consumed by the renderer.