Skip to content

[iOS] Extract FlutterVSyncClient from vsync_waiter_ios#185737

Merged
cbracken merged 6 commits into
flutter:masterfrom
cbracken:extract-fluttervsyncclient
Apr 30, 2026
Merged

[iOS] Extract FlutterVSyncClient from vsync_waiter_ios#185737
cbracken merged 6 commits into
flutter:masterfrom
cbracken:extract-fluttervsyncclient

Conversation

@cbracken

Copy link
Copy Markdown
Member

This refactoring cleans up VSyncClient's API, separating it into a clean, pure Objective-C API in FlutterVSyncClient.h, and a category that declares an initialiser with C++ types such as fml::TaskRunner and the C++ callback to be fired on vsync boundaries. To avoid hardcoding/redeclaring the Testing category that exposes displayLink and onDisplayLink: I've factored out a FlutterVSyncTesting+Testing.h which can be used in FlutterVSyncClientTest.mm and
FlutterViewControllerTest.mm.

This also renames VSyncClient and DisplayLinkManager to include the "Flutter" prefix, though the Swift names drop the prefix as we do with other classes.

While initialisation of VSyncClient still requires C++ types, this refactoring allows users of VSyncClient such as FlutterKeyboardInsetManager and others to be implemented in Swift. Since this is used throughout the embedder, there's significant value in cleaning up the API, as it unblocks new code from needing to be written in Objective-C++.

This is part of a series of refactorings that aim to place a thin, lightweight layer of abstraction between the core C++ engine and the iOS embedder. Core to this effort are VSyncWaiterIOS, PlatformViewIOS, and IOSExternalViewEmbedder.

Background:

flutter::VsyncWaiter is the core engine's common abstraction for a mechanism for waiting for and getting callbacks on frame boundaries vsync events. These callbacks are used by flutter::Animator to produce frames, but also for other purposes, including frame rate correction for touch events in FlutterViewController and syncing keyboard animations in FlutterKeyboardInsetManager.

flutter::VsyncWaiterIOS is its iOS-specific concrete implementation. VsyncWaiterIOS allocates and owns an Objective-C VSyncClient object which is the core of the waiting mechanism. VSyncClient uses a CADisplayLink under the hood, via a probably extraneous DisplayLinkManager wrapper class.

The general flow looks like:

  • On intialisation, VsyncWaiterIOS allocates and initialises a VSyncClient. It hands it a callback that should be called on vsync events, and a task runner whose run loop is used for setting up the CADisplayLink with the correct thread affinity (e.g. the UI thread when used to produce Flutter frames, and to manage insets during keyboard animations, or the platform thread for touch event rate correction).
  • core engine calls VsyncWaiter::AwaitVSync() to wait for the next frame boundary. VSyncWaiterIOS calls [VSyncClient await] which unpauses CADisplayLink so it starts sending vsync events.
  • On the next vsync from CADisplayLink [VSyncClient onDisplayLink:] is called which calculates high-precision frame_start_time and frame_target_time, pauses CADisplayLink again, then fires the vsync callback, notifying the core engine

Follow-ups:

After this refactoring, I have a follow-up patch that migrates this off the C++ fml::TaskRunner and flutter::TaskRunners types and onto the pure Objective-C FlutterFMLTaskRunner and FlutterFMLTaskRunners classes.

Issue: #112232

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.

@cbracken cbracken requested a review from a team as a code owner April 29, 2026 10:47
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 29, 2026
@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. team-ios Owned by iOS platform team labels Apr 29, 2026
@interface VSyncClient (Testing)
- (CADisplayLink*)getDisplayLink;
- (void)onDisplayLink:(CADisplayLink*)link;
@end

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.

This is now in FlutterVSyncClient+Testing.h. Both this and the copy-pasted version in FlutterViewControllerTest.mm have been moved there.

