diff --git a/examples/tests/shader-border-zero.ts b/examples/tests/shader-border-zero.ts new file mode 100644 index 0000000..8ed4429 --- /dev/null +++ b/examples/tests/shader-border-zero.ts @@ -0,0 +1,94 @@ +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export async function automation(settings: ExampleSettings) { + // Snapshot single page + await test(settings); + await settings.snapshot(); +} + +/** + * Degenerate-prop paths of the border/shadow shaders: zero-width borders and + * fully transparent shadow colors. These paths are selected branchlessly in + * the fragment shaders (Mali 400 target), so this locks in that a zero border + * renders exactly like plain Rounded and a zero-alpha shadow renders nothing. + */ +export default async function test({ renderer, testRoot }: ExampleSettings) { + const node = renderer.createNode({ + x: 0, + y: 0, + w: 1920, + h: 1080, + color: 0xffffffff, + parent: testRoot, + }); + + // Zero-width border: must render identical to plain Rounded + renderer.createNode({ + x: 300, + y: 300, + mount: 0.5, + w: 250, + h: 250, + color: 0xff00ffff, + shader: renderer.createShader('RoundedWithBorder', { + radius: 30, + 'border-w': 0, + 'border-color': 0xff0000ff, + }), + parent: node, + }); + + // Zero-width border with a shadow: shadow hugs the node box + renderer.createNode({ + x: 700, + y: 300, + mount: 0.5, + w: 250, + h: 250, + color: 0xff00ffff, + shader: renderer.createShader('RoundedWithBorderAndShadow', { + radius: 30, + 'border-w': 0, + 'border-color': 0xff0000ff, + 'shadow-x': 50, + 'shadow-spread': 50, + 'shadow-blur': 100, + }), + parent: node, + }); + + // Zero-alpha shadow color: no shadow may appear + renderer.createNode({ + x: 1100, + y: 300, + mount: 0.5, + w: 250, + h: 250, + color: 0xff00ffff, + shader: renderer.createShader('Shadow', { + x: 50, + spread: 50, + blur: 100, + color: 0x00000000, + }), + parent: node, + }); + + // Zero-alpha rounded shadow color: plain rounded corners, no shadow + renderer.createNode({ + x: 1500, + y: 300, + mount: 0.5, + w: 250, + h: 250, + color: 0xff00ffff, + shader: renderer.createShader('RoundedWithShadow', { + radius: 30, + 'shadow-x': 50, + 'shadow-spread': 50, + 'shadow-blur': 100, + 'shadow-color': 0x00000000, + }), + parent: node, + }); +} diff --git a/src/core/renderers/webgl/internal/ShaderUtils.test.ts b/src/core/renderers/webgl/internal/ShaderUtils.test.ts new file mode 100644 index 0000000..c08890f --- /dev/null +++ b/src/core/renderers/webgl/internal/ShaderUtils.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import { genGradientColors } from './ShaderUtils.js'; + +type Vec4 = [number, number, number, number]; + +const smoothstep = (e0: number, e1: number, x: number): number => { + const t = Math.min(Math.max((x - e0) / (e1 - e0), 0), 1); + return t * t * (3 - 2 * t); +}; + +const mix = (a: Vec4, b: Vec4, t: number): Vec4 => [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + a[3] + (b[3] - a[3]) * t, +]; + +/** + * Evaluate the generated GLSL statements as JS. The generated code only uses + * `mix`, `smoothstep`, `u_colors[i]`, `u_stops[i]` and `dist`, so shimming + * those makes the string directly executable. + */ +function evalGradient(stops: number[], colors: Vec4[], dist: number): Vec4 { + const src = genGradientColors(stops.length).replace( + 'vec4 colorOut =', + 'let colorOut =', + ); + const fn = new Function( + 'mix', + 'smoothstep', + 'u_stops', + 'u_colors', + 'dist', + `${src}; return colorOut;`, + ); + return fn(mix, smoothstep, stops, colors, dist) as Vec4; +} + +/** + * Reference implementation: the original branchy segment select + * (below first stop / above last stop / smoothstep within the segment). + */ +function referenceGradient( + stops: number[], + colors: Vec4[], + dist: number, +): Vec4 { + dist = Math.min(Math.max(dist, 0), 1); + if (dist <= stops[0]!) return colors[0]!; + const last = stops.length - 1; + if (dist >= stops[last]!) return colors[last]!; + for (let i = 0; i < last; i++) { + if (dist >= stops[i]! && dist <= stops[i + 1]!) { + return mix( + colors[i]!, + colors[i + 1]!, + smoothstep(stops[i]!, stops[i + 1]!, dist), + ); + } + } + return colors[last]!; +} + +const RED: Vec4 = [1, 0, 0, 1]; +const GREEN: Vec4 = [0, 1, 0, 1]; +const BLUE: Vec4 = [0, 0, 1, 0.5]; +const WHITE: Vec4 = [1, 1, 1, 1]; + +describe('genGradientColors', () => { + it('emits no branches (no if / ternary / return)', () => { + const src = genGradientColors(4); + expect(src.includes('if')).toBe(false); + expect(src.includes('?')).toBe(false); + expect(src.includes('return')).toBe(false); + }); + + it('with a single stop resolves to the first color', () => { + const out = evalGradient([0], [RED], 0.7); + expect(out).toEqual(RED); + }); + + it('clamps to the first color below the first stop', () => { + const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.1); + expect(out).toEqual(RED); + }); + + it('clamps to the last color above the last stop', () => { + const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.9); + expect(out).toEqual(GREEN); + }); + + it('interpolates with smoothstep inside a segment', () => { + const dist = 0.5; + const out = evalGradient([0.25, 0.75], [RED, GREEN], dist); + const expected = mix(RED, GREEN, smoothstep(0.25, 0.75, dist)); + for (let i = 0; i < 4; i++) { + expect(out[i]).toBeCloseTo(expected[i]!, 6); + } + }); + + it('matches the reference segment select across a multi-stop ramp', () => { + const stops = [0, 0.3, 0.6, 1]; + const colors = [RED, GREEN, BLUE, WHITE]; + for (let d = 0; d <= 100; d++) { + const dist = d / 100; + const out = evalGradient(stops, colors, dist); + const ref = referenceGradient(stops, colors, dist); + for (let i = 0; i < 4; i++) { + expect(out[i]).toBeCloseTo(ref[i]!, 6); + } + } + }); + + it('is exact at stop boundaries', () => { + const stops = [0, 0.5, 1]; + const colors = [RED, GREEN, BLUE]; + expect(evalGradient(stops, colors, 0)).toEqual(RED); + const mid = evalGradient(stops, colors, 0.5); + for (let i = 0; i < 4; i++) { + expect(mid[i]).toBeCloseTo(GREEN[i]!, 6); + } + expect(evalGradient(stops, colors, 1)).toEqual(BLUE); + }); +}); diff --git a/src/core/renderers/webgl/internal/ShaderUtils.ts b/src/core/renderers/webgl/internal/ShaderUtils.ts index 93a1fae..2c474d1 100644 --- a/src/core/renderers/webgl/internal/ShaderUtils.ts +++ b/src/core/renderers/webgl/internal/ShaderUtils.ts @@ -243,21 +243,22 @@ export const DefaultVertexSource = ` `; /** - * generate fragment source for - * @param stops - * @returns + * Generates branchless gradient-stop evaluation statements for a fragment + * shader. Expects a `float dist` in scope and `u_stops`/`u_colors` uniform + * arrays of length `stops`; leaves the result in `vec4 colorOut`. + * + * The accumulated `mix()` chain is exactly equivalent to selecting the + * segment containing `dist` (for ascending stops): below a segment the + * smoothstep is 0 (no-op), above it is 1 (fully replaced by the next color). + * No `if`/ternary — Mali 400-class fragment pipelines serialize any branch. */ export function genGradientColors(stops: number): string { - let result = ` - float stopCalc = (dist - u_stops[0]) / (u_stops[1] - u_stops[0]); - vec4 colorOut = mix(u_colors[0], u_colors[1], stopCalc); - `; - if (stops > 2) { - for (let i = 2; i < stops; i++) { - result += `colorOut = mix(colorOut, u_colors[${i}], clamp((dist - u_stops[${ - i - 1 - }]) / (u_stops[${i}] - u_stops[${i - 1}]), 0.0, 1.0));`; - } + let result = `vec4 colorOut = u_colors[0];`; + for (let i = 1; i < stops; i++) { + result += ` + colorOut = mix(colorOut, u_colors[${i}], smoothstep(u_stops[${ + i - 1 + }], u_stops[${i}], dist));`; } return result; } diff --git a/src/core/shaders/webgl/Border.ts b/src/core/shaders/webgl/Border.ts index 1618870..06b580d 100644 --- a/src/core/shaders/webgl/Border.ts +++ b/src/core/shaders/webgl/Border.ts @@ -135,24 +135,25 @@ export const Border: WebGlShaderType = { float outerDist = box(boxUv + v_outerBorderUv, v_outerSize - edgeWidth); float innerDist = box(boxUv + v_innerBorderUv, v_innerSize - edgeWidth); - if(u_borderGap == 0.0) { - float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); - float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - resultColor = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a); - resultColor = mix(resultColor, color, innerAlpha); - gl_FragColor = resultColor * u_alpha; - return; - } + float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); + float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); float nodeDist = box(boxUv, v_halfDimensions - edgeWidth); float nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); float borderDist = max(-innerDist, outerDist); float borderAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderDist); - resultColor = mix(resultColor, color, nodeAlpha); - resultColor = mix(resultColor, u_borderColor, borderAlpha * u_borderColor.a); - gl_FragColor = resultColor * u_alpha; + // Branchless gap select -- Mali 400 serializes uniform branches, so both + // composites are computed and mix()-selected on hasGap instead. + vec4 resNoGap = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a); + resNoGap = mix(resNoGap, color, innerAlpha); + + vec4 resGap = mix(resultColor, color, nodeAlpha); + resGap = mix(resGap, u_borderColor, borderAlpha * u_borderColor.a); + + float hasGap = step(0.0001, abs(u_borderGap)); + gl_FragColor = mix(resNoGap, resGap, hasGap) * u_alpha; } `, }; diff --git a/src/core/shaders/webgl/HolePunch.ts b/src/core/shaders/webgl/HolePunch.ts index 778b672..36f0201 100644 --- a/src/core/shaders/webgl/HolePunch.ts +++ b/src/core/shaders/webgl/HolePunch.ts @@ -46,11 +46,18 @@ export const HolePunch: WebGlShaderType = { void main() { vec4 color = texture2D(u_texture, v_textureCoords) * v_color; vec2 p = (v_textureCoords.xy * u_dimensions.xy - u_pos) - u_size; - vec4 r = u_radius; - r.xy = (p.x > 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - p = abs(p) - u_size + r.x; - float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r.x + 2.0; + + // Branchless radius selection based on quadrant + // x: TL, y: TR, z: BR, w: BL + vec2 stepVal = step(vec2(0.0), p); + float r = mix( + mix(u_radius.x, u_radius.y, stepVal.x), + mix(u_radius.w, u_radius.z, stepVal.x), + stepVal.y + ); + + p = abs(p) - u_size + r; + float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r + 2.0; float roundedAlpha = 1.0 - smoothstep(0.0, u_pixelRatio, dist); gl_FragColor = mix(color, vec4(0.0), min(color.a, roundedAlpha)); } diff --git a/src/core/shaders/webgl/LinearGradient.ts b/src/core/shaders/webgl/LinearGradient.ts index e321875..864524c 100644 --- a/src/core/shaders/webgl/LinearGradient.ts +++ b/src/core/shaders/webgl/LinearGradient.ts @@ -6,6 +6,7 @@ import { } from '../templates/LinearGradientTemplate.js'; import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js'; import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js'; +import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js'; export const LinearGradient: WebGlShaderType = { props: LinearGradientTemplate.props, @@ -56,7 +57,6 @@ export const LinearGradient: WebGlShaderType = { # endif #define MAX_STOPS ${props.colors.length} - #define LAST_STOP ${props.colors.length - 1} uniform float u_alpha; @@ -72,24 +72,8 @@ export const LinearGradient: WebGlShaderType = { vec4 getGradientColor(float dist) { dist = clamp(dist, 0.0, 1.0); - - if(dist <= u_stops[0]) { - return u_colors[0]; - } - - if(dist >= u_stops[LAST_STOP]) { - return u_colors[LAST_STOP]; - } - - for(int i = 0; i < LAST_STOP; i++) { - float left = u_stops[i]; - float right = u_stops[i + 1]; - if(dist >= left && dist <= right) { - float lDist = smoothstep(left, right, dist); - return mix(u_colors[i], u_colors[i + 1], lDist); - } - } - return u_colors[LAST_STOP]; + ${genGradientColors(props.colors.length)} + return colorOut; } void main() { diff --git a/src/core/shaders/webgl/RadialGradient.ts b/src/core/shaders/webgl/RadialGradient.ts index f92b080..3f6b08a 100644 --- a/src/core/shaders/webgl/RadialGradient.ts +++ b/src/core/shaders/webgl/RadialGradient.ts @@ -6,6 +6,7 @@ import { } from '../templates/RadialGradientTemplate.js'; import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js'; import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js'; +import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js'; export const RadialGradient: WebGlShaderType = { props: RadialGradientTemplate.props, @@ -37,7 +38,6 @@ export const RadialGradient: WebGlShaderType = { # endif #define MAX_STOPS ${props.colors.length} - #define LAST_STOP ${props.colors.length - 1} uniform float u_alpha; uniform vec2 u_dimensions; @@ -56,25 +56,8 @@ export const RadialGradient: WebGlShaderType = { vec4 getGradientColor(float dist) { dist = clamp(dist, 0.0, 1.0); - - if(dist <= u_stops[0]) { - return u_colors[0]; - } - - if(dist >= u_stops[LAST_STOP]) { - return u_colors[LAST_STOP]; - } - - for(int i = 0; i < LAST_STOP; i++) { - float left = u_stops[i]; - float right = u_stops[i + 1]; - if(dist >= left && dist <= right) { - float lDist = smoothstep(left, right, dist); - return mix(u_colors[i], u_colors[i + 1], lDist); - } - } - - return u_colors[LAST_STOP]; + ${genGradientColors(props.colors.length)} + return colorOut; } void main() { diff --git a/src/core/shaders/webgl/RadialProgress.ts b/src/core/shaders/webgl/RadialProgress.ts index 29151f2..e2eb928 100644 --- a/src/core/shaders/webgl/RadialProgress.ts +++ b/src/core/shaders/webgl/RadialProgress.ts @@ -6,6 +6,7 @@ import { } from '../templates/RadialProgressTemplate.js'; import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js'; import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js'; +import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js'; export const RadialProgress: WebGlShaderType = { props: RadialProgressTemplate.props, @@ -57,7 +58,6 @@ export const RadialProgress: WebGlShaderType = { # endif #define MAX_STOPS ${maxStops} - #define LAST_STOP ${maxStops - 1} #define CAP_ROUND ${props.cap} #define HAS_TRACK ${props.trackColor !== 0 ? 1 : 0} @@ -87,22 +87,8 @@ export const RadialProgress: WebGlShaderType = { vec4 getGradientColor(float dist) { dist = clamp(dist, 0.0, 1.0); - - if (dist <= u_stops[0]) { - return u_colors[0]; - } - if (dist >= u_stops[LAST_STOP]) { - return u_colors[LAST_STOP]; - } - for (int i = 0; i < LAST_STOP; i++) { - float left = u_stops[i]; - float right = u_stops[i + 1]; - if (dist >= left && dist <= right) { - float lDist = smoothstep(left, right, dist); - return mix(u_colors[i], u_colors[i + 1], lDist); - } - } - return u_colors[LAST_STOP]; + ${genGradientColors(maxStops)} + return colorOut; } // Coverage of a disc centered at \`c\` with radius \`r\` at pixel \`p\` (with 1px AA) @@ -116,9 +102,13 @@ export const RadialProgress: WebGlShaderType = { // Effective progress: when u_duration > 0 the shader self-animates from // u_time, otherwise we use the static u_progress prop. countdown == 1 // drains (1 -> 0), countdown == 0 fills (0 -> 1). - float cyclePos = u_duration > 0.0 ? fract(u_time / u_duration) : 0.0; - float animProgress = u_countdown > 0.5 ? 1.0 - cyclePos : cyclePos; - float progress = u_duration > 0.0 ? animProgress : u_progress; + // Branchless (Mali 400 serializes uniform branches): the mix()-guarded + // denominator keeps the division finite when u_duration == 0 -- a + // max()-clamped epsilon would overflow fp16 mediump to Inf/NaN. + float hasDuration = step(0.0001, u_duration); + float cyclePos = fract(u_time / mix(1.0, u_duration, hasDuration)) * hasDuration; + float animProgress = mix(cyclePos, 1.0 - cyclePos, step(0.5, u_countdown)); + float progress = mix(u_progress, animProgress, hasDuration); vec2 p = v_nodeCoords.xy * u_dimensions - u_center; float dist = length(p); @@ -135,7 +125,7 @@ export const RadialProgress: WebGlShaderType = { // Filled arc coverage (1 if in filled arc, else 0). When progress >= 1 the // whole ring is filled regardless of \`t\` -- guards against the mod() seam. - float arcCoverage = progress >= 1.0 ? 1.0 : step(t, progress); + float arcCoverage = max(step(1.0, progress), step(t, progress)); float fillCoverage = ringCoverage * arcCoverage; #if CAP_ROUND @@ -151,8 +141,10 @@ export const RadialProgress: WebGlShaderType = { #endif // Sample gradient. Normalize \`t\` to the *filled* portion so the gradient - // spans the visible arc end-to-end regardless of progress. - float gradT = progress > 0.0 ? clamp(t / progress, 0.0, 1.0) : 0.0; + // spans the visible arc end-to-end regardless of progress. mix()-guarded + // denominator: safe at progress == 0 without fp16 overflow. + float hasProgress = step(0.000001, progress); + float gradT = clamp(t / mix(1.0, progress, hasProgress), 0.0, 1.0) * hasProgress; vec4 fillCol = getGradientColor(gradT); // Composite: track under fill (if track enabled), both gated by ringCoverage. diff --git a/src/core/shaders/webgl/RoundedWithBorder.ts b/src/core/shaders/webgl/RoundedWithBorder.ts index ac66fc9..07000f3 100644 --- a/src/core/shaders/webgl/RoundedWithBorder.ts +++ b/src/core/shaders/webgl/RoundedWithBorder.ts @@ -64,6 +64,15 @@ export const RoundedWithBorder: WebGlShaderType = { v_innerSize = vec2(0.0); v_outerSize = vec2(0.0); + // Defaults for the zero-border case: the fragment shader is branchless + // and always evaluates the border SDFs, so every varying must carry a + // finite value (an unwritten varying is undefined and can poison the + // final mix() with NaN). + v_outerBorderUv = vec2(0.0); + v_innerBorderUv = vec2(0.0); + v_outerBorderRadius = u_radius; + v_innerBorderRadius = u_radius; + if(borderZero == 0.0) { vec4 adjustedBorderWidth = u_borderWidth - 1.0 + clamp(u_borderWidth, -1.0, 1.0); @@ -72,9 +81,6 @@ export const RoundedWithBorder: WebGlShaderType = { float borderBottom = adjustedBorderWidth.z; float borderLeft = adjustedBorderWidth.w; - v_outerBorderUv = vec2(0.0); - v_innerBorderUv = vec2(0.0); - vec2 borderSize = vec2(borderRight + borderLeft, borderTop + borderBottom); vec2 extraSize = borderSize * u_borderAlign; float gapLeft = step(0.001, borderLeft) * u_borderGap; @@ -159,52 +165,49 @@ export const RoundedWithBorder: WebGlShaderType = { varying vec4 v_outerBorderRadius; varying vec2 v_halfDimensions; + // Branchless quadrant radius select (r: x TL, y TR, z BR, w BL) -- + // Mali 400-class fragment pipelines serialize any branch, ternaries included. + float quadRadius(vec2 p, vec4 r) { + vec2 stepVal = step(vec2(0.0), p); + return mix(mix(r.x, r.y, stepVal.x), mix(r.w, r.z, stepVal.x), stepVal.y); + } + float roundedBox(vec2 p, vec2 s, vec4 r) { - r.xy = (p.x > 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - vec2 q = abs(p) - s + r.x; - return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - r.x; + float rad = quadRadius(p, r); + vec2 q = abs(p) - s + rad; + return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - rad; } void main() { vec4 color = texture2D(u_texture, v_textureCoords) * v_color; - vec4 resultColor = vec4(0.0); vec2 boxUv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; float borderZero = 1.0 - step(0.001, dot(abs(u_borderWidth), vec4(1.0))); float edgeWidth = 1.0 / u_pixelRatio; - float nodeDist; - float nodeAlpha; - - if(borderZero == 1.0) { - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); - nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); - gl_FragColor = (color * nodeAlpha) * u_alpha; - return; - } + float nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); + float nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); float outerDist = roundedBox(boxUv + v_outerBorderUv, v_outerSize - edgeWidth, v_outerBorderRadius); float innerDist = roundedBox(boxUv + v_innerBorderUv, v_innerSize - edgeWidth, v_innerBorderRadius); - - if(u_borderGap == 0.0) { - float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); - float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - resultColor = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a); - resultColor = mix(resultColor, color, innerAlpha); - gl_FragColor = resultColor * u_alpha; - return; - } - - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); - nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); + float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - float gapAlpha = max(0.0, innerAlpha - nodeAlpha); + float gapAlpha = max(0.0, innerAlpha - nodeAlpha); float borderDist = max(-innerDist, outerDist); float borderAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderDist); - resultColor = (color * nodeAlpha) + (u_fillColor * gapAlpha); - resultColor = mix(resultColor, u_borderColor, borderAlpha * u_borderColor.a); + // Branchless path select -- Mali 400 serializes uniform branches, so all + // three composites (no gap / gap / zero border) are computed and + // mix()-selected instead of if/else'd. + vec4 resNoGap = mix(vec4(0.0), u_borderColor, outerAlpha * u_borderColor.a); + resNoGap = mix(resNoGap, color, innerAlpha); + + vec4 resGap = (color * nodeAlpha) + (u_fillColor * gapAlpha); + resGap = mix(resGap, u_borderColor, borderAlpha * u_borderColor.a); + + float hasGap = step(0.0001, abs(u_borderGap)); + vec4 resultColor = mix(resNoGap, resGap, hasGap); + resultColor = mix(resultColor, color * nodeAlpha, borderZero); gl_FragColor = resultColor * u_alpha; } `, diff --git a/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts b/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts index a2d17e0..75daebd 100644 --- a/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +++ b/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts @@ -71,6 +71,15 @@ export const RoundedWithBorderAndShadow: WebGlShaderType 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - vec2 q = abs(p) - s + r.x; - return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - r.x; + float rad = quadRadius(p, r); + vec2 q = abs(p) - s + rad; + return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - rad; } float shadowBox(vec2 p, vec2 s, vec4 r) { - r.xy = (p.x > 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - vec2 q = abs(p) - s + r.x; - float dist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; + float rad = quadRadius(p, r); + vec2 q = abs(p) - s + rad; + float dist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - rad; return 1.0 - smoothstep(-u_shadow.w, u_shadow.w + u_shadow.z, dist); } void main() { vec4 color = texture2D(u_texture, v_textureCoords) * v_color; - vec4 resultColor = vec4(0.0); vec2 boxUv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; float borderZero = 1.0 - step(0.001, dot(abs(u_borderWidth), vec4(1.0))); float edgeWidth = 1.0 / u_pixelRatio; - float nodeDist; - float nodeAlpha; - float shadowAlpha; - - if(borderZero == 1.0) { - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); - nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); - shadowAlpha = shadowBox(boxUv - u_shadow.xy, v_halfDimensions + u_shadow.w - edgeWidth, u_radius + u_shadow.z); - resultColor = mix(resultColor, u_shadowColor, shadowAlpha); - gl_FragColor = mix(resultColor, color, nodeAlpha) * u_alpha; - return; - } - if(v_outerSize.x > v_halfDimensions.x || v_outerSize.y > v_halfDimensions.y) { - shadowAlpha = shadowBox(boxUv + v_outerBorderUv - u_shadow.xy, v_outerSize + u_shadow.w - edgeWidth, v_outerBorderRadius + u_shadow.z); - } - else { - shadowAlpha = shadowBox(boxUv - u_shadow.xy, v_halfDimensions + u_shadow.w - edgeWidth, u_radius + u_shadow.z); - } + float nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); + float nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); + + // Shadow geometry select, branchless: when the border grows the quad + // beyond the node (outer size exceeds half dimensions on either axis) + // the shadow hugs the outer border box, otherwise the node box. This + // replaces a varying-driven if/else (worst offender: diverges per + // fragment) with a step()/mix() parameter select into ONE shadowBox call. + // When borderZero, v_outerSize is vec2(0.0) so outerSel is 0 and the + // node-box parameters win, matching the old zero-border path. + float outerSel = max( + step(v_halfDimensions.x, v_outerSize.x), + step(v_halfDimensions.y, v_outerSize.y) + ); + float shadowAlpha = shadowBox( + boxUv + v_outerBorderUv * outerSel - u_shadow.xy, + mix(v_halfDimensions, v_outerSize, outerSel) + u_shadow.w - edgeWidth, + mix(u_radius, v_outerBorderRadius, outerSel) + u_shadow.z + ); float outerDist = roundedBox(boxUv + v_outerBorderUv, v_outerSize - edgeWidth, v_outerBorderRadius); float innerDist = roundedBox(boxUv + v_innerBorderUv, v_innerSize - edgeWidth, v_innerBorderRadius); + float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); + float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - if(u_borderGap == 0.0) { - float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); - float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - resultColor = mix(resultColor, u_shadowColor, shadowAlpha); - resultColor = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a); - resultColor = mix(resultColor, color, innerAlpha); - gl_FragColor = resultColor * u_alpha; - return; - } - - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); - nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); float borderDist = max(-innerDist, outerDist); float borderAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderDist); - resultColor = mix(resultColor, u_shadowColor, shadowAlpha); - resultColor = mix(resultColor, color, nodeAlpha); - resultColor = mix(resultColor, u_borderColor, borderAlpha * u_borderColor.a); + + // Branchless path select -- Mali 400 serializes uniform branches, so all + // three composites (no gap / gap / zero border) are computed and + // mix()-selected instead of if/else'd. + vec4 shadowBase = u_shadowColor * shadowAlpha; + + vec4 resNoGap = mix(shadowBase, u_borderColor, outerAlpha * u_borderColor.a); + resNoGap = mix(resNoGap, color, innerAlpha); + + vec4 resFill = mix(shadowBase, color, nodeAlpha); + vec4 resGap = mix(resFill, u_borderColor, borderAlpha * u_borderColor.a); + + float hasGap = step(0.0001, abs(u_borderGap)); + vec4 resultColor = mix(resNoGap, resGap, hasGap); + resultColor = mix(resultColor, resFill, borderZero); gl_FragColor = resultColor * u_alpha; } `, diff --git a/src/core/shaders/webgl/RoundedWithShadow.ts b/src/core/shaders/webgl/RoundedWithShadow.ts index 9dd1f89..5ec8c0d 100644 --- a/src/core/shaders/webgl/RoundedWithShadow.ts +++ b/src/core/shaders/webgl/RoundedWithShadow.ts @@ -98,18 +98,23 @@ export const RoundedWithShadow: WebGlShaderType = { varying vec4 v_shadowRadius; varying vec2 v_shadowSmooth; + // Branchless quadrant radius select (r: x TL, y TR, z BR, w BL) -- + // Mali 400-class fragment pipelines serialize any branch, ternaries included. + float quadRadius(vec2 p, vec4 r) { + vec2 stepVal = step(vec2(0.0), p); + return mix(mix(r.x, r.y, stepVal.x), mix(r.w, r.z, stepVal.x), stepVal.y); + } + float roundedBox(vec2 p, vec2 s, vec4 r) { - r.xy = (p.x > 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - vec2 q = abs(p) - s + r.x; - return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - r.x; + float rad = quadRadius(p, r); + vec2 q = abs(p) - s + rad; + return (min(max(q.x, q.y), 0.0) + length(max(q, 0.0))) - rad; } float shadowBox(vec2 p, vec2 s, vec4 r) { - r.xy = (p.x > 0.0) ? r.yz : r.xw; - r.x = (p.y > 0.0) ? r.y : r.x; - vec2 q = abs(p) - s + r.x; - float dist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; + float rad = quadRadius(p, r); + vec2 q = abs(p) - s + rad; + float dist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - rad; return 1.0 - smoothstep(v_shadowSmooth.x, v_shadowSmooth.y, dist); } @@ -119,11 +124,8 @@ export const RoundedWithShadow: WebGlShaderType = { float boxDist = roundedBox(v_boxUv, v_boxSize, u_radius); float roundedAlpha = 1.0 - smoothstep(v_boxSmooth.x, v_boxSmooth.y, boxDist); - vec4 resColor = vec4(0.0); - if (u_shadow_color.a > 0.0) { - float shadowAlpha = shadowBox(v_shadowBox, v_shadowSize, v_shadowRadius); - resColor = u_shadow_color * shadowAlpha; - } + float shadowAlpha = shadowBox(v_shadowBox, v_shadowSize, v_shadowRadius); + vec4 resColor = u_shadow_color * (shadowAlpha * step(0.0001, u_shadow_color.a)); resColor = mix(resColor, color, min(color.a, roundedAlpha)); gl_FragColor = resColor * u_alpha; diff --git a/src/core/shaders/webgl/Shadow.ts b/src/core/shaders/webgl/Shadow.ts index 3163fda..6e9bae9 100644 --- a/src/core/shaders/webgl/Shadow.ts +++ b/src/core/shaders/webgl/Shadow.ts @@ -106,11 +106,10 @@ export const Shadow: WebGlShaderType = { float boxDist = box(v_boxUv, v_boxSize); float boxAlpha = 1.0 - smoothstep(v_boxSmooth.x, v_boxSmooth.y, boxDist); - vec4 resColor = vec4(0.0); - if (u_color.a > 0.0) { - float shadowDist = shadowBox(v_shadowBox, v_shadowSize, v_shadowRadius); - resColor = u_color * shadowDist; - } + // Branchless alpha gate -- Mali 400 serializes uniform branches; the + // step() zeroes the shadow term exactly when u_color.a == 0. + float shadowDist = shadowBox(v_shadowBox, v_shadowSize, v_shadowRadius); + vec4 resColor = u_color * (shadowDist * step(0.0001, u_color.a)); resColor = mix(resColor, color, min(color.a, boxAlpha)); gl_FragColor = resColor * u_alpha; diff --git a/visual-regression/certified-snapshots/chromium-ci/shader-border-zero-1.png b/visual-regression/certified-snapshots/chromium-ci/shader-border-zero-1.png new file mode 100644 index 0000000..c0f7347 Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/shader-border-zero-1.png differ