Skip to content

Long-line horizontal virtualization — step 2 of #33 (stacked on #34)#35

Draft
andrewtheart wants to merge 2 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/long-line-horizontal-virtualization
Draft

Long-line horizontal virtualization — step 2 of #33 (stacked on #34)#35
andrewtheart wants to merge 2 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/long-line-horizontal-virtualization

Conversation

@andrewtheart

Copy link
Copy Markdown
Contributor

Stacked on #34. This branch builds on the pixel-offset scroll seam, so until #34 merges this PR's diff also includes #34's files. Please review #34 first; I'll rebase this onto master once it lands. Opened as a draft for that reason.

Step 2 of the editor scroll/render series (#33)

Builds on #34 (the pixel-based scroll seam). This adds horizontal virtualization for very long lines.

The problem

On files with pathologically long lines — minified JS/CSS/JSON, JSONL logs where each record is a single
50k–5M char line — the renderer joins every visible line into one string and lays out the whole thing on
every frame. That per-frame multi-megabyte string build + CanvasTextLayout is the actual cause of
horizontal-scroll stutter (and, at the extreme, layout-size failures). The vertical line virtualization
already in CalculateLinesToRender doesn't help, because the cost is per line width, not line count.

The approach

When any visible line exceeds HorizontalVirtualizationThreshold (50k chars), TextRenderer.Draw lays out
only a sliced window of each visible line — the viewport plus a generous scroll buffer, a few KB total —
instead of the full join. Ordinary files never hit this path (the max-visible-line-length check short-circuits),
so their render path is byte-for-byte unchanged.

The slice is a single uniform window [HorizontalSliceStart, +HorizontalSliceLength) applied to every visible
line, so re-aligning it to document coordinates is one pixel offset (HorizontalSlicePixelOffset, folded into
the existing HorizontalOffset) plus a per-line prefix sum (_renderedLineSlicePrefix) for the multi-line
selection mapping. The window is reused while the viewport stays inside a buffered "safe zone", so small
scrolls don't re-slice.

Document ↔ rendered-index mapping is funneled through three small methods on TextRenderer
(GetRenderedCharacterIndexForDocumentCharacter, GetDocumentCharacterIndexFromRenderedIndex,
GetRenderedLayoutIndexForDocument) that the caret, click hit-testing and selection call. All are no-ops when
virtualization is inactive.

Correctness surfaces covered

  • Text glyphs & search highlights — drawn at HorizontalOffset (now includes the slice offset).
  • CaretCursorRenderer maps the document column into the sliced current-line layout and shifts x.
  • Click hit-testingCursorHelper hit-tests against the slice-shifted margin, then maps back.
  • SelectionSelectionRenderer maps both endpoints through the per-line prefix (the cumulative
    full-line-length math would over-count, since rendered prior lines are sliced/shorter).
  • Syntax highlighting is skipped for a sliced layout (its char offsets would shift) — an explicit trade-off
    for the extreme-long-line case.

Purity & tests

The window arithmetic and the index mapping (where an off-by-one silently misplaces the caret/selection) are
extracted into a WinUI-free HorizontalSliceMath that TextRenderer delegates to, so it's unit-testable
without a GPU canvas. HorizontalSliceMathTests covers the viewport→window computation, the reuse safe-zone,
the per-line slice length, and the column↔rendered-index round-trip.

Known limitation

Whitespace-visualization glyphs are not slice-offset-corrected on virtualized lines (they compute x from the
layout directly). It's off by default and only visible on 50k+ char lines; happy to fold it in if you'd like.

Verification

TextControlBox (library) builds clean; the test project builds clean (x64). As with #34, happy to adjust
naming/scope before the next PR in the series.

Introduce a small IScrollOffsetSource abstraction so the editor scroll position is expressed in pixels rather than legacy scrollbar units (SingleLineHeight / DefaultVerticalScrollSensitivity). ScrollManager routes every offset read/write through it; the ScrollBarOffsetSource adapter backs it with the existing two ScrollBar primitives and does the pixel<->scrollbar-unit conversion internally (ScrollOffsetMath). Behavior is unchanged. Foundation for word-wrap scrolling, long-line horizontal virtualization and diagonal 2-axis touchpad scrolling (FrozenAssassine#33). Adds ScrollOffsetMathTests.
On files with pathologically long lines (minified JS/CSS/JSON, JSONL logs), the renderer joined every visible line into one multi-megabyte string and laid it out on every frame, causing horizontal-scroll stutter. TextRenderer.Draw now lays out only a sliced window of each visible line when a visible line exceeds 50k chars; ordinary files keep the untouched render path. The uniform slice window is re-aligned to document coordinates via HorizontalOffset + a per-line prefix sum, and the caret, click hit-testing and selection map through GetRenderedCharacterIndexForDocumentCharacter / GetDocumentCharacterIndexFromRenderedIndex / GetRenderedLayoutIndexForDocument (all no-ops when inactive). The window/index arithmetic is extracted into a WinUI-free HorizontalSliceMath with unit tests. Step 2 of FrozenAssassine#33.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant