otsukare Thoughts after a day of work

How CSS width is computed in Gecko?

Yesterday, I have written about the strange value of the CSS width when set with a percentage. One Japanese night later, Boris Zbarsky sent me a private email explaining how CSS widths are computed in Gecko. These short emails are a gem of pleasure to read and insights into Gecko. I should almost open a new category here called "Santa Boris". I love them. Thanks.

So what did I learn with Boris? Here an extended version of Boris' email with C++ code. (I'm out of my comfort zone so if you see glaring mistakes, tell me I will happily fix them.)

Yesterday's Summary

In yesterday's article, the following CSS rule was applied on the child of a 360px parent element.

.foo {
    width: 49.99999%;
    border-right: 1px solid #CCC;
}
  • The 49.99999 % of 360 px in Gecko
  • is not 179.999964 px (as you could mathematically expect)
  • but 179.98333740234375 px (rounded to 179.983 px in the devtools UI).

Gecko storage of CSS

In Boris' words:

Gecko doesn't store CSS stuff as floating-point numbers. It stores them as integers, with the unit being 1/60 of a px (so that common fractions like 1/2, 1/3, 1/4, 1/5 are exactly representable).

Or said it another way 1 px = 60 (in Gecko). The 60 constant is defined in AppUnits.h (Brian Birtles helped me find it).

inline int32_t AppUnitsPerCSSPixel() { return 60; }

which in return is called a lot in Units.h.

Specifically in ToAppUnits()

    static nsRect ToAppUnits(const CSSRect& aRect) {
    return nsRect(NSToCoordRoundWithClamp(aRect.x * float(AppUnitsPerCSSPixel())),
                  NSToCoordRoundWithClamp(aRect.y * float(AppUnitsPerCSSPixel())),
                  NSToCoordRoundWithClamp(aRect.width * float(AppUnitsPerCSSPixel())),
                  NSToCoordRoundWithClamp(aRect.height * float(AppUnitsPerCSSPixel())));
  }

Let's continue with Boris' explanation broken down in steps. So instead of 360 px, we have an integer which is 360

  1. 49.99999% * 360
  2. 0.4999999 * (360 * 60)
  3. 0.4999999 * 21600
  4. 10799.99784

but as you can see above when computing the values, it uses a function NSToCoordRoundWithClamp().

inline nscoord NSToCoordRoundWithClamp(float aValue)
{
  /* cut for clarity */
  return NSToCoordRound(aValue);
}

And it goes to a rounding function.

The return trip is similar with FromAppUnits()

  static CSSIntRect FromAppUnitsRounded(const nsRect& aRect) {
    return CSSIntRect(NSAppUnitsToIntPixels(aRect.x, float(AppUnitsPerCSSPixel())),
                      NSAppUnitsToIntPixels(aRect.y, float(AppUnitsPerCSSPixel())),
                      NSAppUnitsToIntPixels(aRect.width, float(AppUnitsPerCSSPixel())),
                      NSAppUnitsToIntPixels(aRect.height, float(AppUnitsPerCSSPixel())));
  }

where

inline int32_t NSAppUnitsToIntPixels(nscoord aAppUnits, float aAppUnitsPerPixel)
{
  return NSToIntRound(float(aAppUnits) / aAppUnitsPerPixel);
/*       NSToIntRound(*/
}

which calls NStoIntRound()

inline int32_t NSToIntRound(float aValue)
{
  return NS_lroundf(aValue);
}

which calls NS_lroundf()

inline int32_t
NS_lroundf(float aNum)
{
  return aNum >= 0.0f ? int32_t(aNum + 0.5f) : int32_t(aNum - 0.5f);
}

(My C++ Kung-Fu (功夫) being miminal, I asked again Brian). NS_roundf() truncates the float number into an 32 bits integer. The construct

return aNum >= 0.0f ? int32_t(aNum + 0.5f) : int32_t(aNum - 0.5f);
/*      (condition) ? true                 : false                 */
  • 1.2 so a positive number, condition is true, then 1.2 + 0.5 = 1.7 convert to int32_t which is 1 by truncation.
  • 1.8, positive number, true, 1.8 + 0.5 = 2.3, truncation to 2.

Let's go back to our 10799.99784 found earlier. It is being truncated to 10799 (and not rounded to 10800).

You can see that happening in nsRuleNode.cpp

/* static */ nscoord
nsRuleNode::ComputeCoordPercentCalc(const nsStyleCoord& aCoord,
                                    nscoord aPercentageBasis)
{
  switch (aCoord.GetUnit()) {
    case eStyleUnit_Coord:
      return aCoord.GetCoordValue();
    case eStyleUnit_Percent:
      return NSToCoordFloorClamped(aPercentageBasis * aCoord.GetPercentValue());
    case eStyleUnit_Calc:
      return ComputeComputedCalc(aCoord, aPercentageBasis);
    default:
      MOZ_ASSERT(false, "unexpected unit");
      return 0;
  }
}

Now let's get back to pixel units.

10799 / 60 = 179.983333333333333… and beyond infinity of 3.

The acute reader would have noticed that this is still not 179.98333740234375 px but close. Boris says:

The difference between that (aka 179.983333333333333…) and 179.98333740234375 is because the multiplication is done on 32-bit floats, which only have 7 decimal digits of accuracy; 179.98333740234375 is the closest representable 32-bit float to 179.983333333333333…

And now you know exactly why and how Gecko computes its CSS width.

Browsers and their sub-pixel units

What other browsers do? The "Ahahahah" moment of this blog post.

Browsers do layout with subpixel units: WebKit/Blink: 1/64 pixels. Gecko: 1/60. Edge: 1/100 — smfr

If a developer from WebKit/Blink could explain how they compute their own widths with links to code, I'm interested. And Edge developers too (without the link to the code but I'll take snippets of code). All of this in the spirit of Web Compatibility and interoperability.

PS: Again thousand of thanks to Boris and Brian.

Otsukare!