Compatibility issues in Shadertoy / webGLSL

[ New 28/02/2017 :  WebGL2.0 compatibility issues. See last section. ]

Sometimes, somebody else shader looks strange, or blank, or broken, on your machine.
Conversely, if your shader works on your machine, it’s not a proof that it works elsewhere.

Usual suspects:

  • Noisy image:  – Variable not initialized.
  • Blank image:  – Negative parameter for log, sqrt, pow.
                               – clamp(min,max,v) instead of clamp(v,min,max).
                               – smoothstep(v, v0,v1) instead of smoothstep(v0,v1, v)
                               – out parameter used instead of inout.
                               – mod(x,0.) or atan(0.,0.)
                               – MIPmap on grey image on firefox.
  • Compilation error (assuming it was ok on author’s system):
                               – Bug in your compiler (e.g. const struct)
                               – too permissive author’s compiler (global initialized with not const expression)
                               – Your system has less than 32bits and a const value is more.
                               – Shader too costly to compile on your system.
  • Browser or system freeze (or crash):
                               – Shader way too costly for your system.

More details are given below.

Fast medic-test: include this Common tab (or variant) . If it does fix the problem,  progressively remove parts to find the guilty command.

Classical Reasons:

There is a full stack of subsystems digesting your shadertoy GLSL ES source before it reaches the GPU: the shadertoy API, the web browser, the OS, the OpenGL handler, the 3D driver, the GPU driver, its GLSL/HLSL compiler, the GPU.  Some driver embedded compilers have different behaviors and different bugs, but it’s even worse. E.g., Windows use either nativeOpenGL or Angle which translate your GLSL source to HLSL (!)  (to switch to OpenGL: Firefox → about:config → webgl.disable-angle = true, webgl.force-enabled = truewhile linux and macOS use (better) native OpenGL. On Windows, Chrome and firefox don’t even rely on the same version of DirectX3D. Some high end tablets like the Ipad use crude GLSL implementation. Browsers override some format flags for textures and buffers. etc,etc,etc.
Any of these can cause issues, so from here we will call this stack “your system”.

  • Some systems implicitly initialize variables and some others not (in the spirit of the spec).
    => Always initialize variables. Comprising “out” parameters.
    • Note that on iOS and old Mac operations like v -= v  won’t be sufficient as initialisation to 0 since sometime the undetermined initial value is not even a valid number ( and NaN – NaN is not zero ).
  • Some systems implicitly extend the validity of invalid operations like log, sqrt or pow of negative, mod(x,0.), atan(0.,0.), and not the others (following the specs).
    => Never use negatives for log, sqrt, pow. (prefer x*x if you want to square).
           Don’t mod on 0.
           Don’t ask for the angle of a null vector. => atan(y,x+1e-15) 
           clamp API is (v,min,max).
    smoothstep API is (v0,v1, v).
  • Some systems loosely implement the spec, making inout for out.
    => Be strict on code requirement; do initialize out parameters, do use inout if incoming value is expected.
  • artifact in texture: often due to MIPmap issues. By default, texture() use MIPmaps, that use space derivatives to find the LOD. But space derivatives require the same register to bet set in a 2×2 neighborhood, which is not guaranteed in a conditionnal statement or in a loop (plus low-end hardwares don’t have space derivatives at all). The behavior is variable when it is not the case, plus Windows/Angle vs OpenGL compilers may trigger a complex and costly strategy to emulate the missing values (but not the other).
  • Some system are more picky than others. For instance grey-level textures implemented as “luminance” are not “renderable” according to the spec, thus not compatible with some operations like MIPmap. Apparently only Firefox is so picky, resulting in blank values.
    => Switch to a colored texture or replace MIPmap flag by Linear.
  • WebGL2 is a lot more picky. See WebGL 2.0 vs WebGL 1.0

There are genuine bugs and hard limitations of some systems:

  • Shadertoy buffers are supposed to be 16bits floats, but some browsers (e.g. chrome) override this for 32bits floats. A shader writer on chrome will not see overflows.
  • Complex expressions including redefinition of variables ( e.g., x = (x=y)*x ) may not be evaluate in the same order on some systems (typically, after translation to HLSL by Windows Angle; API drivers can also have problems), or can even be bugged (O += x -O on some old systems).  (OpenGL is wrongly evaluating first all the subdefinitions while here Angle is right.)
    => for wide compatibility, avoid reusing the same variable redefinition within a single expression – which span includes comma-separated expressions.
  • cond ? op1, op2 : op3, op4  is not always compiled the same: op1,op2 is ok, but add parenthesis to (op3, op4) or it may be compiled as ( cond ? op1, op2 : op3 ), op4  on some systems. It might even depends on further context.
  • Low-end devices or old systems sometimes not implement the full IEEE math such as NaN (indeed, no system fully implement them), or less than 32bits for floats and ints.
    => you may won’t afford a bigger device, but at least be sure to do the updates (drivers, etc).
  • GLSL unrolls most loops and all function calls, so your shader can be extremely longer than you think. This can crash the compiler, timeout it, or request more resource than your GPU can afford.
    => Try to guess the consequences of your coding style.
            Do loop for selecting then treat after, rather than treat or call a function inside loops.
             Fear long nested loops (including the ones in called functions).
             User side: try replacing the loop end value by a shorter value.
  • Some expressions are solved at compilation time rather than at run time (e.g. #define and const expressions), and thus can have different precision or treatment of exceptions. E.g. on some systems the full IEEE is not obeyed at compilation time while it is at run-time: float x=0., y=x/x, z=1./x often behave differently with const float (or using 0. directly).
    The optimizer can also solve or partly solve some more. But optimizers vary a lot with drivers, versions, etc.
  • Compilers are still full of bugs:
    • E.g. complex types like structs, mat, vec might do wrong with const qualifier, or in cond?v1:v2 statements, or in loops without {}.
    • Ternary operator used in vec or switch might give unexpected result or not compile.
    • On windows, some operations may make floor/ceil ignored !
    • Redefining locally a global variable already used in the current function crash the compiler or even the driver on linux.
    • A variable having the name of a function might be not accepted as well on some compilers.
  • => Do the updates.
          – Suppress suspect const qualifier (optimizer is smart enough, anyway)
    – don’t reuse function name for variables
    – don’t reuse global names when both the local and global variables are used in the same block
          – Protect suspect blocks by {} or (). Try replacing by if then else.
  • Some bugs occur at unrolling of long loops or long shaders (e.g. the last instruction of a loop is not always executed, or implicit initialization not always done).
    => Do the updates.
           Rearrange the suspect loop. E.g., move a conditional break as earlier statement.
           Initialize all variables.
            (See also section about long shaders.)
  • The number of simultaneous key accounted in Shadertoy keyboard texture depends on the keys, and probably on the OS. (Web events are just a compatibility mess).
  • There seems to be some GPU-specific and OSX-specific bugs.

There are some pure GLSL bugs (or spec sloppyness)… consistent or not through OSs

Note that often, these correspond to “undefined” in the GLSL spec, even if it is in C/C++ , and some compilers do the reasonable thing. But others “won’t fix” because it’s not in spec.

  • clamp(NaN,0.,1.) should be NaN. It is not. And it is 0 on linux (GLSL), but 1 on windows (Angle).
  • same for smoothstep(NaN) (since it uses clamp)
  • sqrt(v) with v=-1 is NaN… but if v is a const or a #define. Then, it is 0.
  • (-3) % 4 gives 1 on OpenGL and -3 on Windows Angle if -3 is in a non-const variable (otherwise 0).
    While mod(-3.,4.) does gives 1. on all systems (test here).
    [ odd, indeed: on OpengL  (-a)%b is off by 5 if non-const, and always 0 if const. ]
    → Special case trick: replace x%2 with x&1 .
    → a portable integer modulo that works on negative here.
    Usually only the first parameter can be negative.
    #define imod(a,b) ( a >= 0 ? (a) % (b) : -(a)%(b)==0 ? 0 : b – ( -(a) % (b) ) )
  • uint or uvec(negative) is correct for ints and const floats while wrongly gives 0 for unconst floats.
    Patch:   #define unsigned(v)   ( (v) >= 0. ? uint(v) : -1U-uint(-(v)) )   or:  uint(int(floor(v)))
  • int(x) vs int(floor(x)) on negatives can be equal or OpenGL and different on Windows/Angle.
  • Out of bounds array indexes are clamped on OpenGL, but can cause strange non-local side effects on Windows/Angle. Indeed it can even prevent the shader to display anything while eating a lot of CPU/GPU resources (and possible crash).
    → if not totally sure, better to double any array[] with a protected access function #define array(i) array[clamp(i,0,array.length()-1)]  .
    ( a reminder that a simple %array.length() wouldn’t do the job on negatives on Windows ).
  • round(x) is often implemented as roundEven(x) rather than floor(x+.5).
  • Windows Angle allows to note name unused function parameters, but it won’t compile elsewhere.

Attention: Shadertoy masks the warnings if the compilation is ok.  Maybe it could be good practice to once insert a syntax error just to see the warnings, then inserted in the code and indistinguishable from full errors. Example here : sqrt(-1) were indeed warned as invalid at compilation time.

Classical float “bugs”: (not specific to GLSL)

  • x/x might not be exactly 1., even for int-in-floats (integers up to 16,777,216 are exactly represented by IEEE floats on 32bits. + – * will be exact… but not the division). This is due to the fact that compilers generally replace division by multiplication with the inverse.
    A consequence is that fract(x/x) might be ~1 instead of 0 (about 10% of times)
  • mod on int-in-floats has some bugs (due to the division as above). e.g. mod(33.,33.) might be 33, some for about 10% of values. 😦 (Note that you can’t verify by testing this on const since it would be resolved at compiler time, not run time).
    => If you really aim at integer operations, emulate a%b with a-a/b*b [webGL2 now have full integer arithmetic: just do it !]
  • A reminder than floats have limited precision and span… and worse for 16bits floats.
    A goodies and a trap are denormalized floats: IEEE provides an extension of the range, at the price of collapsing precision.
    Note that without this extension, 32 floats overflow for exp(83.) (same for sinh,cosh,tanh), giving NaN and thus black in Shadertoy.
  • -0 is not totally equal to +0. If you test equality or order directly you’ll find as expected, but a surprise comes when comparing there inverse. Indeed it’s a IEEE feature. If some rare case, this unforgotten sign can create bugs (or save the day in geometry).
  • IEEE treatment of NaN and INF can be surprising, despite logical. In shadertoy NaN is displayed as contaminating black, while +INF is white and -INF is black.
    But their implementation can also be bugged in some cases, or not implemented at all in low-end GPUs.
  • ints op behave strangely on negatives. Rule of thumb: often doing sign(x)*op(abs(x)). It might be dangerous in conversions, e.g. int(-1.2) = -1 . Sometime what you really want is int(floor(x)) .

Extensions:

Not all extensions are available on all browser (check here). You can check that an extension is there using #ifdef GL_EXT_shader_texture_lod (for instance).
Alas, all extensions now part of WebGL2 core won’t have the old define set: the extension bag is totally different. You can test webGL version with __VERSION__ ( example here .)

Some more subtle issues:

  • Some browsers seem to decompress R,G,B texture channels on slightly different ways.
  • Shadertoy don’t currently use sRGB textures. You have to ungamma – regamma them yourself, but it means that interpolation and MIPmap are slightly biased (but most shader writers seems to don’t know gamma issues at all, anyway 🙂 ).
  • Sound buffering seems to be done on very different ways depending of the system. No problem with sound playing, but issues start when you inspect inside (e.g. time sync and precise buffering range).
  • A shadertoy can be displayed at very different resolutions, depending on your screen size, window size, and various other factors. The aspect ratio can varies, the size might not even be even. So a special configuration causing a glitch might occurs just at your personal display size.
  • In particular, derivative and MIPmap level evaluation are done within 2×2 pixels blocks. So a very slight shift might make a discontinuity invisible or causing a glitch. This typically occurs when you force fract(uv) or use angles as texture coordinates. Also with derivative of variables that might not be set in the neighbor pixel.
  • The shader looks a lot darker or more saturated for you (or for all others).
    => Ever heard about gamma correction ? 🙂
    In particular, is your monitor in “multimedia mode”,
    or didn’t you played with the contrast or gamma curve (on monitor or on GPU preferences window) ?
  • Textures, sound, video are loaded asynchronously and can sometime be a few frame late.
    =>  If you precompute data in a Buffer, keep redoing it for a few dozen frames.
            e.g.,  if ( iFrame < 30 ) { init } .

Testing OpenGL vs Angle vs D3D version on Windows

On Windows the browser (at least Chrome and Firefox) can use either true GLSL-ES or transpiling to HLSL via the Angle library provided by Google.
You can switch: (more and updates here )

  • firefox:   URL:  about:config   ; search “angle”; click on webgl.disable-angle to switch (immediate effect)
  • chrome: relaunch with chrome.exe --use-gl=desktop (or create an alias).
    [edit]: nowadays, rather do chrome.exe --use-angle=gles   or   =gl

Moreover, Angle use two different versions of D3D on chrome vs firefox, which can thus shows up different bugs and behaviors: in case of doubt, try both.

Remote testing your shadertoy on different browser and OS:

  • browserling.com   ( 3′ free trial without registration on all Windows and Android. Unlimited (?) on Windows7 )
  • Free Windows/Linux/MacOS online Emulator: onworks.net, or as Chrome plugin
  • Geekprank XP simulator  :-p
  • There are many others with free trial after registration stage, e.g. testingbot
  • [ no WebGL ! ] browsershots.org  ( free, all OS/browsers. Unselect all (bottom) and pick only what you need ! )

WebGL2.0 compatibility issues

  • Are you sure your browser is webGL2.0 ?  -> Test here.
  • on Windows, % and %= works even on floats, while the spec specifies it should only be valid for ints. → On floats, only use mod.
  • Continuation to next line with \ (e.g. for macros) :
    not accepted initially by Firefox.
    Not accepted by Firefox ESR (new bug ?)
  • Return in divergent branches:
    webGL2 GLSL-ES compilers are new, thus come with new bugs. An old issue came back: when one branch of parallel evaluation has a return while others don’t. The return can then be missed, possibly crashing the compiler/driver via infinite loop, or “just” cause wrong or slow results.
  • If: nVidia (linux+windows) ignores diverging returns in if. Test here.
  • Switch: Windows ignore diverging returns inside switch. Test here , here.
  • Switch: linux refuses unreachable statements, typically: return; break;
  • texture() in non-unrollable loop:
    Windows/Angle tries to do something horrible to guess MIPmap level. In case of non-unrollable loop this generates so much extra code that it can easily overwhelm the compiler. But OpenGL might just get MIPmap totally wrong if pixels aren’t locally coherent, anyway.
    -> use texelFetch or textureLOD instead. Test here.
  • Declarations in for:
    Windows bug if a loop counter is declared in another loop. (for(int i,j;..) for (;j<N;j++) )

Link to all glsl bug related shadertoys.

See Also

Morimea’s list of bugs and strange behaviors (with live experiments).

6 thoughts on “Compatibility issues in Shadertoy / webGLSL

  1. I have a different compatibility on an AMD FX 8350, and many many others have the same bug: thumbnail freezing all of chrome.exe!!! I haven’t used shadertoy much to put in any code at all because it just crashes constantly. someone should tell IQ… here is a page with people having all the same bug. Again, I have had this bug on shadertoy for 2-3 years. http://www.pouet.net/topic.php?which=10194&page=1

    Like

    • The whole discussion in your link is very old ( Jan 2015 ). A reminder that Shadertoy is a light demo-system firstly targeted to reasonable systems (including not to bugged drivers and browsers). Still, since 2017 you can select “show just image thumbnail instead” in your profile preferences, which solve the issue at browsing. You then just have to be sure to be logged before browsing, or to bookmark a safe page (e.g., your profile) to be sure to don’t crash before the login prompt. 😉

      Like

  2. The command-line flag for launching chrome on windows using OpenGL does not work for me, but it’s close.

    Does not work:
    /Chrome.exe –use-angle=gl

    Works:
    /Chrome.exe –use-angle=OpenGL

    Like

    • After testing:
      No, it’s wrong: these are 2 different things, among more.
      use-angle=gl : is the way ( and require OpenGL driver & lib be installed, I presume ).
      use-angle=OpenGL and warp : use software emulator ( slow, possibly not complete )
      use-angle=gles : also the way. use OpenGL ES 3.2 rather than OpenGL 4.5.0 for option gl
      use-angle=d3d9, use-angle=d3d11 : use transpilation to HLSL, then version 9 or 11 of the lib D3D.

      Mhhh, right now on my spare Windows =gl gives black windows, while =gles works.

      Like

      • I wonder if it uses different flags on different platforms ??
        I could find no documentation for the “–use-angle” flag.
        I inferred the argument here by going to chrome://flags, searching for ‘angle’,
        noting the “#use-angle” tag (which links to chrome://flags/#use-angle ),
        and looking at the available options in the drop-down:
        * Default
        * OpenGL
        * D3D11
        * D3D9
        * D3D11on12

        Like

Leave a comment