feat(core)!: renderOnlyInViewport — skip draw submission for bounds-margin nodes (default on)#98
Merged
Merged
Conversation
…rgin nodes boundsMargin currently bundles two consequences: nodes in the margin ring preload their textures AND are fully rendered every frame (quad writes, texture binds, draw-call segmentation), with the GPU clipping away the invisible fragments. On TV SoCs those GL calls are pure CPU tax for content that produces no pixels. New renderer setting renderOnlyInViewport (default false): margin-ring nodes keep full updates and texture ownership — fetch/decode/upload and cleanup protection are unchanged, the margin remains the preload runway — but they stay out of the render list until they actually intersect the viewport. Render-list rebuilds move from the margin edge to the visible edge (same frequency per crossing); per-frame quad submission drops by the ring count. Gated in CoreNode.updateIsRenderable (main path + failed-texture placeholder branch) and in CoreTextNode's SDF override, which bypasses the base method. RenderState processing already raises IsRenderable, so margin<->viewport crossings recompute renderability with no new plumbing; default-off users pay one boolean check. Adds a ?strictrender=true examples URL param (like ?novao) and a render-only-in-viewport example page so the clipped-quad overhead can be A/B'd with the ?debug=true overlay on target devices. Measured: quads dropped exactly by the margin-ring count with pixel-identical output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Margin-ring nodes now skip draw submission by default; set renderOnlyInViewport: false in renderer settings to restore legacy clipped-draw behavior. The examples ?strictrender param flips to an opt-out (?strictrender=false) accordingly. Verified: full 176-snapshot VRT suite passes with the default on — the gate is pixel-neutral across the entire example corpus. 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
New renderer setting
renderOnlyInViewport, enabled by default (setrenderOnlyInViewport: falseto restore legacy behavior). Nodes inside the bounds margin but outside the visible viewport keep their full update pass and texture ownership — fetch, decode, throttled GPU upload, and memory-manager cleanup protection are all unchanged, so the margin remains the preload runway — but they stay out of the render list until they actually intersect the viewport.Why
boundsMargincurrently bundles two consequences: margin-ring nodes preload their textures and are fully rendered every frame — quad-buffer writes, texture binds, draw-call segmentation — with the GPU clipping away every fragment. Per the TV cost model, those GL calls are pure driver-CPU tax for content that produces zero pixels. A 50-card margin ring is 50 quad submissions and their share of render-op overhead on every scroll frame.How
CoreNode.updateIsRenderable()gains a two-comparison gate (cached fields, explicit comparisons): renderable additionally requiresrenderState === InViewport. Applied on the main path and the failed-texture placeholder branch. Texture ownership (needsTextureOwnership) deliberately keeps the oldInBoundsrule — that's the decoupling.CoreTextNode's SDF override gets the same gate — it bypasses the base method entirely (live testing caught this: quad counts didn't move until SDF text was gated separately).RenderStateprocessing already raisesIsRenderable, so margin↔viewport crossings recompute renderability in the same update pass. Opted-out users pay one boolean check perupdateIsRenderable.Measured
On the new
render-only-in-viewportexample (6 viewport quads, 2 margin-ring quads, 5 beyond) with the?debug=trueoverlay: gate off →quads 10, gate on →quads 8— exactly the ring count — with pixel-identical screenshots. A global?strictrender=falseexamples URL param (mirroring?novao) restores legacy behavior for device A/B testing; the real question worth measuring on TV hardware is whether render-list rebuilds landing on visible-edge frames (same per-crossing frequency as today, different timing) affect scroll smoothness.Trade-offs for reviewers
renderableevent and autosize patching fire at viewport entry instead of margin entry — flagged in the setting docs; worth checking nothing app-side depends on margin-edge timing before enabling.OutOfBounds-RTT constraint.placeholderColorif still loading, as today).Testing
CoreNode.test.ts/CoreTextNode.test.ts: margin node not renderable but still owns its texture; viewport node renderable; crossings toggle both directions; out-of-bounds still releases ownership; placeholder and color-only nodes gated; SDF text gated; explicit opt-out preserves legacy behavior. 436 tests pass.automationexport: there is nothing for a snapshot to capture; the observable artifact is the perf counters).pnpm build, prettier, eslint clean.🤖 Generated with Claude Code