Skip to content

Proposal: Framework needs to be aware of physical pixels #151065

@knopp

Description

@knopp

I raised this a while ago, but then gave up and closed the issue. It is a fundamental issue though so I'm reopening it.

Consider the following snippet:

 runApp(
    Align(
      alignment: Alignment.topLeft,
      child: Padding(
        padding: const EdgeInsets.only(left: 20, top: 20),
        child: Container(
          decoration: BoxDecoration(
              color: const Color.fromRGBO(255, 166, 0, 0.428),
              border: Border.all(color: Colors.yellow, width: 1)),
          width: 64,
          height: 45,
          child: Center(
            child: Container(
              decoration: BoxDecoration(
                  border: Border.all(color: Colors.yellow, width: 1)),
              width: 42,
              height: 22,
            ),
          ),
        ),
      ),
    ),
  );

When you run this on a device with 100% pixel scaling, you get the following result:

Screenshot 2024-06-30 at 13 56 40

A commonly used 4k 27" monitor on windows will run with 150% pixel scaling factor. Running any Flutter application on such setup, you can pretty much guarantee that every single widget border will be blurry. It is near impossible to deliver a sharp looking Flutter application on Windows that works correctly with fractional scale factors (and as mentioned above sometimes even with a basic 100% scaling).

This is a problem that has been solved pretty much by every modern toolkit (WinUI, Compose, Browser) except for Flutter, with keeps stubbornly pretending that it is rendering into an infinite resolution surfaces, which is just not the case.

There are many places where this causes issues i.e. #14288, #90926, #25531, #31707 or #143420, last of which has been solved with a really ugly hack.

Another big issue stemming from this is that all layout in Flutter is performed in logical pixel space on floating point numbers. Floating point math is not associative, which causes many subtle and difficult to find issues, such as this:

Proposed solution

The solution for this issue is rather straightforward, unfortunately it's also a major breaking change which makes applying it difficult.

  1. Remove the device pixel ratio scaling transform from the app. Using transformation for this is simply too blunt and causes the issues above. This transform creates an illusion that we don't need to deal with the presence of physical pixels, but this quickly falls apart when running of anything with less than 200% device pixel ratio.
  2. All user specified unit (i.e. border size, padding, dimensions, font size) would be specified in device independent coordinates. This could even be an extension type to make things explicit, though I'm not sure what would happen with existing geometric primitives like Offset or Rect.
  3. All layout would be performed in physical pixel coordinates and integers.
  4. All painting would be performed in physical pixel coordinates.

The conversion from logical to physical pixel space is a simple scale + round(). i.e.

For example imagine that you have 1px border around widget and 1.5 device pixel ratio. In which case
physicalPixels = (1 * 1.5).round() = 2

This would offset the layout by two physical pixels, and would also paint as two physical pixels.

Constraints and geometry would probably need to be converted to integers and in physical pixel space. There is an issue where some of the Widget API is using constraints (i.e. LayoutBuilder, ConstrainedBox), we would need to have a convenient way to convert back and fort from physical to logical pixel space.

Changes to layout

All layout would be performed in physical pixel space on integral coordinates. This would ensure that under normal circumstances (no arbitrary transform in the app), all widgets would be laid out on exact physical pixel boundaries. This would require some changes in the layout algorithms, for example consider flex with 3 expanded widgets constrained to 100 physical pixels. The flex can only produce integral coordinates and must fill all the space, so the final dimensions of the widgets would be 33, 33 and 34 pixels.

Arbitrary scale transforms

Most Flutter applications likely don't use arbitrary scale transforms, apart from transient things like a zoom effect on hover. I'd wager that in majority of Flutter application there is exactly one persistent scale transform in the layer tree - the device pixel ratio scale. With the changes proposed here this would be removed, replaced by manual scale + roundToInteger in appropriate places in framework.

Of course nothing prevents user from adding other transforms that will not respect physical pixel boundaries, but that case aligning to physical pixels is likely not a concern so it is orthogonal to the proposal here.

Engine changes

This would not require any engine changes at all. This needs to be all handled at framework level, since it impacts layout. If done correctly, final transform in the layout tree will all be pixel snapped by the time they reach the engine.

Migration

This is the tricky part. If anyone has an idea of how we could gradually migrate Render Objects, Constraints, Geometries and painting to physical space and integral coordinates (for layout) I'd love to hear about it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: proposalA detailed proposal for a change to Fluttercustomer: crowdAffects or could affect many people, though not necessarily a specific customer.frameworkflutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions