|
| 1 | +Title: WebGL2 Cross Platform Issues |
| 2 | +Description: Things to be aware of when trying to make your WebGL app work everywhere. |
| 3 | +TOC: Cross Platform Issues |
| 4 | + |
| 5 | +I probably comes as no shock that not all WebGL programs work on all devices or |
| 6 | +browser. |
| 7 | + |
| 8 | +Here's a list of most of the issues you might run into off the top of my head |
| 9 | + |
| 10 | +## Performance |
| 11 | + |
| 12 | +A top end GPU probably runs 100x faster than a low-end GPU. The only way around |
| 13 | +that that I know of is to either aim low, or else give the user options like |
| 14 | +most Desktop PC apps do where they can choose performance or fidelity. |
| 15 | + |
| 16 | +## Memory |
| 17 | + |
| 18 | +Similarly a top end GPU might have 12 to 24 gig of ram where as a low end GPU |
| 19 | +probably has less than 1gig. (I'm old so it's amazing to me low end = 1gig since |
| 20 | +I started programming on machines with 16k to 64k of memory 😜) |
| 21 | + |
| 22 | +## Device Limits |
| 23 | + |
| 24 | +WebGL has various minimum supported features but your local device might support |
| 25 | +> than that minimum which means it will fail on other devices that support less. |
| 26 | +
|
| 27 | +Examples include: |
| 28 | + |
| 29 | +* The max texture size allowed |
| 30 | + |
| 31 | + 2048 or 4096 seems to be reasonable limits. At least as of 2020 it looks like |
| 32 | + [99% of devices support 4096 but only 50% support > 4096](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE). |
| 33 | + |
| 34 | + Note: the max texture size is the maximum dimension the GPU can process. It |
| 35 | + doesn't mean that GPU has enough memory for that dimension squared (for a 2D |
| 36 | + texture) or cubed (for a 3D texture). For example some GPUs have a max size of |
| 37 | + 16384. But a 3D texture 16384 on each side would require 16 terabytes of |
| 38 | + memory!!! |
| 39 | + |
| 40 | +* The maximum number of vertex attributes in a single program |
| 41 | + |
| 42 | + In WebGL1 the minimum supported is 8. In WebGL2 it's 16. If you're using more than that |
| 43 | + then your code will fail on a machine with only the minimum |
| 44 | + |
| 45 | +* The maximum number of uniform vectors |
| 46 | + |
| 47 | + These are specified separately for vertex shaders and fragment shaders. |
| 48 | + |
| 49 | + In WebGL1 it's 128 for vertex shaders and 16 for fragment shaders |
| 50 | + In WebGL2 it's 256 for vertex shaders and 224 for fragment shaders |
| 51 | + |
| 52 | + Note that uniforms can be "packed" so the number above is how many `vec4`s |
| 53 | + can be used. Theoretically you could have 4x the number of `float` uniforms. |
| 54 | + but there is an algorithm that fits them in. You can imagine the space as |
| 55 | + an array with 4 columns, one row for each of the maximum uniform vectors above. |
| 56 | + |
| 57 | + ``` |
| 58 | + +-+-+-+-+ |
| 59 | + | | | | | <- one vec4 |
| 60 | + | | | | | | |
| 61 | + | | | | | | |
| 62 | + | | | | | V |
| 63 | + | | | | | max uniform vectors rows |
| 64 | + | | | | | |
| 65 | + | | | | | |
| 66 | + | | | | | |
| 67 | + ... |
| 68 | +
|
| 69 | + ``` |
| 70 | + |
| 71 | + First `vec4`s are allocated with a `mat4` being 4 `vec4`s. Then `vec3`s are |
| 72 | + fit in the space left. Then `vec2`s followed by `float`s. So imagine we had 1 |
| 73 | + `mat4`, 2 `vec3`s, 2 `vec2`s and 3 `float`s |
| 74 | +
|
| 75 | + ``` |
| 76 | + +-+-+-+-+ |
| 77 | + |m|m|m|m| <- the mat4 takes 4 rows |
| 78 | + |m|m|m|m| |
| 79 | + |m|m|m|m| |
| 80 | + |m|m|m|m| |
| 81 | + |3|3|3| | <- the 2 vec3s take 2 rows |
| 82 | + |3|3|3| | |
| 83 | + |2|2|2|2| <- the 2 vec2s can squeeze into 1 row |
| 84 | + |f|f|f| | <- the 3 floats fit in one row |
| 85 | + ... |
| 86 | +
|
| 87 | + ``` |
| 88 | +
|
| 89 | + Further, an array of uniforms is always vertical so for example if the maximum |
| 90 | + allowed uniform vectors is 16 then you can not have a 17 element `float` array |
| 91 | + and in fact if you had a single `vec4` that would take an entire row so there |
| 92 | + are only 15 rows left meaning the largest array you can have would be 15 |
| 93 | + elements. |
| 94 | +
|
| 95 | + My advice though is don't count on perfect packing. Although the spec says the |
| 96 | + algorithm above is required to pass there are too many combinations to test |
| 97 | + that all drivers pass. Just be aware if you're getting close the limit. |
| 98 | +
|
| 99 | + note: varyings and attributes can not be packed. |
| 100 | +
|
| 101 | +* The maximum varying vectors. |
| 102 | +
|
| 103 | + WebGL1 the minimum is 8. WebGL2 it's 16. |
| 104 | +
|
| 105 | + If you use more than your code will not work on a machine with only the minimum. |
| 106 | +
|
| 107 | +* The maximum texture units |
| 108 | +
|
| 109 | + There are 3 values here. |
| 110 | +
|
| 111 | + 1. How many texture units there are |
| 112 | + 2. How many texture units a vertex shader can reference |
| 113 | + 3. How many texture units a fragment shader can reference |
| 114 | +
|
| 115 | + <table class="tabular-data"> |
| 116 | + <thead> |
| 117 | + <tr><th></th><th>WebGL1</th><th>WebGL2</th></tr> |
| 118 | + </thead> |
| 119 | + <tbody> |
| 120 | + <tr><td>min texture units that exist</td><td>8</td><td>32</td></tr> |
| 121 | + <tr><td>min texture units a vertex shader can reference</td><th style="color: red;">0!</td><td>16</td></tr> |
| 122 | + <tr><td>min texture units a fragment shader can reference</td><td>8</td><td>16</td></tr> |
| 123 | + </tbody> |
| 124 | + </table> |
| 125 | +
|
| 126 | + It's important to note the **0** for a vertex shader in WebGL1. Note that that's probably not the end of the world. |
| 127 | + Apparently [~97% of all devices support at least 4](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_TEXTURE_IMAGE_UNITS). |
| 128 | + Still, you might want to check so you can either tell the user that your app is not going to work for them or |
| 129 | + you can fallback to some other shaders. |
| 130 | +
|
| 131 | +There are other limits as well. To look them up you call `gl.getParameter` with |
| 132 | +the following values. |
| 133 | +
|
| 134 | +<div class="webgl_center"> |
| 135 | +<table class="tabular-data"> |
| 136 | + <tbody> |
| 137 | + <tr><td>MAX_TEXTURE_SIZE </td><td>max size of a texture</td></tr> |
| 138 | + <tr><td>MAX_VERTEX_ATTRIBS </td><td>num attribs you can have</td></tr> |
| 139 | + <tr><td>MAX_VERTEX_UNIFORM_VECTORS </td><td>num vec4 uniforms a vertex shader can have</td></tr> |
| 140 | + <tr><td>MAX_VARYING_VECTORS </td><td>num varyings you have</td></tr> |
| 141 | + <tr><td>MAX_COMBINED_TEXTURE_IMAGE_UNITS</td><td>num texture units that exist</td></tr> |
| 142 | + <tr><td>MAX_VERTEX_TEXTURE_IMAGE_UNITS </td><td>num texture units a vertex shader can reference</td></tr> |
| 143 | + <tr><td>MAX_TEXTURE_IMAGE_UNITS </td><td>num texture units a fragment shader can reference</td></tr> |
| 144 | + <tr><td>MAX_FRAGMENT_UNIFORM_VECTORS </td><td>num vec4 uniforms a fragment shader can have</td></tr> |
| 145 | + <tr><td>MAX_CUBE_MAP_TEXTURE_SIZE </td><td>max size of a cubemap</td></tr> |
| 146 | + <tr><td>MAX_RENDERBUFFER_SIZE </td><td>max size of a renderbuffer</td></tr> |
| 147 | + <tr><td>MAX_VIEWPORT_DIMS </td><td>max size of the viewport</td></tr> |
| 148 | + </tbody> |
| 149 | +</table> |
| 150 | +</div> |
| 151 | +
|
| 152 | +That is not the entire list. For example the max point size and max line thickness |
| 153 | +but you should basically assume the max line thickness is 1.0 and that POINTS |
| 154 | +are only useful for simple demos where you don't care about |
| 155 | +[the clipping issues](#points-lines-viewport-scissor-behavior). |
| 156 | +
|
| 157 | +WebGL2 adds several more. A few common ones are |
| 158 | +
|
| 159 | +<div class="webgl_center"> |
| 160 | +<table class="tabular-data"> |
| 161 | + <tbody> |
| 162 | + <tr><td>MAX_3D_TEXTURE_SIZE </td><td>max size of a 3D texture</td></tr> |
| 163 | + <tr><td>MAX_DRAW_BUFFERS </td><td>num color attachments you can have</td></tr> |
| 164 | + <tr><td>MAX_ARRAY_TEXTURE_LAYERS </td><td>max layers in a 2D texture array</td></tr> |
| 165 | + <tr><td>MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS </td><td>num varyings you can output to separate buffers when using transform feedback</td></tr> |
| 166 | + <tr><td>MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS</td><td>num varyings you can output when sending them all to a single buffer</td></tr> |
| 167 | + <tr><td>MAX_COMBINED_UNIFORM_BLOCKS </td><td>num uniform blocks you can use overall</td></tr> |
| 168 | + <tr><td>MAX_VERTEX_UNIFORM_BLOCKS </td><td>num uniform blocks a vertex shader can use</td></tr> |
| 169 | + <tr><td>MAX_FRAGMENT_UNIFORM_BLOCKS </td><td>num uniform blocks a fragment shader can use</td></tr> |
| 170 | + </tbody> |
| 171 | +</table> |
| 172 | +</div> |
| 173 | +
|
| 174 | +## Depth Buffer resolution |
| 175 | +
|
| 176 | +A few really old mobile devices use 16bit depth buffers. Otherwise, AFAICT 99% |
| 177 | +of devices use a 24bit depth buffer so you probably don't have to worry about |
| 178 | +this. |
| 179 | +
|
| 180 | +## readPixels format/type combos |
| 181 | +
|
| 182 | +Only certain format/type combos are guaranteed to work. Other combos are |
| 183 | +optional. This is covered in [this article](webgl-readpixels.html). |
| 184 | +
|
| 185 | +## framebuffer attachment combos |
| 186 | +
|
| 187 | +Framebuffers can have 1 or more attachments of textures and renderbuffers. |
| 188 | +
|
| 189 | +In WebGL1 only 3 combinations of attachments are guaranteed to work. |
| 190 | +
|
| 191 | +1. a single format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` |
| 192 | +2. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a |
| 193 | + format = `DEPTH_COMPONENT` renderbuffer attached as `DEPTH_ATTACHMENT` |
| 194 | +3. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a |
| 195 | + format = `DEPTH_STENCIL` renderbuffer attached as `DEPTH_STENCIL_ATTACHMENT` |
| 196 | +
|
| 197 | +All other combinations are up to the implementation which you check by calling |
| 198 | +`gl.checkFramebufferStatus` and seeing if it returned `FRAMEBUFFER_COMPLETE`. |
| 199 | +
|
| 200 | +WebGL2 guarantees to be able to write to many more formats but still has the |
| 201 | +limit in that **any combination can fail!** Your best bet might be if all the |
| 202 | +color attachments are the same format if you attach more than 1. |
| 203 | +
|
| 204 | +## Extensions |
| 205 | +
|
| 206 | +Many features of WebGL1 and WebGL2 are optional. The entire point of having an |
| 207 | +API called `getExtension` is that it can fail if the extension does not exist |
| 208 | +and so you should be checking for that failure and not blindly assuming it will |
| 209 | +succeed. |
| 210 | +
|
| 211 | +Probably the most common missing extension on WebGL1 and WebGL2 is |
| 212 | +`OES_texture_float_linear` which is the ability to filter a floating point |
| 213 | +texture, meaning the ability to support setting `TEXTURE_MIN_FILTER` and |
| 214 | +`TEXTURE_MAX_FILTER` to anything except `NEAREST`. Many mobile devices do not |
| 215 | +support this. |
| 216 | +
|
| 217 | +In WebGL1 another often missing extension is `WEBGL_draw_buffers` which is the |
| 218 | +ability to attach more than 1 color attachment to a framebuffer is still at |
| 219 | +around 70% for desktop and almost none for smartphones (that seems wrong). |
| 220 | +Basically any device that can run WebGL2 should also support |
| 221 | +`WEBGL_draw_buffers` in WebGL1 but still, it's apparently still an issue. If you |
| 222 | +are needing to render to multiple textures at once it's likely your page needs a |
| 223 | +high end GPU period. Still, you should check if the user device supports it and |
| 224 | +if not provide a friendly explanation. |
| 225 | +
|
| 226 | +For WebGL1 the following 3 extensions seem almost universally supported so while |
| 227 | +you might want to warn the user your page is not going to work if they are |
| 228 | +missing it's likely that user has an extremely old device that wasn't going to |
| 229 | +run your page well anyway. |
| 230 | +
|
| 231 | +They are, `ANGLE_instance_arrays` (the ability to use [instanced drawing](webgl-instanced-drawing.html)), |
| 232 | +`OES_vertex_array_object` (the ability to store all the attribute state in an object so you can swap all |
| 233 | +that state with a single function call. See [this](webgl-attributes.html)), and `OES_element_index_uint` |
| 234 | +(the ability to use `UNSIGNED_INT` 32 bit indices with [`drawElements`](webgl-indexed-vertices.html)). |
| 235 | +
|
| 236 | +## attribute locations |
| 237 | +
|
| 238 | +A semi common bug is not looking up attribute locations. For example you have a vertex shader like |
| 239 | +
|
| 240 | +```glsl |
| 241 | +attribute vec4 position; |
| 242 | +attribute vec2 texcoord; |
| 243 | +
|
| 244 | +uniform mat4 matrix; |
| 245 | +
|
| 246 | +varying vec2 v_texcoord; |
| 247 | +
|
| 248 | +void main() { |
| 249 | + gl_Position = matrix * position; |
| 250 | + v_texcoord = texcoord; |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +Your code assumes that `position` will be attribute 0 and `texcoord` will be |
| 255 | +attribute 1 but that is not guaranteed. So it runs for you but fails for someone |
| 256 | +else. Often this can be a bug in that you didn't do this intentionally but |
| 257 | +through an error in the code things work when the locations are one way but not |
| 258 | +another. |
| 259 | + |
| 260 | +There are 3 solutions. |
| 261 | + |
| 262 | +1. Always look up the locations. |
| 263 | +2. Assign locations by calling `gl.bindAttribLocation` before calling `gl.linkProgram` |
| 264 | +3. WebGL2 only, set the locations in the shader as in |
| 265 | + |
| 266 | + ```glsl |
| 267 | + #version 300 es |
| 268 | + layout(location = 0) vec4 position; |
| 269 | + latout(location = 1) vec2 texcoord; |
| 270 | + ... |
| 271 | + ``` |
| 272 | + |
| 273 | + Solution 2 seems the most [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) where as solution 3 |
| 274 | + seems the most [W.E.T.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions) unless |
| 275 | + you're generating your textures at runtime. |
| 276 | + |
| 277 | +## GLSL undefined behavior |
| 278 | + |
| 279 | +Several GLSL functions have undefined behavior. For example `pow(x, y)` is |
| 280 | +undefined if `x < 0`. There is a longer list at [the bottom of the article on |
| 281 | +spot lighting](webgl-3d-lighting-spot.html). |
| 282 | + |
| 283 | +## Shader precision issues |
| 284 | + |
| 285 | +In 2020 the biggest issue here is if you use `mediump` or `lowp` in your shaders |
| 286 | +then on desktop the GPU will really use `highp` but on mobile they'll actually be |
| 287 | +`mediump` and or `lowp` and so you won't notice any issues when developing on desktop. |
| 288 | + |
| 289 | +See [this article for more details](webgl-precision-issues.html). |
| 290 | + |
| 291 | +## Points, Lines, Viewport, Scissor behavior |
| 292 | + |
| 293 | +`POINTS` and `LINES` in WebGL can have a max size of 1 and in fact for `LINES` |
| 294 | +that is now the most common limit. Further whether points are clipped when their |
| 295 | +center is outside the viewport is implementation defined. See the bottom of |
| 296 | +[this article](webgl-drawing-without-data.html#pointissues). |
| 297 | + |
| 298 | +Similarly, whether or not the viewport clips vertices only or also pixels is |
| 299 | +undefined. The scissor always clips pixels so turn on the scissor test and set |
| 300 | +the scissor size if you set the viewport smaller than the thing you're drawing |
| 301 | +to and you're drawing LINES or POINTS. |
| 302 | + |
0 commit comments