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
- 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
- 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
- Row virtualization in DiffView/CodeView: recycle row rendering against the visible rect, gutter-style
- 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.
Summary
LTXLabeldraws its entire text layout into a single CALayer backing store viadraw(_:). 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):
The failing layers are the
DiffView's inner contentLTXLabel(and its background layer); the outerMarkdownTextViewlabel 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):
RawDiffMarkdownNormalizer) with ~300+ rows → blank content, gutter visibleLTXLabelcrosses the ceilingSmall content renders fine (e.g. a 171-line diff ≈ 3,400pt works).
Root cause
LTXLabel.draw(_:)ignores the dirty rect and draws the fulltextLayoutinto one backing store (Sources/Litext/LTXLabel/TextLayout/LTXLabel+Draw.swift)DiffView.performLayoutsizes the inner label to the full content (textView.frame.height = textSize.height), so tall content directly produces an over-limit layerSecondary 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
draw(_:)honor the dirty rect and cull non-intersecting lines — prerequisite for any tiling approach and a perf win on its ownCATiledLayer(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 viewWorkaround 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 recycledUITableViewrows — 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.