perf(webgl): skip redundant uniform uploads in bindRenderOp#92
Merged
Conversation
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>
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
bindRenderOpnow keeps shadow copies of each program's GL uniform state and only issuesgl.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
useProgramswitches, so interleaved ops (default-shader quads between rounded posters — the typical card structure) dedupe fully.Why it's sound
bindRenderOpis the only writer of these uniforms (attach()only callsuseProgram; the SDF shader has no props/update and never enters the collection loop), so the per-program shadows always mirror GL truth.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 existingshader.props !== undefinedguard.)-1cannot 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
WebGlShaderProgram.uniformDedup.test.ts, viaObject.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 buildclean; webgl + CoreNode suites 74/74.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, andtext-align(SDF program path) all render correctly with no GL errors.🤖 Generated with Claude Code