fractional-scale-v1: should advise on rounding calculation
The fractional-scale-v1
protocol describes how, given a fractional scale value, clients should compute the a buffer size to use in conjunction with wp_viewport. Quoting:
The buffer size is calculated by multiplying the surface size by the scale. [...] if a surface has a surface-local size of 100 px by 50 px and wishes to submit buffers with a scale of 1.5, then a buffer of 150px by 75 px should be used and the wp_viewport destination rectangle should be 100 px by 50 px.
For toplevel surfaces, the size is rounded halfway away from zero. The rounding algorithm for subsurface position and size is not defined.
Say a client wants to submit a wl_surface which is W by H logical pixels in size, following the advice of the above protocol; and the scale factor provided by wp_fractional_scale_v1::preferred_scale is the fraction S / 120. Then the width of the wl_buffer that the client should submit is (expressed using pseudocode):
round(W * (S / 120))
The rounding rule is that if W*(S/120) has fractional part less than 1/2, the number is rounded down; and if it has fractional part >= 1/2, the number is rounded up. To compute this by hand, one would use rational numbers; for example, if W=300, S=11, then 300 * (11/120) = 3300/120 = 55/2 = 27 ½ , which rounds to 28.)
Unfortunately, the C programming language has only two easily accessible types of numbers: bounded range integers and IEEE754 binary floating point. One can compute the above formula using just integers ((W*S + 60)/120
), but more general calculations are tricky to do right. Computations with floating point look closer to the mathematical formulas. For example, directly translating the above pseudocode formula to C can be done like this:
int W = 300;
int S = 11;
double scale = S / 120.;
int v = llround(W * scale);
However, because binary floating point can only approximate multiples of 1/120, this code gives the wrong output; scale
evaluates to 0.09166666666666666 = 0x1.7777777777777p-4
, and W * scale
evaluates to 27.499999999999996 = 0x1.b7fffffffffffp+4
, which has fractional part less than 1/2; so llround
produces 27, which is incorrect.
Depending on whether computations are done in float, double, or a mixture of the two; and on the order of operations, direct translations into C may (or may not) give incorrect results for a small fraction of possible inputs. See https://github.com/swaywm/swaybg/pull/56#discussion_r1114733556 for some more analysis. (Note: If one must use floating point, one way to calculate the buffer dimensions correctly is to bias the value before rounding, like llround( W * (S / 120.) + 1/240.)
, and use double precision everywhere.).
Considering the above, I think that The text of the fractional-scale-v1
protocol should be edited to help implementers avoid this type of error.
A prior thread on this topic is at https://github.com/swaywm/swaybg/pull/56#discussion_r1112237340.