Skip to content

LTXLabel renders blank past Core Animation's backing-store ceiling (~16,384px) — large diffs/code blocks #9

Description

@gtokman

Summary

LTXLabel draws its entire text layout into a single CALayer backing store via draw(_:). Once the label's pixel size exceeds Core Animation's backing-store limit (~16,384px per dimension ≈ 5,400pt at 3x), CA refuses to allocate the store and the label renders blank — no crash, no error surfaced to the caller.

Observed in v0's native chat when rendering a large unified diff (pnpm-lock.yaml, 2,668 lines):

diff patch chars=118996 lines=2668
diff bounding size width=402 height=55243
-[<CALayer> display]: Ignoring bogus layer size (1910.64, 55243.00), contentsScale 3.0,
  backing store size (5731.91, 165729.00)
-[<CALayer> display]: Ignoring bogus layer size (1886.64, 53357.00), contentsScale 3.0,
  backing store size (5659.91, 160071.00)

The failing layers are the DiffView's inner content LTXLabel (and its background layer); the outer MarkdownTextView label hits the same ceiling for tall documents. The diff's line-number gutter keeps rendering because it's a viewport-sized, scroll-synced view — so the user sees gutter numbers and row highlights next to a blank content column.

Repro

Render any markdown/diff whose laid-out height exceeds ~5,400pt at 3x (~260 diff rows at 21pt/row):

  • ```diff fence (or raw unified diff via RawDiffMarkdownNormalizer) with ~300+ rows → blank content, gutter visible
  • Equivalently any single code block / document tall enough that one LTXLabel crosses the ceiling

Small content renders fine (e.g. a 171-line diff ≈ 3,400pt works).

Root cause

  • LTXLabel.draw(_:) ignores the dirty rect and draws the full textLayout into one backing store (Sources/Litext/LTXLabel/TextLayout/LTXLabel+Draw.swift)
  • DiffView.performLayout sizes the inner label to the full content (textView.frame.height = textSize.height), so tall content directly produces an over-limit layer
  • Nothing in the pipeline detects or reports the CA rejection

Secondary concern even below the ceiling: a single ~4,000pt-tall wide diff label costs a ~180MB backing store at 3x — monolithic rendering scales memory with content length.

Possible directions

  1. Rect-aware drawing (incremental, low risk): make draw(_:) honor the dirty rect and cull non-intersecting lines — prerequisite for any tiling approach and a perf win on its own
  2. Tiled backing for oversized labels: CATiledLayer (needs thread-safe layout access, tile-fade disabled, and care around the streaming-reveal glyph-alpha redraws) or manual viewport-window drawing driven by the enclosing scroll view
  3. Row virtualization in DiffView/CodeView: recycle row rendering against the visible rect, gutter-style
  4. At minimum: detect an over-limit layout and surface it (assert/log/callback), so consumers can fall back instead of silently showing nothing

Workaround in the consumer (v0 chat)

The diff sheet now splits large patches at hunk boundaries into ≤150-line chunks (synthesizing continuation @@ -old,count +new,count @@ headers so gutter numbers stay correct) and renders them as recycled UITableView rows — only visible chunks hold backing stores. Works, but it leans on unified-diff structure the library shouldn't need consumers to know about; happy to delete it once the label can draw tall content.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions