Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[rotation_distortion]send interpolated viewport metrics to address rotation distortion #40412

Closed

Conversation

hellohuanlin
Copy link
Contributor

@hellohuanlin hellohuanlin commented Mar 17, 2023

Send interpolated viewport metrics to make the rotation smoother. Result is pretty good, as show below.

There are still some minor amount of distortion. There are 2 main reasons:

  1. Flutter drawing happens on the UI thread, which is not iOS main thread. The delay between issuing engine command till the drawing happens is indeterministic and could vary depending on how busy/idle the app is during the rotation. This amount of distortion is unavoidable, and unlikely solvable.
  2. We use linear interpolation for simplicity, but iOS window rotation is likely non-linear, with ease-in ease-out effect. Consider using non-linear interpolation if this becomes a problem.

Currently we are calling framework from engine per frame. A potential performance improvement can be just sending the rotation duration to the framework, and have the framework to do the interpolation by itself. Though this will require a whole lot more refactoring on both engine and framework.

3.size.interpolation.mp4

List which issues are fixed by this PR. You must list at least one issue.

Fixes flutter/flutter#16322

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@hellohuanlin hellohuanlin force-pushed the rotation_size_interpolation branch 10 times, most recently from b7b91d2 to 2a51b57 Compare March 22, 2023 16:12
@hellohuanlin hellohuanlin marked this pull request as ready for review March 22, 2023 16:44
Copy link
Contributor

@dnfield dnfield left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM.

I think an alternative we might consider is to update the framework/Dart API so that this could be done in Dart code instead - but if iOS is really the only platform where this happens that may be overkill.

It would be worth filing a bug to improve the curve used on this. Historically iOS users care about this kind of thing more than I would have expected, and with this patch it should be relatively easy to fix for someone who wants to take the time to figure that out.

UIEdgeInsets fromPadding,
CGSize toSize,
UIEdgeInsets toPadding) {
CGFloat scale = [UIScreen mainScreen].scale;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest checking if the view is loaded before doing this:

Suggested change
CGFloat scale = [UIScreen mainScreen].scale;
UIScreen* mainScreen = self.mainScreenIfViewLoaded;
if (mainScreen == nil) {
return;
}
CGFloat scale = mainScreen.scale;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just did some research - it turned out to be more involved and likely requires non-trivial refactoring to make it work in unit test, as explained here.

/**
* Interpolate the viewport metrics for smoother rotation transition.
*/
void FLTInterpolateViewportMetrics(flutter::ViewportMetrics& viewportMetrics,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing in a viewportMetrics reference, can this instead be a method on the view controller that directly accesses _viewportMetrics? Or was that too difficult to test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I had issue with testing - OCMock/NSInvocation only supports populating NSObject references, and not structs. And I do not want to expose viewportMetrics property as it's anti-pattern. So I made this pure function for easier testing.


// End of rotation. Invalidate the timer.
if (rotationProgress == 1) {
_isDuringRotationTransition = NO;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this potentially racy, can -viewWillTransitionToSize be called before the timer is invalidated/the rotation is complete? Can the rotation be reversed halfway through? Maybe this should instead be tracked with a NSConditionLock? Or the timer object be cached on the view controller and invalidated if -viewWillTransitionToSize is called a second time before it's complete?

Copy link
Contributor Author

@hellohuanlin hellohuanlin Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I did some experiment - the viewWillTransitionToSize itself behaves nicely, it gets called only after the previous rotation is done. However, race can still happen due to a few reasons (added in the comment).

__block double rotationProgress = 0;
// Timer is retained by the run loop, and will be released after invalidated.
[NSTimer
scheduledTimerWithTimeInterval:kRotationViewportMetricsUpdateInterval
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe replace kRotationViewportMetricsUpdateInterval with 1/[DisplayLinkManager displayRefreshRate] for pro-motion devices.

Copy link
Contributor Author

@hellohuanlin hellohuanlin Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not necessary during rotation since the device is moving. I've even tried 30fps - not too bad either (And i don't have a pro motion device)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can test with promotion device

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to pull the branch, or can wait for me to fix my laptop's engine build (hopefully this week).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, let me see if I can expense one.

@hellohuanlin hellohuanlin force-pushed the rotation_size_interpolation branch from 3a45ba4 to 580b9d2 Compare March 22, 2023 22:04
@hellohuanlin
Copy link
Contributor Author

hellohuanlin commented Mar 23, 2023

Update: @cyanglaz and I found a performance issue for complex projects like flutter gallery.

Let me try a few things. Worst case scenario we may have to rely on the snapshot approach.

CGSize oldSize = self.view.bounds.size;
UIEdgeInsets oldPadding = self.view.safeAreaInsets;

__block double rotationProgress = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively use NSProgress, this is fine though.

@@ -1299,7 +1419,7 @@ - (void)viewDidLayoutSubviews {
_viewportMetrics.physical_height = viewBounds.size.height * scale;

[self updateViewportPadding];
[self updateViewportMetrics];
[self updateViewportMetricsIfNeeded:NO];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the call sites it's not clear what the boolean does (missing named parameters in Objective-C 😞). How about -updateViewportMetricsIfNeeded and -updateViewportMetricsIfNeededForRotation?

@hellohuanlin
Copy link
Contributor Author

hellohuanlin commented Mar 27, 2023

To share an update: I tried fine-tuning the interpolation count to address the perf issue on complex UI, and found 1/20 to 1/30 second (rather than 1/60 second) update interval worked for flutter gallery. However, this resulted in very "un-smooth" transition which is easily noticeable.

There is also a good news - Fun story, when i was playing basketball yesterday, a new solution came to my mind, and I call it delayed swap:

In the originally behavior, we swap the width/height immediately before the rotation, resulting in roughly ~4x distortion in the beginning. With "delayed swap" solution, we swap the width/height right in the middle of the rotation (i.e. delay the swap for half of the transition duration).

If I am imagining it right, this new "delayed swap" solution should give us the same result as the "snapshot" solution (reducing ~4x distortion to ~2x), but without the drawback of "snapshot" solution (i.e. it should work well with dynamic content like animation or video).

This "delayed swap" solution is much easier than both "size interpolation" and "snapshot" solution. I am quickly trying it out and hope to get some good result🤞

auto-submit bot pushed a commit that referenced this pull request Apr 17, 2023
…distortion (#40730)

The "size interpolation" solution didn't go well (more context [here](#40412 (comment))). Then a new solution came to my mind, and I call it **"delayed swap"**:

In the originally behavior, we swap the width/height immediately before the rotation, resulting in roughly ~4x distortion in the beginning. With "delayed swap" solution, we **swap the width/height right in the middle of the rotation** (i.e. delay the swap for half of the transition duration).

This new "delayed swap" solution gives us the same benefit as the "snapshot" solution: 
- reducing ~4x distortion to ~2x
- most distorted frames occur in the middle of rotation when it's moving the fastest, making it hard to notice

And it fixes the drawback of "snapshot" solution: 
- it works well with dynamic content like animation or video
- it doesn't have a ~0.5 second penalty when taking the snapshot

Looks pretty good on flutter gallery: 

https://user-images.githubusercontent.com/41930132/228383137-7cd09982-89a9-4c83-bf55-9431de708278.mp4

*List which issues are fixed by this PR. You must list at least one issue.*

Fixes flutter/flutter#16322

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rotation on iOS does not look right
4 participants