@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 refactors the iOS VSync implementation by extracting the VSync client and display link management into standalone classes, FlutterVSyncClient and FlutterDisplayLinkManager. The changes involve updating the build configuration, renaming classes for consistency, and refactoring VsyncWaiterIOS to use the new client. Feedback highlights critical safety concerns regarding CADisplayLink target management, including potential crashes from dangling pointers in asynchronous tasks, the need for explicit invalidation in dealloc, and a possible division-by-zero error during refresh rate calculations.

@interface FlutterVSyncClient ()

- (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner
callback:(flutter::VsyncWaiter::Callback)callback;

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.

This change is purely a refactoring to separate the Obj-C from the C++ but doesn't make any logic changes.

This header is purely transitional; the next patch migrates to FlutterFMLTaskRunner and a regular Obj-C block callback, and we delete this.

@@ -0,0 +1,79 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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.

This header and the .mm implementation file below are extracted from vsync_waiter_ios.{h,mm}. The pure C++ core engine binding class VsyncWaiterIOS remains in those files, and these contain the Obj-C client classes.

@end

@implementation VsyncWaiterIosTest
@implementation FlutterVSyncClientTest

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.

This was never testing the C++ VsyncWaiterIos class; renamed to reflect what it actually does.

@cbracken cbracken force-pushed the extract-fluttervsyncclient branch from 57d640b to f3d580d Compare April 29, 2026 12:20
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 29, 2026
@cbracken cbracken added engine flutter/engine related. See also e: labels. CICD Run CI/CD and removed engine flutter/engine related. See also e: labels. labels Apr 29, 2026
hellohuanlin
hellohuanlin previously approved these changes Apr 29, 2026
if (_allowPauseAfterVsync) {
link.paused = YES;
}
_callback(std::move(recorder));

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.

Interesting! So this is the code that actually triggers Flutter to render the next frame?

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.

Yeah the core engine needs a source of vsyncs to schedule frames. When using the embedder API, the user would supply a VsyncCallback in their FlutterProjectArgs. For iOS/Android/Fuchsia, we have the VsyncWaiter class, with platform-specific subclasses that implements the vsync provider. Specifically VsyncProvider::AwaitVsync() is what the engine calls when it wants to schedule a frame. You can grep for Animator::RequestFrame() to see where this happens.

I wrote up the high level flow here:
https://github.com/flutter/flutter/blob/master/docs/engine/Life-of-a-Flutter-Frame.md

This refactoring cleans up VSyncClient's API, separating it into a
clean, pure Objective-C API in FlutterVSyncClient.h, and a category that
declares an initialiser with C++ types such as fml::TaskRunner and the
C++ callback to be fired on vsync boundaries. To avoid
hardcoding/redeclaring the Testing category that exposes `displayLink` and
`onDisplayLink:` I've factored out a FlutterVSyncTesting+Testing.h which
can be used in FlutterVSyncClientTest.mm and
FlutterViewControllerTest.mm.

This also renames VSyncClient and DisplayLinkManager to include the
"Flutter" prefix, though the Swift names drop the prefix as we do with
other classes.

While initialisation of VSyncClient still requires C++ types, this
refactoring allows users of VSyncClient such as FlutterKeyboardInsetManager
and others to be implemented in Swift. Since this is used throughout the
embedder, there's significant value in cleaning up the API, as it
unblocks new code from needing to be written in Objective-C++.

This is part of a series of refactorings that aim to place a thin,
lightweight layer of abstraction between the core C++ engine and the iOS
embedder. Core to this effort are VSyncWaiterIOS, PlatformViewIOS, and
IOSExternalViewEmbedder.

Background:

flutter::VsyncWaiter is the core engine's common abstraction for a
mechanism for waiting for and getting callbacks on frame boundaries
vsync events. These callbacks are used by flutter::Animator to produce
frames, but also for other purposes, including frame rate correction for
touch events in FlutterViewController and syncing keyboard animations in
FlutterKeyboardInsetManager.

flutter::VsyncWaiterIOS is its iOS-specific concrete implementation.
VsyncWaiterIOS allocates and owns an Objective-C VSyncClient object
which is the core of the waiting mechanism. VSyncClient uses a
CADisplayLink under the hood, via a probably extraneous
DisplayLinkManager wrapper class.

The general flow looks like:
* On intialisation, VsyncWaiterIOS allocates and initialises a
  VSyncClient. It hands it a callback that should be called on vsync
  events, and a task runner whose run loop is used for setting up the
  CADisplayLink with the correct thread affinity (e.g. the UI thread
  when used to produce Flutter frames, and to manage insets during
  keyboard animations, or the platform thread for touch event rate
  correction).
* core engine calls VsyncWaiter::AwaitVSync() to wait for the next frame
  boundary. VSyncWaiterIOS calls [VSyncClient await] which unpauses
  CADisplayLink so it starts sending vsync events.
* On the next vsync from CADisplayLink [VSyncClient onDisplayLink:] is
  called which calculates high-precision frame_start_time and
  frame_target_time, pauses CADisplayLink again, then fires the vsync
  callback, notifying the core engine 

Follow-ups:

After this refactoring, I have a follow-up patch that migrates this off
the C++ fml::TaskRunner and flutter::TaskRunners types and onto the pure
Objective-C FlutterFMLTaskRunner and FlutterFMLTaskRunners classes.

Issue: flutter#112232
@cbracken cbracken force-pushed the extract-fluttervsyncclient branch from 3b08263 to c8382c3 Compare April 29, 2026 23:28
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 29, 2026
@cbracken cbracken added this pull request to the merge queue Apr 30, 2026
Merged via the queue into flutter:master with commit 6eea11a Apr 30, 2026
197 checks passed
@cbracken cbracken deleted the extract-fluttervsyncclient branch April 30, 2026 02:25
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request May 1, 2026
Roll Flutter from 81bc3d69535f to 707dbc0420a3 (85 revisions)

flutter/flutter@81bc3d6...707dbc0

2026-05-01 [email protected] Removing TODOs from the WebParagraph code and addressing technical debts. (flutter/flutter#185168)
2026-05-01 [email protected] Ensure that vulkan_interface.h gets included before vk_mem_alloc.h (flutter/flutter#185777)
2026-05-01 [email protected] [flutter_tools] Bump dwds dependency to v27.1.1 (flutter/flutter#185357)
2026-05-01 [email protected] Roll Dart SDK from 6d4a319cbdac to 9aa7097f2e3e (3 revisions) (flutter/flutter#185888)
2026-05-01 [email protected] Roll Skia from fa1dcb289709 to 7ac6d42f2fd0 (1 revision) (flutter/flutter#185887)
2026-05-01 [email protected] Roll Skia from 54cc00adde38 to fa1dcb289709 (3 revisions) (flutter/flutter#185880)
2026-05-01 [email protected] [iOS] Migrate to FlutterFMLTaskRunner(s) (flutter/flutter#185815)
2026-05-01 [email protected] Remove material imports from navigator_on_did_remove_page_test and scrollable_in_overlay_test (flutter/flutter#182546)
2026-05-01 [email protected] Roll Skia from 2e279266f06a to 54cc00adde38 (3 revisions) (flutter/flutter#185872)
2026-05-01 [email protected] dev: Remove unused parameters (flutter/flutter#185345)
2026-05-01 [email protected] Roll Fuchsia Linux SDK from HN5VYzftnf_B8T-n9... to VnzuUefDQR0UhQ1L1... (flutter/flutter#185870)
2026-05-01 [email protected] Use g_free when using glib memory allocation (flutter/flutter#185519)
2026-05-01 [email protected] Roll Dart SDK from d30df3428f2e to 6d4a319cbdac (2 revisions) (flutter/flutter#185862)
2026-05-01 [email protected] Remove trivial test utility cross-imports from material and cupertino… (flutter/flutter#184295)
2026-05-01 [email protected] Inline test callback painter in tab scaffold test (flutter/flutter#184851)
2026-05-01 [email protected] [a11y] Add support for high contrast themes in the a11y assessments app  (flutter/flutter#185801)
2026-05-01 [email protected] [a11y assessment app] Use default color for banner (flutter/flutter#185804)
2026-04-30 [email protected] Added name to AUTHORS (flutter/flutter#185586)
2026-04-30 [email protected] Remove semantics_tester import from raw_material_button_test.dart (flutter/flutter#184806)
2026-04-30 [email protected] Remove semantics_tester import from user_accounts_drawer_header_test.dart (flutter/flutter#184809)
2026-04-30 [email protected] Roll Skia from 7b88c5c281e5 to 2e279266f06a (5 revisions) (flutter/flutter#185854)
2026-04-30 [email protected] Handle symmetric rectangular and elliptical round super ellipses in the uber SDF renderer  (flutter/flutter#185695)
2026-04-30 [email protected] Match on process name before killing for SwiftPM (flutter/flutter#185774)
2026-04-30 [email protected] Sync CHANGELOG.md from stable (flutter/flutter#185838)
2026-04-30 [email protected] Roll Dart SDK from 25910e31a6d2 to d30df3428f2e (5 revisions) (flutter/flutter#185839)
2026-04-30 [email protected] Check cross imports test subfolders (flutter/flutter#185493)
2026-04-30 [email protected] test: inline TestCallbackPainter in cupertino/picker_test.dart (flutter/flutter#185398)
2026-04-30 [email protected] Update customer testing version (flutter/flutter#185830)
2026-04-30 [email protected] Adapt the Metal shader library output list for debug versus release mode (flutter/flutter#185798)
2026-04-30 [email protected] [Impeller] Port a recent Vulkan swapchain fence waiting fix to the AHB version of the swapchain (flutter/flutter#185763)
2026-04-30 [email protected] Roll Skia from 26a59aa71eff to 7b88c5c281e5 (1 revision) (flutter/flutter#185821)
2026-04-30 [email protected] Roll Skia from 6b4167b4e204 to 26a59aa71eff (4 revisions) (flutter/flutter#185808)
2026-04-30 [email protected] [a11y] Mark SemanticsNode dirty when customSemanticsActions change  (flutter/flutter#185707)
2026-04-30 [email protected] Roll Skia from 1bd2f1cc2746 to 6b4167b4e204 (8 revisions) (flutter/flutter#185799)
2026-04-30 [email protected] [iOS] Extract FlutterVSyncClient from vsync_waiter_ios (flutter/flutter#185737)
2026-04-30 [email protected] Roll Fuchsia Linux SDK from nnv8-SSam6yE8dw4z... to HN5VYzftnf_B8T-n9... (flutter/flutter#185782)
2026-04-29 [email protected] [iOS] Soften TaskRunner.postTask(delay:task:) delay check (flutter/flutter#185729)
2026-04-29 [email protected] Add reportErrors to ImageStreamListener (flutter/flutter#180327)
2026-04-29 [email protected] Roll Skia from f5c781c083c7 to 1bd2f1cc2746 (5 revisions) (flutter/flutter#185761)
2026-04-29 [email protected] Update merge semantics logic to merge sibling nodes (flutter/flutter#183745)
2026-04-29 [email protected] Roll Packages from ba80f8f to cde5b36 (12 revisions) (flutter/flutter#185748)
2026-04-29 [email protected] examples: Remove unused parameters (flutter/flutter#185346)
2026-04-29 [email protected] Update TickerMode.getValuesNotifier to handle initialization during State.dispose (flutter/flutter#185248)
2026-04-29 [email protected] Update triage links (flutter/flutter#185714)
2026-04-29 [email protected] Add support for high contrast and color inversion on Android (flutter/flutter#182263)
2026-04-29 [email protected] Roll Skia from 05251260fda6 to f5c781c083c7 (2 revisions) (flutter/flutter#185743)
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants