Skip to content

LEGACY_GL_EMULATION: generated fixed-function fragment shaders use mediump — fp16 overflow where mediump is half precision #27948

Open

Description

@TheTomBritton

Version

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:

#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif

The FFP generator should emit the same conditional header instead of unconditional mediump. (That is the fix we ship, via post-processing.)

Activity

  1. juj commented on Oct 14, 2026

    @juj
    Collaborator

    Is the issue here contained to shaders generated from this single line?

    'precision mediump float;',

    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.

  2. TheTomBritton commented on Oct 14, 2026

    @TheTomBritton
    Author

    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 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:

    #ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
    #else
    precision mediump float;
    #endif
    

    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.

  3. juj commented on Oct 14, 2026

    @juj
    Collaborator

    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)

  4. TheTomBritton commented on Oct 23, 2026

    @TheTomBritton
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions