iosglslwebglregl

Inexplicable behavior in WebGL shader on iOS


I'm seeing some strange behavior that I can't explain in a WebGL shader I've written. This is not an area of expertise for me, so it's entirely possible that I'm misunderstanding something pretty simple, but I'm not sure what.

I have a codepen illustrating the strange behavior here https://codepen.io/bjvanminnen/pen/XqMpvL.

void main () {
    vec3 color = vec3(0.);

    vec2 loc1 = vec2(0.5, 0.) / u_res;
    vec2 loc2 = vec2(1.5, 0.) / u_res;
    float val1 = texture2D(u_tex, loc1).r * 255.;
    float val2 = texture2D(u_tex, loc1).r * 255.;
    color.r = val1 == val2 ? 1. : 0.;
    // commenting/uncommenting this line somehow affects the b value
    // on iOS, when uncommented we end up with (53, 0, 255, 255)
    // when commented we end up with (255, 0, 0, 255)
    // I can think of no reason why the below line should affect color.b
    color.r = floor(val1) / 255.;

    color.b = val1 == 53. ? 1. : 0.;      
    gl_FragColor = vec4(color, 1.);
}

A summary of what's happening

My expectations: When the second color.r line is commented out, I would expect a result of (53, 0, 255, 255). This IS what I see on my desktop, but on my iOS device I see (53, 0, 0, 255). It appears to be the case that val1 ends up being slightly larger than 53 on iOS. I'm assuming this is just floating point weirdness.

What's super strange and I can't understand is that when I comment out the second color.r line, I get (255, 0, 255, 255) on my desktop - which makes sense - but (255, 0, 0, 255) on my iOS device.

In other words the presence/non-presence of the line color.r = floor(val1) / 255.; is somehow changing the result of color.b.

Have I encountered some strange iOS bug, or am I misunderstanding something here?


Solution

  • Looking in the GLSL ES 1.0 spec there's a bunch of sections on invariance starting at section 4.6

    The important part for your case is probably 4.6.2

    4.6.2 Invariance Within Shaders

    When a value is stored in a variable, it is usually assumed it will remain constant unless explicitly changed. However, during the process of optimization, it is possible that the compiler may choose to recompute a value rather than store it in a register. Since the precision of operations is not completely specified (e.g. a low precision operation may be done at medium or high precision), it would be possible for the recomputed value to be different from the original value.

    Values are allowed to be variant within a shader. To prevent this, the invariant qualifier or invariant pragma must be used.

    Within a shader, there is no invariance for values generated by different non-constant expressions, even if those expressions are identical.

    Example 1:

    precision mediump;
    vec4 col;
    vec2 a = ...
    ...
    col = texture2D(tex, a); // a has a value a1
    ...
    col = texture2D(tex, a); // a has a value a2 where possibly a1 ≠ a2
    

    To enforce invariance in this example use:

    #pragma STDGL invariant(all)
    

    Example 2:

    vec2 m = ...;
    vec2 n = ...;
    vec2 a = m + n;
    vec2 b = m + n; // a and b are not guaranteed to be exactly equal
    

    There is no mechanism to enforce invariance between a and b.

    You're comparing a float to an exact value which seems dangerous in pretty much any language. If I change this line

    color.b = val1 == 53. ? 1. : 0.; 
    

    to

    color.b = val1 > 52.9 && val1 < 53.1 ? 1. : 0.; 
    

    Then it works as expected for me. Also, if I write out val1 with

    color.g = val1 / 153.;
    

    I don't see it change. It's 88 whether or not that line is commented out even though your test fails. That suggests to me it's not 53.0 in one case. It's 53.000000000001 or 52.9999999999999 etc...