Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for different pixel densities #16

Open
chearon opened this issue Apr 5, 2024 · 11 comments
Open

Better support for different pixel densities #16

chearon opened this issue Apr 5, 2024 · 11 comments

Comments

@chearon
Copy link
Owner

chearon commented Apr 5, 2024

Right now the method is to scale the canvas by a certain amount before painting and round paint coordinates for rects to the nearest integer. But that only produces sharp edges when the zoom is an integer.

It's not hard to support any arbitrary zoom level (like 1.3333). Don't scale the canvas at all and instead, the painting code receives the zoom level as an input, multiplies the layout coordinates by that, and then rounds. This is particularly useful for browser zoom, which makes the window.devicePixelRatio non-even. Things should line up correctly.

Browsers use a much more complicated method of snapping the layouts to pixels. I don't totally know why that is; maybe to produce more desirable results at the extremes.

@nicoburns
Copy link

See DioxusLabs/taffy#369 for some investigation into how browsers to rounding. Conclusions:

  • For layout: use fractional values (don't round)
  • For painting: round positions (not lengths). So for each box, round left and right positions separately and then compute width from the new left and right positions.
  • I believe there is special handling for border widths which get separately rounded to integer values earlier in the process. Not quite sure on the details of this, but browsers will still happily render a 0.5px border with a single pixel in contexts with a pixel ratio of 2.

@chearon
Copy link
Owner Author

chearon commented Apr 10, 2024

Thanks for that! Your comment here is interesting:

Rounding of fractional pixels does not affect layout at all. As can be clearly be seen in the test 2 screenshot, once the whole-pixel rounding has occured there is actually room to render both grandchildren nodes on the same line for the 1st and 3rd children. But they wrap anyway because the layout is computed using the unrounded float values

I made a similar example here a while back (zoom in and out a lot - their relative sizes change even though their defined pixels sum to the same) that shows that browsers do round the layout boxes. I agree they don't totally re-layout after that. Sounds like that's what Taffy does now (rounds the layout boxes before render)?

I can see this getting difficult. If an inline table changed total size, the line it's on has to change size too. It's like you have to do layout again, but only changing sizes and positions rather than what flex item or text run goes on what line. Did you come across anything like that with flexboxes-in-flexboxes?

For painting: round positions (not lengths). So for each box, round left and right positions separately and then compute width from the new left and right positions.

In the issue you posted, I can see why this would fix it. According to this old WebKit Wiki, Safari rounded lengths sometimes, and edges sometimes. They don't say when they use which methods though.

@nicoburns
Copy link

made a similar example here a while back (zoom in and out a lot - their relative sizes change even though their defined pixels sum to the same) that shows that browsers do round the layout boxes.

Hmm... I'm not quite sure what I'm supposed to be looking at here. Is the ratio between the first and second columns that you're comparing? How are you measuring the widths?

What I found (mostly testing Chrome, although FF seemed similar) was that:

  • getBoundingClientRect() will give you fractional values for box size and position.
  • Older JS APIs like offsetWidth, clientWidth, and scrollWidth will give you those same values naively rounded (literally the fractional values with Math.round` called on them.
  • That rounding is not what is used for painting which seems to use an algorithm similar to the one that Taffy and Yoga use where it is the fractional x/y values relative to the viewport that is rounded (and widths/heights then recomputed from those rounded positions).
  • This rounding appears not to affect layout in my observations (they're not available to JS and it seems that when implementing layout algorithms they can be ignored (and just rounded at the end)). I haven't checked table layout though and it's possible that things are different there.

If an inline table changed total size, the line it's on has to change size to

My mental model is that it doesn't ever change size. It ends up with a fractional size. Which will be rounded up or down by (0 < x < 1) pixels for paint but that won't cause anything else to move.

According to this old WebKit Wiki, Safari rounded lengths sometimes, and edges sometimes. They don't say when they use which methods though.

Yeah, I think I've read that before. I think borders thicknesses are one of those cases but I'm not sure on all of them. I suspect different browsers may do this slightly differently in some cases.

@chearon
Copy link
Owner Author

chearon commented Apr 10, 2024

You should see the table and the float (which is defined to be the same height as the table when you sum its row sizes) have different heights:

Screen Shot 2024-04-10 at 6 42 36 PM Screen Shot 2024-04-10 at 6 42 22 PM

You can use getBoundingClientRect() and you'll get (very!) different values for the table's height at different zoom levels.

If you put something beneath those, it gets shifted too. So the browser actually does a whole second pass on the layout tree before painting. Not only do the boxes change size, but the boxes that depend on the positions of other boxes also get moved.

Some of the other tests I've done sound more like what you're saying, which could be because it doesn't always manifest obviously. I'll see if I can find some browser code that tells us more.

@nicoburns
Copy link

Ah I see. This does actually seem to be something to do with the borders. Remove the borders between cells and the table is always exactly 400px high (and always matches the float if you change it to have 400px height)

@chearon
Copy link
Owner Author

chearon commented Apr 10, 2024

Huh, interesting. With the borders, though, I can't explain it unless pixel snapping is layout-aware. If it's only reproducible with borders it means something, but I don't know what.

@nicoburns
Copy link

My understanding is that there is an additional pixel rounding/snapping step specifically for borders (rounding to integer physical pixels perhaps?) that takes place either prior to layout or as part of layout. I can't remember where I've seen this, but I've definitely seen this referenced in material about one of the major browsers somewhere.

@chearon
Copy link
Owner Author

chearon commented Apr 10, 2024

That would explain it! Kinda complicates zooming/pixel density though. Let me know if you remember where you read that.

I feel relieved. I can't find much in Firefox that would indicate pixel snapping in/after layout, and I've read some things saying it happens in WebRender (lots of pixel snapping here). So I think you're right, it can mostly be done in painting.

@nicoburns
Copy link

This isn't what I read before, but this issue (w3c/csswg-drafts#3720) has good information. In particular w3c/csswg-drafts#3720 (comment):

Gecko's border rounding behavior (round down in device pixels, except round values between 0 and 1 device pixels up to 1)

This test is also interesting:
https://codepen.io/SelenIT/pen/LbbGZL

@chearon
Copy link
Owner Author

chearon commented Apr 11, 2024

This test is also interesting:
https://codepen.io/SelenIT/pen/LbbGZL

So getBoundingClientRect pretty much proves that borders are rounded before layout. Because width and height remain fractional from CSS to getBoundingClientRect, but borders don't. Huh.

I also found this comment which says it definitively:

I think all browsers snap borders to whole-number display-pixel-values (at least, they do for borders less than 1px) -- so e.g. 0.4px will just round up to one display-pixel (and so will 0.01px), at computed-value-time (i.e. and it actually reserves & occupies that much layout space, rather than the specified tiny-fraction-of-a-pixel).

Thanks for pointing me in the right direction. I guess it's because the edge-snapping behavior could snap borders to zero-sized? Rounding during computed style calc is a little better than snapping to at least 1px during paint since it won't go over content, but the whole thing strikes me as too odd to be better. Did Taffy adopt this behavior/are you planning on it?

@nicoburns
Copy link

nicoburns commented Apr 11, 2024

w3c/csswg-drafts#5210 (comment) also seems good:

To snap a length as a border width means to convert that length (presumed to already be nonnegative) as follows:

if the length is greater than 0 and less than 1 device pixel, it should be rounded up to 1 device pixel;
if the length is greater than 1 device pixel, it should be rounded down to the nearest integer number of device pixels.

So it seems like border values in device pixels are rounded to integer values (again device pixels) using this floor if > 1, ceil if < 1 formula

Where device pixel is defined as:

A device pixel is the smallest unit of area on the device output capable of displaying its full range of colors. For typical color screens, it’s a square or somewhat rectangular region containing a red, green, and blue subpixel. Many non-traditional outputs exist that can blur this definition, such as by displaying some colors at higher resolutions. Such devices still expose some equivalent notion of "device pixel", however.

Taffy doesn't currently implement this, but probably will, perhaps as an option (although I think it can currently be implemented outside of Taffy (and for Blitz/Stylo-Dioxus, I wonder whether stylo will actually already be doing this for us).

Currently Taffy has no concept of scale factor: you get ought the units you use as input. But Taffy's existing rounding/pixel-snapping behaviour effectively assumes physical pixels (it doesn't make much sense to snap to logical pixels) and we may well want to make it a setting (where you can the scale to 1.0 to get the existing behaviour).

At a meta-level, I'd definitely like Taffy to serve a role in documenting this kind of "kinda-in-the-spec-but-kinda-not" stuff.

Also:

chearon added a commit that referenced this issue Oct 18, 2024
chearon added a commit that referenced this issue Oct 18, 2024
chearon added a commit that referenced this issue Nov 20, 2024
Ref #16. Just need to snap border, padding, and content areas
during paint (absolutify?) and then I can close it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants