Violating GLSL ES array indexing requirements

My game uses OpenGL ES 2.0 on Android for compatibility purposes, because the data Google provides on Android device OpenGL ES compatibility is practically useless, however it does suggest that over 11% of all Android devices (in the world?) only support OpenGL ES 2.0, and the situation with Vulkan is even worse with 45% not supporting it at all! So rather than worry about compatibility, I decided to limit myself to GLES2.

Unfortunately, GLES2 doesn't support hardware instancing, which is a pretty important feature for achieving good performance. My solution was to implement my own custom version of instancing by maxing out all of the available space for uniforms to store instancing data (positions, textures, etc). This actually works pretty well since some GLES implementations provide a lot of uniform space, but it has introduced me to a weird part of the GLSL ES spec that I didn't know about before.

Apparently, indexing an array with anything that isn't a constant-index-expression is considered undefined behavior. These are essentially either constant expressions, loop indices, or a combination of both.

OpenGL ES Shading Language Version 1.00 Revision 17 (12 May, 2009)

This is a problem for me because in addition to instancing, my renderer performs automatic batching of draw calls, including batching instances that don't share the same texture. This helps minimize draw calls, but it means that a single batch may need multiple textures to be bound at the same time. When the fragment stage comes around, I need to be able to know which is the correct texture to sample, and I do that by passing a texture index from the vertex shader.

In other words, this is roughly what my fragment shader looks like:

1
2
3
4
5
6
7
8
varying lowp vec3 fUVT;

uniform sampler2D texSet[TextureUnitCount];

void main(){
    int tId = int(ceil(fUVT.z));
    gl_FragColor = texture2D(texSet[tId], fUVT.xy);
}

The Z component of the input fUVT contains the texture ID. I use it to access the texture I want via simple array indexing: texSet[tId]

But of course, fUVT is not a constant-index-expression! so this is technically undefined behavior. What are the actual consequences of this? Usually, nothing. On all mobile devices I've tested it, it has worked perfectly. The only device I encountered an issue with is my trusty old Thinkpad X220T.

Usually, undefined behavior is a boring thing: a subtle bug, an anti-climactic crash. But in this particular case, we can actually see it!

Here is a screenshot of my game on my desktop:

good turtle

And here is that same exact scene on my X220T:

bad turtle

Besides the obviously broken textures, the game, animations, etc all work fine with no crashing.

There are a couple of ways I can work around this, but here is one really weird solution that I found works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
varying lowp vec3 fUVT;

uniform sampler2D texSet[TextureUnitCount];

void main(){
    int tId = int(ceil(fUVT.z));

    for(int i = 0; i < TextureUnitCount; i++){
        if(i == tId){ //WTF?!
            gl_FragColor = texture2D(texSet[i], fUVT.xy);
            break;
        }
    }
}

It's weird, it's horribly inefficient, and it's disastrously slow on some devices I've tested it on, but it works because loop variables are technically considered constant-index-expressions!