Lindsay Edwards

Your language is your worst device's subset

On this page

On the same little mobile game, I opened an old dither shader I had written and recoiled at how ugly it was. A Bayer matrix declared as a constant, and then, instead of indexing it like a sane person, a sixteen-branch if/else ladder over integer coordinates. A sibling shader was worse: it rebuilt its dither value with unrolled bit arithmetic rather than a table lookup.

My first instinct was that past me had been sloppy and I should clean it up. So I did. I replaced the ladder with the obvious one-liner.

float threshold = bayer[y][x];

It ran perfectly on my machine. Then I exported to the mobile renderer and the shader would not compile.

The verbosity was load-bearing#

I am a backend engineer by trade, so my mental model of a program is “the code the language spec allows”. On a desktop GPU that model held. Dynamic indexing into an array or matrix inside a fragment shader, where the index comes from a varying rather than a constant, worked fine.

The mobile-class renderer flatly refuses it. Non-constant indexing into matrices and arrays in a fragment shader is not something it will do. Desktop would let me. The phone would not.

So the ugly if/else ladder was not past me being careless. It was past me having already hit this wall and worked around it, then apparently forgetting the whole episode. The sixteen branches turn a dynamic lookup into sixteen constant lookups, one per branch, which the mobile compiler is happy with. The unrolled bit arithmetic in the sibling shader did the same job by never touching an array at all.

I had cleaned up code whose ugliness was the entire point.

You are writing the intersection, not the spec#

Here is the thing that reframed it for me. I thought I was writing GLSL. I was not. I was writing the subset of GLSL that every one of my target devices agreed to run. And that subset is defined by the weakest device on the list, not by the spec and definitely not by my dev machine.

Your effective language is the lowest common denominator your worst target accepts. Everything your best hardware allows on top of that is a trap with a delay on it.

The delay is what makes it nasty. Everything works while you develop, because you develop on the capable machine. The gap only shows up at export, or worse, on a device you do not own, in a review you cannot reproduce. In product terms the game looks flawless on the machine you test on and is broken for exactly the budget-phone players you were building it for. You end up discovering the boundary one painful compile error at a time.

Find the floor first#

The fix, obviously, was to revert my tidy one-liner back to the branchy version and leave a comment explaining why it has to stay ugly. Future me will thank present me, assuming future me reads comments, which the evidence suggests is optional.

The broader habit is one I already knew from backend work but had let slip here. Before you write against a platform, find its floor. The oldest runtime you support. The smallest database. The strictest browser. The cheapest phone. Write to that, not to the machine under your desk.

This bit me precisely because I brought desktop assumptions to a mobile target. But the shape of the mistake is universal. The language you are allowed to speak is the one your worst listener understands. Learn that subset on purpose, up front, or you will keep learning it the hard way at the worst possible moment.

Keep reading