You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
LEGACY_GL_EMULATION: generated fixed-function fragment shaders use mediump — fp16 overflow where mediump is half precision #27948
emcc 6.0.12; verified present on main (src/lib/libglemu.js, the fixed-function fsSource assembly). Flags: -sLEGACY_GL_EMULATION.
Summary
The generated fixed-function fragment shaders begin with a plain
'precision mediump float;',
On GPUs where mediump is genuinely half precision in fragment shaders (Apple GPUs via Metal/ANGLE, many mobile GPUs), fp16 has a max finite value of 65504 and an 11-bit mantissa. GL 1.x content routinely pushes values beyond that through the fragment stage — texel-space or homogeneous texture coordinates being the common case. In our port, large polygons rendered solid black on macOS Chrome because interpolated texture coordinates overflowed fp16; the same build was pixel-perfect on desktop GPUs where mediump maps to fp32 (and on SwiftShader), which makes the failure easy to miss in CI.
Suggested fix
GLEmulation.init already patches user shaders with a guarded header:
If you change that line unconditionally to precision highp float;, does the issue go away?
If so, then if you (your agent?) could do a bit of OpenGL 1.x specification lawyering and see if the spec wording text requires that GL 1.x fixed function should always operate in fp32? If so, I recommend that we just change that line unconditionally to highp.
Contained to that line: our production builds post-process the generated code to exactly precision highp float; in the emitted fragment shaders — nothing else changed in this area — and the fp16 artifacts on Apple GPUs are gone. That has been our shipped workaround since filing this.
Spec: GL 1.5 §2.1.1 (Floating-Point Computation) requires that "the maximum representable magnitude of a floating-point number used to represent positional or normal coordinates must be at least 2^32; the maximum representable magnitude for colors or texture coordinates must be at least 2^10. The maximum representable magnitude for all other floating-point values must be at least 2^32", and the same section requires individual results of floating-point operations to be accurate to about 1 part in 10^5. GLSL ES mediump minimums (relative precision 2^-10, range ±2^14) miss both bounds, so a minimal mediump implementation — fp16, which is what Metal uses — cannot implement GL 1.x fixed function to spec. highp (2^-16, ±2^62) can. The failure we hit is exactly the "all other floating-point values" case: texture coordinates that have been through a texture matrix carry a projective q, and the s·q/t·q intermediates overflow fp16's 2^16 max magnitude long before the 2^32 the spec asks for.
Precedent: ANGLE's GLES1 fixed-function emulation declares precision highp float; in both its vertex and fragment shader headers.
One nit on unconditionally: in a WebGL1/ES2 context, highp support in fragment shaders is technically optional (GL_FRAGMENT_PRECISION_HIGH), and libglemu already uses the guarded form for user shaders in its ensurePrecision wrapper around glShaderSource. So the drop-in safest version of the same change is:
which resolves to highp everywhere WebGL2 runs (ES3 requires fragment highp) and on effectively all WebGL1 hardware anyone still targets. Happy to send the one-liner PR in either form — your call which.
If you can wrap a PR that adds the ifdeffed form, that would work well. Also would be good to add a test, even if that test might not fire on regular desktop browsers. (maybe at some point we might get to run the browser suite on mobile devices)
PR for the guarded form: #28062. It emits the same #ifdef GL_FRAGMENT_PRECISION_HIGH header GLEmulation already adds to user shaders, and adds test_gl_ffp_texcoord_precision. That test uses texture coordinates from 1000 to 1001 (where fp16 can only step by 0.5) and checks them texel by texel. As you expected, it won't fire on desktop: on Chrome/ANGLE Metal (M2 Pro) and SwiftShader it passes with and without the change.
I also need to correct my original report. Re-testing, Chrome on my M2 Pro runs mediump at full precision (70000.375 survives a mediump varying), so the black polygons I saw probably weren't fp16 overflow. The case for the change is the spec and GPUs where mediump really is fp16. The other LEGACY_GL_EMULATION browser tests pass locally.
Activity
juj commentedon Oct 14, 2026
Is the issue here contained to shaders generated from this single line?
emscripten/src/lib/libglemu.js
Line 2383 in 77b5f2f
If you change that line unconditionally to
precision highp float;, does the issue go away?If so, then if you (your agent?) could do a bit of OpenGL 1.x specification lawyering and see if the spec wording text requires that GL 1.x fixed function should always operate in fp32? If so, I recommend that we just change that line unconditionally to highp.
TheTomBritton commentedon Oct 14, 2026
Yes on both counts.
Contained to that line: our production builds post-process the generated code to exactly
precision highp float;in the emitted fragment shaders — nothing else changed in this area — and the fp16 artifacts on Apple GPUs are gone. That has been our shipped workaround since filing this.Spec: GL 1.5 §2.1.1 (Floating-Point Computation) requires that "the maximum representable magnitude of a floating-point number used to represent positional or normal coordinates must be at least 2^32; the maximum representable magnitude for colors or texture coordinates must be at least 2^10. The maximum representable magnitude for all other floating-point values must be at least 2^32", and the same section requires individual results of floating-point operations to be accurate to about 1 part in 10^5. GLSL ES
mediumpminimums (relative precision 2^-10, range ±2^14) miss both bounds, so a minimalmediumpimplementation — fp16, which is what Metal uses — cannot implement GL 1.x fixed function to spec.highp(2^-16, ±2^62) can. The failure we hit is exactly the "all other floating-point values" case: texture coordinates that have been through a texture matrix carry a projective q, and the s·q/t·q intermediates overflow fp16's 2^16 max magnitude long before the 2^32 the spec asks for.Precedent: ANGLE's GLES1 fixed-function emulation declares
precision highp float;in both its vertex and fragment shader headers.One nit on unconditionally: in a WebGL1/ES2 context, highp support in fragment shaders is technically optional (
GL_FRAGMENT_PRECISION_HIGH), and libglemu already uses the guarded form for user shaders in itsensurePrecisionwrapper aroundglShaderSource. So the drop-in safest version of the same change is:which resolves to highp everywhere WebGL2 runs (ES3 requires fragment highp) and on effectively all WebGL1 hardware anyone still targets. Happy to send the one-liner PR in either form — your call which.
juj commentedon Oct 14, 2026
Thanks for the clarifications.
If you can wrap a PR that adds the ifdeffed form, that would work well. Also would be good to add a test, even if that test might not fire on regular desktop browsers. (maybe at some point we might get to run the browser suite on mobile devices)
TheTomBritton commentedon Oct 23, 2026
PR for the guarded form: #28062. It emits the same
#ifdef GL_FRAGMENT_PRECISION_HIGHheader GLEmulation already adds to user shaders, and addstest_gl_ffp_texcoord_precision. That test uses texture coordinates from 1000 to 1001 (where fp16 can only step by 0.5) and checks them texel by texel. As you expected, it won't fire on desktop: on Chrome/ANGLE Metal (M2 Pro) and SwiftShader it passes with and without the change.I also need to correct my original report. Re-testing, Chrome on my M2 Pro runs mediump at full precision (70000.375 survives a mediump varying), so the black polygons I saw probably weren't fp16 overflow. The case for the change is the spec and GPUs where mediump really is fp16. The other LEGACY_GL_EMULATION browser tests pass locally.