Skip to content

feat(core)!: renderOnlyInViewport — skip draw submission for bounds-margin nodes (default on)#98

Merged
chiefcll merged 2 commits into
mainfrom
feat/render-only-in-viewport
Jun 11, 2026
Merged

feat(core)!: renderOnlyInViewport — skip draw submission for bounds-margin nodes (default on)#98
chiefcll merged 2 commits into
mainfrom
feat/render-only-in-viewport

Conversation

@chiefcll

@chiefcll chiefcll commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What

New renderer setting renderOnlyInViewport, enabled by default (set renderOnlyInViewport: false to 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.

// default — margin-ring nodes preload but are not drawn
const renderer = new RendererMain({ boundsMargin: 400, ... });
// opt out (legacy clipped-draw behavior)
const renderer = new RendererMain({ boundsMargin: 400, renderOnlyInViewport: false, ... });

Why

boundsMargin currently 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 requires renderState === InViewport. Applied on the main path and the failed-texture placeholder branch. Texture ownership (needsTextureOwnership) deliberately keeps the old InBounds rule — 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).
  • No new transition plumbing: RenderState processing already raises IsRenderable, so margin↔viewport crossings recompute renderability in the same update pass. Opted-out users pay one boolean check per updateIsRenderable.

Measured

On the new render-only-in-viewport example (6 viewport quads, 2 margin-ring quads, 5 beyond) with the ?debug=true overlay: gate off → quads 10, gate on → quads 8 — exactly the ring count — with pixel-identical screenshots. A global ?strictrender=false examples 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

  • The renderable event 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.
  • RTT subtrees inherit render-state logic, so the gate also skips margin-ring content inside render textures — a widening of the existing OutOfBounds-RTT constraint.
  • No pop-in: textures finished loading back at the margin edge, so a node's first visible frame renders its real content (or its placeholderColor if still loading, as today).

Testing

  • Because the default is on, the full 176-snapshot VRT suite now runs every example through the gate — it passes 100%, which is direct evidence the feature is pixel-neutral across the corpus (it was also run and passed with the gate off).
  • 10 new unit tests across 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.
  • Full 176-snapshot visual regression suite passes in the CI Docker container — expected, since the feature changes no pixels by design (which is also why the new example page has no automation export: there is nothing for a snapshot to capture; the observable artifact is the perf counters).
  • pnpm build, prettier, eslint clean.

🤖 Generated with Claude Code

chiefcll and others added 2 commits June 11, 2026 08:13
…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>
@chiefcll chiefcll changed the title feat(core): renderOnlyInViewport — skip draw submission for bounds-margin nodes feat(core)!: renderOnlyInViewport — skip draw submission for bounds-margin nodes (default on) Jun 11, 2026
@chiefcll chiefcll merged commit dbc1c58 into main Jun 11, 2026
1 check passed
@chiefcll chiefcll deleted the feat/render-only-in-viewport branch June 11, 2026 12:48
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