perf(sdf): translation fast path for scrolling text + skip unchanged SDF uploads#121
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
addSdfQuadsand 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.renderQuadsnow dispatches three ways:addSdfCachedQuads.tx/tymoved. The newWebGlRenderer.addSdfTranslatedQuadsmem-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.set()(bit-exact memcpy). Packed ABGR colors live in the sameFloat32Array, and patterns like0xFFFFFFFF— plain white text — are float32 NaNs that element-wise float reads/writes can canonicalize into0x7FC00000, 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 viabufferData(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 thesdfBufferChangedinvariant 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:addSdfQuads)addSdfTranslatedQuads)invalidateQuadBuffer) — a reorder moves identical bytes to different offsetsrenderRTT) — clobbers the GL buffer's contents/size mid-frameensureSdfBufferCapacity)Impact
bufferDatadriver copy is skipped entirely.Testing
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 growthSdfTextRenderer.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 pathstress-single-level-text(100 white SDF texts animating x/y — pure translate path with the NaN color pattern): crisp, correctly colored across framestext,text-ssdf(static),text-scaling(miss path): render correctly, console cleanpnpm buildclean; Prettier/ESLint clean (0 errors)🤖 Generated with Claude Code