Skip to content

iOS: Remove FlutterMetalLayer - #193073

Draft
knopp wants to merge 5 commits into
flutter:masterfrom
knopp:vsync3a
Draft

knopp wants to merge 5 commits into
flutter:masterfrom
knopp:vsync3a

Conversation

@knopp

@knopp knopp commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

Fixes #189373

This PR depends on #191444 (for now squashed as first commit )

Removes FlutterMetalLayer switching back to CAMetalLayer, but ensures that the content is always present with transaction. Reduces the touch to preset latency by another frame matching UIKit.

Background:

FlutterMetalLayer was a wrong solution for a misunderstood problem: We were struggling to get CAMetaLayer with presentsWithTransaction=YES present from raster thread. Calling [drawable present] either from raster thread or through a hop to main thread resulted in very bad frame pacing. As a workaround FlutterMetalLayer was implemented, mirroring the API for CAMetalLayer, it worked through manually setting the IOSurface as layer content.

FlutterMetalLayer fixed the frame pacing issue completely, but introduced another frame of latency (on top of the one caused by smooth event dispatcher). When investigating this latency I realized that the root cause was that because of asynchronous rendering (raster thread) we submit the content after the CA commit for current frame. For FlutterMetalLayer this results in the content being rendered in next frame (extra latency), for CAMetalLayer this causes dropped frames.

CA Commit is a UIKit phase that happens after all other phases (event dispatch, display link callback, layout, draw). The solution to fix both the latency and restore CAMetalLayer is to delay the CA commit until raster thread is done. This is done by holding the main thread at the latest possible time (see the changes in vsync_waiter_ios.mm).

Pre-launch Checklist

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

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@knopp
knopp requested review from a team as code owners September 20, 2026 19:06
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Sep 20, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. platform-macos Building on or for macOS specifically platform-fuchsia Fuchsia code specifically a: desktop Running on desktop e: impeller Impeller rendering backend issues and features requests team-ios Owned by iOS platform team team-macos Owned by the macOS platform team labels Sep 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request removes the PointerDataDispatcher and SmoothPointerDataDispatcher classes, routing pointer events directly through the Animator and RuntimeController. It also removes FlutterMetalLayer in favor of standard CAMetalLayer on iOS, refactoring VsyncWaiterIOS to manage frame pacing via a layout trampoline view, and removes secondary vsync callbacks from VsyncWaiter. The review feedback identifies three critical issues: a potential data race on in_await_ in VsyncWaiter::FireCallback, an unsafe UIKit call on a background thread during VsyncWaiterIOS destruction, and a data race on the onFrameSubmitted property in FlutterPlatformViewsController due to its nonatomic declaration.

Comment on lines 88 to 93
VsyncWaiterIOS::~VsyncWaiterIOS() {
// This way, we will get no more callbacks from the display link that holds a weak (non-nilling)
// reference to this C++ object.
[client_ invalidate];
[layout_trampoline_view_ removeFromSuperview];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Safe UIKit Call on Destruction\n\nVsyncWaiterIOS::~VsyncWaiterIOS() calls [layout_trampoline_view_ removeFromSuperview]. Since VsyncWaiter is ref-counted via std::shared_ptr, if the last reference to VsyncWaiterIOS is released on a background thread (e.g., inside a background task or callback), the destructor will execute on that background thread. Calling UIKit methods like removeFromSuperview on a background thread is unsafe and can lead to crashes or undefined behavior.\n\nWe should defensively dispatch the removeFromSuperview call to the main thread if the destructor is executed on a background thread.

VsyncWaiterIOS::~VsyncWaiterIOS() {\n  // This way, we will get no more callbacks from the display link that holds a weak (non-nilling)\n  // reference to this C++ object.\n  [client_ invalidate];\n  if (layout_trampoline_view_) {\n    UIView* view = layout_trampoline_view_;\n    if ([NSThread isMainThread]) {\n      [view removeFromSuperview];\n    } else {\n      dispatch_async(dispatch_get_main_queue(), ^{\n        [view removeFromSuperview];\n      });\n    }\n  }\n}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't know why are you shouting but VsyncWaiterIOS should definitely be not released on background thread.

@property(nonatomic, weak) UIViewController<FlutterViewResponder>* _Nullable flutterViewController;

/// Called from main thread when a frame has been submitted.
@property(readwrite, nonatomic) void (^onFrameSubmitted)(void);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Thread Safety of onFrameSubmitted Property\n\nThe onFrameSubmitted property is declared as nonatomic in FlutterPlatformViewsController.h:\nobjc\n@property(nonatomic, weak) UIViewController<FlutterViewResponder>* _Nullable flutterViewController;\n\n/// Called from main thread when a frame has been submitted.\n@property(readwrite, nonatomic) void (^onFrameSubmitted)(void);\n\nHowever, submitFrame: is called on the raster thread, where self.onFrameSubmitted is read:\nobjc\n if (self.onFrameSubmitted != nil) {\n\nSince the property is written on the main thread and read on the raster thread, declaring it as nonatomic introduces a data race. In Objective-C, accessing a nonatomic property concurrently from multiple threads is unsafe and can lead to undefined behavior or crashes.\n\n### Solution\nWe should declare the onFrameSubmitted property as copy (which is atomic by default) to ensure thread-safe accessors are generated by the compiler.

@property(readwrite, copy) void (^onFrameSubmitted)(void);

@knopp
knopp marked this pull request as draft September 21, 2026 21:09
@flutter-dashboard

Copy link
Copy Markdown

This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically platform-macos Building on or for macOS specifically team-ios Owned by iOS platform team team-macos Owned by the macOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] FlutterMetalLayer re-presents stale frames: transitions visibly run backwards

1 participant