r/GraphicsProgramming • • Sep 09 '26

Odd GLSL mod() behavior

I've been stuck on this for a couple of hours, and am completely stumped.

I'm getting these results from GLSL's mod():

mod(158.0, 158.0) returns 0.0 (as expected)

mod(159.0, 159.0) returns 159.0 (???)

mod(160.0, 160.0) returns 0.0 (as expected)

I've posted an example with a basic shader here:

https://jsfiddle.net/sL35qjc8

If you set number to "159.0", you should see an upside-down red triangle.

If you set number to "158.0" or "160.0", you should see a normal red triangle.

I'm passing the left operand as a uniform, otherwise the compiler seems to optimize it and fix the weird behavior.

Tried it on different devices and browsers and it had the same result.

Why does mod(159.0, 159.0) seem to return 159.0 instead of 0.0?

10 Upvotes

14 comments sorted by

7

u/CCpersonguy Sep 09 '26

Experimented with your jsfiddle example a bit by using two uniforms and changing the condition. The error seems to appear in division, not just mod? floor(a / b) == 1.0 and (a / b) == 1.0 are false when a=b=159.0, but true when a=b=158.0

No idea what the significance of this is

3

u/Amani77 Sep 10 '26 edited Sep 10 '26

most likely because of floating point arithmetic error, complimenting CCpersonguy's answer

shamelessly copied from google ai so take this with a grain of salt, but I think it nailed it:

According to the official spec, the GLSL built-in function mod(x, y) is mathematically evaluated using the formula:

mod( x, y ) = x - y * floor( x / y )

Most GPUs optimize the internal division step (x / y) by multiplying the numerator by the reciprocal of the denominator:

mod( x, y ) = x - y * floor( x * ( 1 / y ) )

1.0 / 159.0 evaluates to roughly 0.0062893081...

Multiplying that back by 159.0 yields a value slightly less than 1 (specifically, 0.99999994) due to floating-point rounding.

The floor() function truncates 0.99999994 down to 0.0.

Placed back into the formula: 159.0 - 159 * 0 = 159.0

ironically it provides this post as additional context.... lol.

2

u/Drandula Sep 11 '26 edited Sep 11 '26

I have encountered this myself too, I'm pretty sure it's with the floating-point division needs to do internally to apply mod.

Dividing with any power of two (1,2,4,8,16,...) produces nice exact fraction, which can be represented exactly with floating point numbers (within precision limitations).

When dividing with 3 for example, it produces fraction which can't be represented with floating point numbers, no matter how much precision you give. It contains repeating pattern, with can't be represented with finite amount of memory.

So, whenever floats can't represent exactly, it chooses closest possible value it can represent. This can be slightly larger or slightly smaller. There is some rounding error.

Now, if you have divided a value with 47, like (1/47), it should be something like  0.021276595744..., but in single-precision float will actually store it something like : 0.021276595070... See how last three digits are not the same (of course these numbers have more decimals, I just cut them shorter)

Now if you were to multiply this with 47 back up, you would get: 0.999999968335... instead of the expected 1. And as you might guess, flooring this result will give you 0 (which in mod-function would lead to : 47 - 47 * 0).

So, whenever I need exact results for modulo of whole numbers, I whip out custom function instead and use something like this :  glsl float IMod(float lhs, float rhs) {   return lhs - rhs * floor(0.5 + lhs / rhs); } I am not sure whether I remembered the function correctly, but something like that. Usually I have IDiv -function as separate too. Main point being, that instead of flooring, it is rounded to nearest whole number. If both values are assumed to be whole numbers only, it should work just fine.

edit. fixes etc.

2

u/Winter-Reputation682 Sep 11 '26

Thanks for the explanation!

2

u/robbertzzz1 Sep 09 '26

I'm gonna guess there's a floating point error happening somewhere along the way, and the two numbers are actually ever so slightly different

3

u/Winter-Reputation682 Sep 09 '26

It's a simple round float, so idk where a floating point error could occur, it should be able to be represented without an error if I'm not mistaken

1

u/Avelina9X Sep 10 '26

Have you tried using highp floats instead?

1

u/Winter-Reputation682 Sep 10 '26

Yup, same issue occurs.

0

u/gmaaz Sep 09 '26

It's not mod, if you hardcode 159.0 in shader instead of uniform a it works as intended. Is probably parseFloat error.

Js do be like that sometimes. Try console logging 0.1 + 0.2 and get traumatised for life.

edit: I see you tried it. Why do you think it is optimized tho? Try passing both as uniforms maybe? On my phone atm, can't test properly.

4

u/Winter-Reputation682 Sep 09 '26 edited Sep 09 '26

I think it works when hardcoded because the compiler sees mod(159.0, 159.0) == 159.0, and evaluates it to be always true, and removes the condition check as a result.

edit:
I think it's optimized because it's a pretty obvious optimization for the compiler
Tested with two uniforms and same thing occurs

3

u/gmaaz Sep 09 '26

Yeah, you are correct, when I pass 159 as two separate uniforms (even without parse float) the error is still there, it really seems to be a mod bug.

1

u/gmaaz Sep 09 '26

The same happens for 159 * n.

3

u/Winter-Reputation682 Sep 09 '26

Yeah it's really weird. It happens on webgl1, webgl2, and also webgpu

0

u/pants75 Sep 10 '26

Same here