Skip to content

perf(webgl): batch distinct shader nodes with equal uniform collections#120

Merged
chiefcll merged 1 commit into
mainfrom
perf/batch-equal-value-shader-nodes
Jul 2, 2026
Merged

perf(webgl): batch distinct shader nodes with equal uniform collections#120
chiefcll merged 1 commit into
mainfrom
perf/batch-equal-value-shader-nodes

Conversation

@chiefcll

@chiefcll chiefcll commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

The render-op batch test in WebGlRenderer.reuseRenderOp rejected any two quads whose shader nodes were different instances (curShader !== shader). Apps create one shader node per card (renderer.createShader('Rounded', {...}) at construction), so a rail of visually identical rounded cards never batched — one render op per card, each paying useShader + bindRenderOp + scissor + drawElements dispatch on the driver CPU. On TV SoCs (~0.5–2µs per GL call) that's a meaningful per-frame tax for the standard left-nav/rows layout.

How

Shader nodes with equal value keys already share their uniform collection by reference (the shader-value cache in WebGlShaderNode.update), and collections are immutable after fill — so reference equality implies value equality. The gate now batches distinct shader nodes when they resolve to:

  1. the same program, and
  2. the same uniform collection (reference compare).

Program identity is required for correctness: the shader value key (CoreShaderNode.createValueKey) does not include the shader type, so an equal collection reached from a different program (key collision across shader types) must never merge. A regression test covers this case.

WebGlShaderProgram.reuseRenderOp gets the same one-reference compare in place of the per-node for..in over resolvedProps (the prop walk is kept as a fallback for distinct collections), and no longer invokes the node.time getter for programs without a time uniform.

Measured

?test=stress-tv&debug=true, 200 rounded-rect cards (tier "rounded rect only"), clean dev server per side:

draws quads GL calls/interval
main 205 204 845
this PR 6 204 48

Reviewer notes

  • The per-op system checks (worldAlpha / dimensions / time) still run in WebGlShaderProgram.reuseRenderOp after the gate, so alpha-animated or differently-sized cards split correctly.
  • Verified visually that different-props nodes still split: the shader-rounded example (every node has different corner/border/shadow props) renders pixel-identical — borders, shadows, and per-corner radii intact. The stress-tv interleaved tier (cards + unique image textures) still splits on texture changes as before.
  • Batching semantics for same-instance shader nodes, the default shader, RTT, and clipping are unchanged.

Testing

  • Full unit suite: 295 tests pass
  • New: WebGlRenderer.reuseRenderOp.test.ts (8 tests — shared-collection batching, cross-program rejection, clipping/default paths, program-level veto still consulted) and WebGlShaderProgram.reuseRenderOp.test.ts (7 tests — reference short-circuit, prop-compare fallback, alpha/dimension rejects, time getter untouched, canBatch delegation)
  • pnpm build clean; Prettier/ESLint clean (0 errors)

🤖 Generated with Claude Code

The render-op batch test rejected any two quads whose shader NODES were
different instances (curShader !== shader), but apps create one shader
node per card — so a rail of identical rounded cards never batched:
one draw op per card, each paying useShader + bindRenderOp + scissor +
drawElements dispatch on the driver CPU.

Shader nodes with equal value keys already share their uniform
collection by reference (the shader-value cache), and collections are
immutable after fill — so reference equality implies value equality.
Batch when the shader nodes resolve to the same program AND the same
uniform collection. Program identity is required because the value key
does not include the shader type, so an equal collection reached from
a different program must not merge.

WebGlShaderProgram.reuseRenderOp uses the same one-reference compare in
place of the per-node for..in over resolvedProps (kept as fallback for
distinct collections), and no longer invokes the node.time getter for
programs without a time uniform.

Measured on stress-tv, 200 rounded-rect cards (tier "rounded rect
only"): 205 draws -> 6 draws for the identical scene. Different-props
nodes (shader-rounded example) still split correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chiefcll chiefcll merged commit 0bd60fb into main Jul 2, 2026
1 check passed
@chiefcll chiefcll deleted the perf/batch-equal-value-shader-nodes branch July 2, 2026 03:38
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