refactor(shaders): make all WebGL fragment shaders branchless for Mali 400#118
Merged
Conversation
…i 400 Mali 400-class fragment pipelines have no dynamic branching: every if/ else/ternary serializes both paths for the batch, uniform-driven branches included. Replace all fragment-shader branches with step()/mix() arithmetic: - Ternary quadrant-radius selects in roundedBox/shadowBox (RoundedWith Shadow/Border/BorderAndShadow, HolePunch) now use the step+mix pattern from Rounded. - The varying-driven shadow-geometry branch in RoundedWithBorderAndShadow (worst offender: diverges per fragment) is now a step/mix parameter select into a single shadowBox call. - borderZero / u_borderGap early-return paths in RoundedWithBorder, RoundedWithBorderAndShadow and Border compute all composites and mix()-select. Runtime step/mix was chosen over compile-time #define variants because shader props are runtime-mutable (animations write shaderProps directly) while the program is fixed at createShader time — keying a variant on border-w/border-gap would break prop animation. The border vertex shaders now initialize all border varyings in the zero-border case so the always-evaluated fragment SDFs never read undefined values. - u_color.a / u_shadow_color.a gates in Shadow and RoundedWithShadow are step() multiplies. - Gradient stop loops (LinearGradient, RadialGradient, RadialProgress) use the branchless mix-accumulation from genGradientColors, rewritten to smoothstep-per-segment (pixel-identical to the old segment select for ascending stops) and unrolled per colors:N program variant. - RadialProgress duration/countdown/progress ternaries are step/mix with mix()-guarded denominators (a max()-epsilon would overflow fp16 mediump to Inf/NaN). Verified against the certified chromium-ci snapshots in Docker: 180/180 pass, pixel-identical to the branchy shaders. Adds shader-border-zero covering the merged degenerate paths (zero-width border, zero-alpha shadow) plus unit tests proving genGradientColors matches the reference segment select. 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
Removes every
if/else/ternary from the WebGL fragment shaders, per the Mali 400 / OpenGL ES 2.0 GPU floor documented in CLAUDE.md (in-order scalar fragment pipelines serialize both sides of any branch — including uniform-driven ones).r.xy = (p.x > 0.0) ? r.yz : r.xw; ...) inRoundedWithShadow,RoundedWithBorder,RoundedWithBorderAndShadow,HolePunch→ the branchlessstep()+mix()select already used inRounded(sharedquadRadius()helper).RoundedWithBorderAndShadow(worst offender — diverges per fragment) selecting between twoshadowBoxcalls →step()/mix()parameter select into a singleshadowBoxcall.borderZero,u_borderGap,u_color.a,u_shadow_color.a) inRoundedWithBorder,RoundedWithBorderAndShadow,Border,Shadow,RoundedWithShadow→ compute all composites,mix()-select the result.if+ return-inside-loop inLinearGradient,RadialGradient,RadialProgress→ the previously-unusedgenGradientColors()mix-accumulation inShaderUtils, rewritten to smoothstep-per-segment so it is pixel-identical to the old segment select, and unrolled percolors:N#definevariant.RadialProgressuniform ternaries (u_duration,u_countdown, progress normalize) →step()/mix(), withmix()-guarded denominators so the divisions stay finite at 0 without overflowing fp16mediumpto Inf/NaN (amax()-epsilon guard would).Why step/mix instead of
#definevariants for the uniform branchesShader props are runtime-mutable — animations write
shaderPropsdirectly andCoreShaderNodeprop setters fireRecalcUniforms— while the program is fixed atcreateShader()time (getCacheMarkersis evaluated once). Keying a compile-time variant onborder-w == 0/border-gap == 0would silently break a border animating from 0. The structuralcolors:Nmarkers (array length can't animate) keep their#defineunrolling. On Mali 400 the old serialized branch already paid for all paths per batch, so computing-everything+mix is equal or cheaper.Reviewer notes
v_outerBorderUv/v_innerBorderUv/v_*BorderRadiusin the zero-border case — the branchless fragment always evaluates the border SDFs, and an unwritten varying is undefined and can poison the finalmix()with NaN. (The remaining vertex-shaderif(borderZero == 0.0)is fine: the Mali 400 constraint is the fragment pipeline.)step(0.0001, abs(u_borderGap))—abs()matters, negative gaps are valid (seeshader-border-gap) and the original condition was== 0.0.RoundedWithBorder*stays at 7/8 rows),#ifdef GL_FRAGMENT_PRECISION_HIGHguards kept, every non-void GLSL function ends in an unconditionalreturn, uniform values remain a pure function ofresolvedProps+ node w/h.Verification
chromium-cisnapshots (generated from the old branchy shaders), run in Docker: 180/180 pass, pixel-identical.shader-border-zeroexample test + certified snapshot covering the merged degenerate paths (zero-width border on bothRoundedWithBorder*shaders, zero-alpha shadow onShadow/RoundedWithShadow) — manually inspected.genGradientColorsGLSL (viamix/smoothstepshims) against the original segment-select reference across a 100-point sweep.pnpm build,pnpm test(274 tests),pnpm lintclean.🤖 Generated with Claude Code