Skip to content

perf(sdf): translation fast path for scrolling text + skip unchanged SDF uploads#121

Merged
chiefcll merged 1 commit into
mainfrom
perf/sdf-scroll-fast-path-and-upload-skip
Jul 2, 2026
Merged

perf(sdf): translation fast path for scrolling text + skip unchanged SDF uploads#121
chiefcll merged 1 commit into
mainfrom
perf/sdf-scroll-fast-path-and-upload-skip

Conversation

@chiefcll

@chiefcll chiefcll commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Two SDF text hot-path fixes targeting the standard TV scroll scenario (rows of cards with titles translating under a clipped viewport). Follows #119/#120 from the same render-loop review.

1. Translation fast path for the SDF vertex cache

The SDF vertex cache stores world-space vertices, so a scrolling text node missed it on every frame — the hit test required all six transform components to match. Each miss re-ran per-glyph matrix math (16 mul + 16 add per glyph) in addSdfQuads and re-snapshotted the result back into the cache (a second full copy, plus a realloc when glyph count changed) — every node, every scroll frame.

SdfTextRenderer.renderQuads now dispatches three ways:

  • Exact hit (unchanged): single mem-copy via addSdfCachedQuads.
  • Translation hit (new): scale/rotation components match, only tx/ty moved. The new WebGlRenderer.addSdfTranslatedQuads mem-copies the cached vertices and adds (dx, dy) to the two position floats of each vertex. The cache keeps its original base transform, so every frame recomputes from the same reference — no accumulation drift and no per-frame re-snapshot.
  • Miss (unchanged): full recompute + snapshot.

⚠️ Correctness subtlety for reviewers: the copy must remain a typed-array set() (bit-exact memcpy). Packed ABGR colors live in the same Float32Array, and patterns like 0xFFFFFFFF — plain white text — are float32 NaNs that element-wise float reads/writes can canonicalize into 0x7FC00000, corrupting colors. Only the two position floats are touched after the copy, and a regression test asserts bit-exact color preservation through the translated path.

2. Skip the per-frame SDF GPU upload when bytes are unchanged

render() uploaded the entire SDF buffer via bufferData(DYNAMIC_DRAW) on every drawn frame — so any focus pulse or image fade re-uploaded every glyph on screen (96 bytes/glyph; exactly the "guaranteed CPU tax" in the CLAUDE.md TV cost model). This implements the sdfBufferChanged invariant that CLAUDE.md already documents:

uploadSdfBuffer() skips the upload when every write this frame was an exact cache-hit mem-copy (sdfBufferChanged === false) and the total size matches the last upload. Exact hits write byte-identical data, and identical offsets are guaranteed because every source of reorder or resize raises the flag, failing conservative per the project rule:

  • cache-miss recompute (addSdfQuads)
  • the new translated copy (addSdfTranslatedQuads)
  • render-list rebuild (invalidateQuadBuffer) — a reorder moves identical bytes to different offsets
  • RTT partial upload (renderRTT) — clobbers the GL buffer's contents/size mid-frame
  • SDF backing-store growth (ensureSdfBufferCapacity)

Impact

  • Scroll frames: per text node drops from full per-glyph transform + snapshot copy-back to one memcpy + 2 adds per vertex.
  • Non-text animation frames (focus pulses, fades — constant in TV UIs): the SDF bufferData driver copy is skipped entirely.
  • Scrolling text still uploads every frame (bytes genuinely change) — that's correct; the CPU to produce those bytes is what shrank.

Testing

  • Full unit suite: 310 tests pass (20 new)
    • WebGlRenderer.sdfBuffer.test.ts: translated-copy math (positions shifted, UVs/distanceRange untouched), bit-exact NaN-pattern color preservation, append/zero-glyph behavior, upload-skip semantics (skip on identical frame; re-upload on flag or size change), and every flag raiser incl. render-list rebuild and buffer growth
    • SdfTextRenderer.test.ts: three-way dispatch — miss snapshots the cache, exact hit mem-copies, translation hit passes the right delta and does NOT re-snapshot, scale/rotation/color/alpha changes fall back to the miss path
  • Manual, examples dev server:
    • stress-single-level-text (100 white SDF texts animating x/y — pure translate path with the NaN color pattern): crisp, correctly colored across frames
    • text, text-ssdf (static), text-scaling (miss path): render correctly, console clean
  • pnpm build clean; Prettier/ESLint clean (0 errors)

🤖 Generated with Claude Code

…uploads

Two SDF text hot-path fixes for the TV scroll scenario:

1. Translation fast path. The SDF vertex cache is world-space, so a
   scrolling text node missed it every frame: full per-glyph matrix
   math plus a cache re-snapshot (a second full copy) per node per
   frame. The hit test now compares only the scale/rotation components;
   when just tx/ty moved, addSdfTranslatedQuads mem-copies the cached
   vertices and adds the delta to the two position floats per vertex.
   The cache keeps its original base transform, so every frame
   recomputes from the same reference (no drift) and nothing is
   re-snapshotted. The copy must stay a typed-array set(): packed ABGR
   colors share the Float32Array and patterns like 0xFFFFFFFF (white)
   are float32 NaNs that element-wise copies could canonicalize.

2. Skip the SDF GPU upload when bytes are unchanged. render() uploaded
   the entire SDF buffer via bufferData on every drawn frame — any
   focus pulse or image fade re-uploaded every glyph on screen. This
   implements the sdfBufferChanged invariant documented in CLAUDE.md:
   uploadSdfBuffer skips when every write this frame was an exact
   cache-hit mem-copy and the size matches the last upload. The flag is
   raised by every byte- or offset-changing path, failing conservative:
   cache-miss recompute, translated copy, render-list rebuild (reorder
   moves identical bytes to different offsets), RTT partial upload
   (clobbers the GL buffer mid-frame), and backing-store growth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chiefcll chiefcll merged commit 5162645 into main Jul 2, 2026
1 check passed
@chiefcll chiefcll deleted the perf/sdf-scroll-fast-path-and-upload-skip branch July 2, 2026 19:59
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