Skip to content

perf(webgl): skip redundant uniform uploads in bindRenderOp#92

Merged
chiefcll merged 1 commit into
mainfrom
perf/skip-redundant-uniform-uploads
Jun 10, 2026
Merged

perf(webgl): skip redundant uniform uploads in bindRenderOp#92
chiefcll merged 1 commit into
mainfrom
perf/skip-redundant-uniform-uploads

Conversation

@chiefcll

Copy link
Copy Markdown
Contributor

What

bindRenderOp now keeps shadow copies of each program's GL uniform state and only issues gl.uniform* calls when values actually differ. The shader uniform collection pass (the four bucket loops + their GL calls) is skipped entirely when the same collection object is already bound.

Why

Every render op re-issued its full uniform sequence — u_pixelRatio, u_resolution, u_alpha, u_dimensions, plus the shader's collection — even when the program already held identical values. On Chrome, each GL call is serialized into the GPU-process command buffer, which is CPU cost on the embedded targets this engine serves. A TV grid of ~50 shaded cards (each its own op today) pays ~600 uniform calls per frame for values that rarely change; with this change a row of same-prop, same-size cards pays the sequence once and ~0 uniform calls per subsequent card, leaving only texture binds and draws.

No batching or adjacency is required. GL uniform values are state on the program object and persist across useProgram switches, so interleaved ops (default-shader quads between rounded posters — the typical card structure) dedupe fully.

Why it's sound

  1. bindRenderOp is the only writer of these uniforms (attach() only calls useProgram; the SDF shader has no props/update and never enters the collection loop), so the per-program shadows always mirror GL truth.
  2. Uniform collections are immutable after fill and shared by identity: WebGlShaderNode.update() either reuses a value-key-cached collection or creates and fills a fresh one — never mutates one afterward. Reference equality therefore implies value equality. (The one in-place-mutation path — propless shaders — is already excluded by the existing shader.props !== undefined guard.)
  3. Conservative failure direction: an unrecognized collection or changed scalar causes a redundant upload (today's behavior), never a wrong skip. Sentinel -1 cannot collide with real values (all ≥ 0).

Edge cases covered: RTT passes flip pixelRatio/resolution and the value compare catches both directions; canvas resize re-uploads via the compare; context loss requires app reload by design (no restore path to invalidate against). Canvas2D backend untouched.

Testing

  • 8 new unit tests (WebGlShaderProgram.uniformDedup.test.ts, via Object.create — no GL context): full upload on first bind, zero calls on identical re-bind, dedup across interleaved ops, single-uniform re-upload on alpha/dimension change, conservative re-upload for distinct-identity collections, RTT flip both directions, propless-shader guard.
  • pnpm build clean; webgl + CoreNode suites 74/74.
  • Browser-verified: shader-border (the adversarial page — many RoundedWithBorder nodes with different border widths/sides/colors/radii per node renders each correctly, proving distinct collections re-upload), shader-rounded, and text-align (SDF program path) all render correctly with no GL errors.
  • CI visual regression should be pixel-identical — the GPU receives the same values, just not redundantly.

🤖 Generated with Claude Code

Every render op re-issued its full gl.uniform* sequence even when the
program already held identical values - on Chrome each of those calls is
serialized into the GPU-process command buffer, so a screen of ~50
single-op shaded cards paid ~600 uniform calls per frame for values that
rarely change.

bindRenderOp now keeps shadow copies of the program's uniform state and
compare-and-sets: u_pixelRatio, u_resolution, u_time, u_alpha and
u_dimensions are only re-uploaded when their values differ, and the shader
uniform collection pass (loops + gl calls) is skipped entirely when the
same collection object is already bound.

This is sound because GL uniform values are per-program state that
persists across useProgram switches (interleaved ops of other programs
don't disturb them), bindRenderOp is the only writer of these uniforms,
and uniform collections are created once, filled once, and shared by
reference across shader nodes with equal value keys - reference equality
implies value equality. The failure direction is conservative: an
unrecognized collection causes a redundant upload, never a wrong skip.

No batching or adjacency required: ops interleaved with other shaders
(default-shader quads between rounded posters) still dedupe fully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chiefcll chiefcll merged commit 1a843a8 into main Jun 10, 2026
1 check passed
@chiefcll chiefcll deleted the perf/skip-redundant-uniform-uploads branch June 10, 2026 13:08
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