Skip to content

[iOS] Migrate VSyncClient and DisplayLinkManager to Swift#187001

Merged
cbracken merged 1 commit into
flutter:masterfrom
cbracken:swift-vsyncclient
May 26, 2026
Merged

[iOS] Migrate VSyncClient and DisplayLinkManager to Swift#187001
cbracken merged 1 commit into
flutter:masterfrom
cbracken:swift-vsyncclient

Conversation

@cbracken

Copy link
Copy Markdown
Member

This also splits DisplayLinkManager to its own file.

(void) casts needed to be added to OCMock usages of DisplayLinkManager because by default Swift warns on unused results and overall, it's safer to keep that behaviour than marking the method results as okay to discard.

This renames the setMaxRefreshRate refreshRate parameter to requestedRate since it was shadowing self.refreshRate. Makes the code a bit more readable and a bit more future-proof.

Finally, this also makes refreshRate a computed property, which ensures that any callers users get a rate within a valid range. It also makes the code in setMaxRefreshRate a little more readable.

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 May 23, 2026 11:47
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 23, 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 May 23, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

///
/// - Parameter requestedRate: The target maximum refresh rate in Hertz.
@objc
public func setMaxRefreshRate(_ requestedRate: Double) {

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.

Difference: I've renamed refreshRate here to requestedRate since it was shadowing self.refreshRate. This eliminates that potential trap for future changes.

/// this property falls back to `defaultRefreshRate` (60Hz).
@objc
public var refreshRate: Double {
return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate

@cbracken cbracken May 23, 2026

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.

Difference: Previously, this was done while setting refreshRate in onDisplayLink() but moving it here makes that code a bit more readable and ensures that we're always returning a valid value here even if onDisplayLink() has never been called. Further, it centralises the logic so that if we ever use this elsewhere, we get a safe rate automatically rather than having to apply this check everywhere.

@cbracken

Copy link
Copy Markdown
Member Author

/gemini review

@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 migrates the iOS VSync and display link management from Objective-C to Swift, introducing VSyncClient.swift and DisplayLinkManager.swift while removing the legacy FlutterVSyncClient files. Feedback identifies a potential retain cycle in VSyncClient due to CADisplayLink strongly retaining its target and suggests simplifying the displayRefreshRate property in DisplayLinkManager by removing redundant logic.

Comment on lines +10 to +73
public class VSyncClient: NSObject {
private static let defaultRefreshRate: Double = 60.0

private let isVariableRefreshRateEnabled: Bool
private let callback: (CFTimeInterval, CFTimeInterval) -> Void

/// The display link used to coordinate vsync callbacks.
@objc
internal private(set) var displayLink: CADisplayLink?

private var _refreshRate: Double = defaultRefreshRate

/// The current display refresh rate in Hertz, rounded to the nearest integer value.
///
/// This value is calculated during each vsync callback as the inverse of the frame duration (the
/// time between the current frame and the target next frame). The resulting frequency is rounded
/// to the nearest whole number to smooth out minor hardware timestamp variations.
///
/// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition),
/// this property falls back to `defaultRefreshRate` (60Hz).
@objc
public var refreshRate: Double {
return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate
}

/// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync
/// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync
/// callback continuously.
@objc
public var allowPauseAfterVsync: Bool = true

/// Initializes the vsync client.
///
/// - Parameters:
/// - taskRunner: The task runner to use for posting tasks.
/// - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled.
/// - maxRefreshRate: The maximum refresh rate to configure the display link with.
/// - callback: The callback to invoke when a vsync signal is received.
@objc
public init(
taskRunner: TaskRunner,
isVariableRefreshRateEnabled: Bool,
maxRefreshRate: Double,
callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void
) {
self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled
self._refreshRate = maxRefreshRate
self.callback = callback

super.init()

let link = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:)))
link.isPaused = true
self.displayLink = link

setMaxRefreshRate(maxRefreshRate)

// Capture a weak reference to self to ensure we don't add the display link to the run loop if
// the client has already been deallocated.
taskRunner.postTask { [weak self] in
guard let self else { return }
self.displayLink?.add(to: .current, forMode: .common)
}
}

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.

high

The current implementation of VSyncClient has a retain cycle. CADisplayLink strongly retains its target, and VSyncClient strongly retains the displayLink. Because of this cycle, deinit will never be called, and the memory will leak unless invalidate() is called manually.

In Swift, this is typically solved by using a private proxy object as the target of the CADisplayLink, which holds a weak reference back to the client.

public class VSyncClient: NSObject {
  private static let defaultRefreshRate: Double = 60.0

  private let isVariableRefreshRateEnabled: Bool
  private let callback: (CFTimeInterval, CFTimeInterval) -> Void

  /// The display link used to coordinate vsync callbacks.
  @objc
  internal private(set) var displayLink: CADisplayLink?

  private var displayLinkProxy: DisplayLinkProxy?
  private var _refreshRate: Double = defaultRefreshRate

  /// The current display refresh rate in Hertz, rounded to the nearest integer value.
  ///
  /// This value is calculated during each vsync callback as the inverse of the frame duration (the
  /// time between the current frame and the target next frame). The resulting frequency is rounded
  /// to the nearest whole number to smooth out minor hardware timestamp variations.
  ///
  /// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition),
  /// this property falls back to `defaultRefreshRate` (60Hz).
  @objc
  public var refreshRate: Double {
    return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate
  }

  /// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync
  /// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync
  /// callback continuously.
  @objc
  public var allowPauseAfterVsync: Bool = true

  /// Initializes the vsync client.
  ///
  /// - Parameters:
  ///   - taskRunner: The task runner to use for posting tasks.
  ///   - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled.
  ///   - maxRefreshRate: The maximum refresh rate to configure the display link with.
  ///   - callback: The callback to invoke when a vsync signal is received.
  @objc
  public init(
    taskRunner: TaskRunner,
    isVariableRefreshRateEnabled: Bool,
    maxRefreshRate: Double,
    callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void
  ) {
    self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled
    self._refreshRate = maxRefreshRate
    self.callback = callback

    super.init()

    let proxy = DisplayLinkProxy(target: self)
    self.displayLinkProxy = proxy
    let link = CADisplayLink(target: proxy, selector: #selector(DisplayLinkProxy.onDisplayLink(_:)))
    link.isPaused = true
    self.displayLink = link

    setMaxRefreshRate(maxRefreshRate)

    // Capture a weak reference to self to ensure we don't add the display link to the run loop if
    // the client has already been deallocated.
    taskRunner.postTask { [weak self] in
      guard let self else { return }
      self.displayLink?.add(to: .current, forMode: .common)
    }
  }

  private class DisplayLinkProxy: NSObject {
    weak var target: VSyncClient?
    init(target: VSyncClient) { self.target = target }
    @objc func onDisplayLink(_ link: CADisplayLink) {
      target?.onDisplayLink(link)
    }
  }

@cbracken cbracken May 23, 2026

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.

Yes, the existing Obj-C code also had this retain cycle, and in fact this has existed all the way back to the original pure C++ implementation in the first versions of Flutter.

Because of this cycle, deinit will never be called, and the memory will leak unless invalidate() is called manually.

We do already call VSyncClient.invalidate in all code where we use this class.

That said, I do have a followup that introduces a private DisplayLinkRelay class and fixes this altogether, however this patch is big enough already and this is an existing issue in the code that is safely mitigated at every callsite, so not an issue in production. It is however, a fragile design, which is why I have a fix in a followup.

Followup for reference (it's even got a test): https://github.com/flutter/flutter/compare/master...cbracken:flutter:vsync-weak-relay?expand=1 (specifically the second commit here: dca7b2c)

@cbracken cbracken May 23, 2026

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.

The full history for reference:

Commit 54ee61e (October 24, 2016) — "Migrate vsync away from Mojo services": The strong retain cycle was introduced here when it was first created. We were still using MRC and VSyncClient was a private class inside vsync_waiter_ios.mm. The client retains the CADisplayLink which strongly retains the client as its target. Note that we didn't manually invalidate it here, which was a bug!

Commit 5f95d3b (August 1, 2024) — "Migrate vsync_waiter_ios to ARC": The vsync waiter code was migrated to ARC. The circular reference and therefore the leak are still there.

Commit 6eea11a (April 30, 2026) — "[iOS] Extract FlutterVSyncClient from vsync_waiter_ios": A few weeks ago, I extracted the class to FlutterVSyncClient.mm. I noticed the circular reference and added a manual -[FlutterVSyncClient invalidate] method and specifically pointed out // Break retain cycle in there, where I unregister the display link from the run loop. Cleanup still isn't automatic, but at least it's happening now! This fixed the leak, but the code is still not as future-proof as it should be.

Commit d095fdee (May 2, 2026) (THIS PATCH!) — "[iOS] Migrate VSyncClient and DisplayLinkManager to Swift": This is this patch patch and I'm trying to stick to a 1:1 migration of these two classes Swift. We're still doing exactly the same invalidation as I introduced in the last patch -- just in Swift now.

Commit 83c2a7ff (May 23, 2026) (My upcoming patch I linked above) — "[iOS] Eliminate strong retain cycle from VSyncClient": This is my upcoming patch that introduces DisplayLinkRelay as a weak proxy, which breaks the retain cycle and we no longer need to worry about forgetting to call invalidate manually (though it's still a good idea to keep things 100% deterministic and clear it out when we want to).

I asked Gemini to generate Mermaid graphs of the retain graph before and after the upcoming patch.

Here's the current situation:

classDiagram
    class Owner {
        +FlutterVSyncClient client_
    }
    class VSyncClient {
        +CADisplayLink displayLink
        +onDisplayLink()
    }
    class CADisplayLink {
        +id target
        +SEL selector
    }
    class NSRunLoop {
        +DisplayLink activeLinks
    }

    Owner --> VSyncClient : strong
    VSyncClient --> CADisplayLink : strong (displayLink)
    CADisplayLink --> VSyncClient : strong (target)
    NSRunLoop --> CADisplayLink : strong (when active)

Loading

And here's what it'll look like after my patch:

classDiagram
    class Owner {
        +VSyncClient client_
    }
    class VSyncClient {
        +CADisplayLink displayLink
        +onDisplayLink()
    }
    class CADisplayLink {
        +id target
    }
    class DisplayLinkRelay {
        +weak VSyncClient target
        +onDisplayLink()
    }
    class NSRunLoop {
        +DisplayLink activeLinks
    }

    Owner --> VSyncClient : strong
    VSyncClient --> CADisplayLink : strong (displayLink)
    CADisplayLink --> DisplayLinkRelay : strong (target)
    DisplayLinkRelay ..> VSyncClient : "weak (target)"
    NSRunLoop --> CADisplayLink : strong (when active)

Loading

You might be asking about the strong NSRunLoop ownership. Calling invalidate() instructs the runloop to deregister it. That's why we manually call invalidate() today in all places we use it, but we also do it in deinit() (I added this in the previous commit) just to be 100% that if we forget to call it, it doesn't end up retained.

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.

Thanks for writing this up! I didn't know CADisplayLink retains its target.

@cbracken cbracken May 26, 2026

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 this was news to me too from poking at it a bunch in the previous patches, hence why I added the invalidate() calls.

Discussion on StackOverflow:
https://stackoverflow.com/questions/47368609/definitively-do-you-have-to-invalidate-a-cadisplaylink-when-the-controller-di

This article had a bunch of discussion of general Swift gotchas:
https://medium.com/fueled-engineering/memory-management-in-swift-common-issues-90dd7c08b77

Fortunately, we were never creating bajillions of these guys in the first place, but definitely nice to fix it.

Comment on lines +56 to +80
public static var displayRefreshRate: Double {
// TODO(cbracken): This code is incorrect. https://github.com/flutter/flutter/issues/185759
//
// We create a new CADisplayLink, call `preferredFramesPerSecond` on it, then immediately throw
// it away. As noted below, the default value for `preferredFramesPerSecond` is zero, in which
// case, we just return UIScreen.main.maximumFramesPerSecond in all cases; everything before
// that line can be deleted.
//
// If we intend to support configurable preferred FPS, then we should provide API for it. We
// should delete this code either way.

let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:)))
displayLink.isPaused = true
let preferredFPS = displayLink.preferredFramesPerSecond

// From Docs:
// The default value for preferredFramesPerSecond is 0. When this value is 0, the preferred
// frame rate is equal to the maximum refresh rate of the display, as indicated by the
// maximumFramesPerSecond property.
if preferredFPS != 0 {
return Double(preferredFPS)
}

return Double(UIScreen.main.maximumFramesPerSecond)
}

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

As noted in the TODO and the linked issue, the current implementation of displayRefreshRate is redundant and incorrect. Creating a CADisplayLink just to read its default preferredFramesPerSecond (which is always 0 until set) is inefficient and doesn't provide the intended information. Since the fallback logic already handles returning the maximum refresh rate of the screen, the code creating the display link can be removed to simplify the property.

  public static var displayRefreshRate: Double {
    // TODO(cbracken): This code is incorrect. https://github.com/flutter/flutter/issues/185759
    //
    // If we intend to support configurable preferred FPS, then we should provide API for it.
    // We should delete this code either way.
    return Double(UIScreen.main.maximumFramesPerSecond)
  }

@cbracken cbracken May 23, 2026

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 will be deleted two followups from now.

See this issue from when we last discussed this same feedback:
#185759

This also splits DisplayLinkManager to its own file.

`(void)` casts needed to be added to OCMock usages of DisplayLinkManager
because by default Swift warns on unused results and overall, it's safer
to keep that behaviour than marking the method results as okay to
discard.

This renames the `setMaxRefreshRate` `refreshRate` parameter to
`requestedRate` since it was shadowing `self.refreshRate`. Makes the
code a bit more readable and a bit more future-proof.

Finally, this also makes refreshRate a computed property, which ensures
that any callers users get a rate within a valid range. It also makes
the code in `setMaxRefreshRate` a little more readable.

Issue: flutter#112232
@cbracken cbracken force-pushed the swift-vsyncclient branch from 1427463 to 5632af9 Compare May 23, 2026 12:08
@github-actions github-actions Bot removed the CICD Run CI/CD label May 23, 2026
@cbracken cbracken added the CICD Run CI/CD label May 23, 2026
@cbracken

Copy link
Copy Markdown
Member Author

FYI this is the updated version of #186225 after my reland of the Obj-C-ification of FlutterVSyncClient.

// found in the LICENSE file.

import Foundation
import QuartzCore

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.

these 2 are auto imported by UIKit, so can remove

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.

Oh good call -- thanks for spotting! Since it's small, to avoid the hours sitting in re-run, I'll sneak this into the followup that migrates this to a Swift 6 concurrency compliant shared instance that we can later make injectable to improve testability if that's alright.

/// `true` in the application's `Info.plist`. iPad Pro devices will use 120Hz by default.
///
/// - Note: This class contains only stateless `static` properties and class methods and should not
/// be instantiated.

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.

Will testing be a problem in swift without ocmock?

@cbracken cbracken May 26, 2026

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.

Oh hey I have a followup for exactly this issue :) I can't remember if I linked to it from here but I did a cleanup that makes it a shared instance and swift 6 concurrency safe. That will let us inject it (or a fake).

Comment on lines +10 to +73
public class VSyncClient: NSObject {
private static let defaultRefreshRate: Double = 60.0

private let isVariableRefreshRateEnabled: Bool
private let callback: (CFTimeInterval, CFTimeInterval) -> Void

/// The display link used to coordinate vsync callbacks.
@objc
internal private(set) var displayLink: CADisplayLink?

private var _refreshRate: Double = defaultRefreshRate

/// The current display refresh rate in Hertz, rounded to the nearest integer value.
///
/// This value is calculated during each vsync callback as the inverse of the frame duration (the
/// time between the current frame and the target next frame). The resulting frequency is rounded
/// to the nearest whole number to smooth out minor hardware timestamp variations.
///
/// If the current refresh rate is unknown or invalid (e.g., during startup or a paused transition),
/// this property falls back to `defaultRefreshRate` (60Hz).
@objc
public var refreshRate: Double {
return _refreshRate > 0.0 ? _refreshRate : VSyncClient.defaultRefreshRate
}

/// Default value is `true`. Vsync client will pause vsync callback after receiving a vsync
/// signal. Setting this property to `false` can avoid this and vsync client will trigger vsync
/// callback continuously.
@objc
public var allowPauseAfterVsync: Bool = true

/// Initializes the vsync client.
///
/// - Parameters:
/// - taskRunner: The task runner to use for posting tasks.
/// - isVariableRefreshRateEnabled: Whether variable refresh rate should be enabled.
/// - maxRefreshRate: The maximum refresh rate to configure the display link with.
/// - callback: The callback to invoke when a vsync signal is received.
@objc
public init(
taskRunner: TaskRunner,
isVariableRefreshRateEnabled: Bool,
maxRefreshRate: Double,
callback: @escaping (CFTimeInterval, CFTimeInterval) -> Void
) {
self.isVariableRefreshRateEnabled = isVariableRefreshRateEnabled
self._refreshRate = maxRefreshRate
self.callback = callback

super.init()

let link = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:)))
link.isPaused = true
self.displayLink = link

setMaxRefreshRate(maxRefreshRate)

// Capture a weak reference to self to ensure we don't add the display link to the run loop if
// the client has already been deallocated.
taskRunner.postTask { [weak self] in
guard let self else { return }
self.displayLink?.add(to: .current, forMode: .common)
}
}

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.

Thanks for writing this up! I didn't know CADisplayLink retains its target.

@cbracken cbracken added this pull request to the merge queue May 26, 2026
@cbracken

Copy link
Copy Markdown
Member Author

I'll send the followup shortly that does the migration of DisplayLinkManager to an implementation that makes it fully Swift 6 concurrency compliant, and makes it an instance so it could in theory be injected to tests in future.

Merged via the queue into flutter:master with commit 5bcd096 May 26, 2026
201 checks passed
@cbracken cbracken deleted the swift-vsyncclient branch May 26, 2026 22:30
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request May 27, 2026
flutter/flutter@f3a4b98...c8f2f16

2026-05-27 98614782+auto-submit[bot]@users.noreply.github.com Reverts "[Flutter GPU] Add explicit draw counts (#186639)" (flutter/flutter#187170)
2026-05-27 engine-flutter-autoroll@skia.org Roll Dart SDK from 7dcea971af6b to f3db7b7d9801 (2 revisions) (flutter/flutter#187144)
2026-05-27 bdero@google.com [Flutter GPU] Add explicit draw counts (flutter/flutter#186639)
2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from f9db7748563e to fa944af10f91 (1 revision) (flutter/flutter#187139)
2026-05-27 bdero@google.com [Flutter GPU] Flutter GPU ShaderLibrary in-place reload for shader hot reload (flutter/flutter#186793)
2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from a692cbf38952 to f9db7748563e (8 revisions) (flutter/flutter#187135)
2026-05-27 burak.karahan@mail.ru Use local semantics tester in Material button tests (flutter/flutter#186667)
2026-05-27 rmolivares@renzo-olivares.dev Filter out `a: text input` from `team-framework` PR triage  (flutter/flutter#187129)
2026-05-27 bkonyi@google.com [Engine] Fix silent buffer mismatch bug in FML Win32 CommandLineFromPlatform (flutter/flutter#187120)
2026-05-27 srawlins@google.com examples: Remove unused parameters (flutter/flutter#185819)
2026-05-27 116356835+AbdeMohlbi@users.noreply.github.com Remove another `String.valueOf` (flutter/flutter#186628)
2026-05-27 alex.medinsh@gmail.com Print trace when skipping flavor-specific and platform-specific assets (flutter/flutter#187045)
2026-05-26 adilhanney@disroot.org Fix `InteractiveViewer` memory leak from undisposed `CurvedAnimation`s (flutter/flutter#185826)
2026-05-26 meylis@divine.video Fix AccessibilityBridge startup crash on vendor-modified Android (flutter/flutter#184630)
2026-05-26 keertip@users.noreply.github.com Update data driven fixes docs (flutter/flutter#186217)
2026-05-26 ahmedsameha1@gmail.com Add more 0x0 size tests part10 (flutter/flutter#186201)
2026-05-26 46920873+gabrimatic@users.noreply.github.com Disable spell check for obscured text (flutter/flutter#186329)
2026-05-26 chris@bracken.jp [iOS] Migrate VSyncClient and DisplayLinkManager to Swift (flutter/flutter#187001)
2026-05-26 bkonyi@google.com [flutter_tools, devicelab] Add safety filesystem guard to tests (flutter/flutter#186748)
2026-05-26 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from Itd2Jq_ZIABH2rW7B... to k9EizfEGJO4WwQN9-... (flutter/flutter#187115)
2026-05-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 00e625453c43 to 7dcea971af6b (1 revision) (flutter/flutter#187117)
2026-05-26 rmacnak@google.com Remove use of deprecated copy_trees. (flutter/flutter#187091)
2026-05-26 15619084+vashworth@users.noreply.github.com Use resolved implementation plugins with SwiftPM (flutter/flutter#187097)
2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from 27a819894f7c to a692cbf38952 (3 revisions) (flutter/flutter#187109)
2026-05-26 engine-flutter-autoroll@skia.org Roll Packages from 69cf959 to fc795e5 (4 revisions) (flutter/flutter#187114)
2026-05-26 mvincentong@gmail.com Handle simctl process exceptions during discovery (flutter/flutter#186501)
2026-05-26 mvincentong@gmail.com Clarify precache enabled platforms help (flutter/flutter#186878)
2026-05-26 bkonyi@google.com [tool] Refactor artifacts.dart to use enhanced enums and reduce duplication (flutter/flutter#187063)
2026-05-26 jason-simmons@users.noreply.github.com Build script updates for syncing engine sources and building artifacts on linux-arm64 (flutter/flutter#186917)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC boetger@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
creatorpiyush pushed a commit to creatorpiyush/packages that referenced this pull request Jun 10, 2026
…r#11795)

flutter/flutter@f3a4b98...c8f2f16

2026-05-27 98614782+auto-submit[bot]@users.noreply.github.com Reverts "[Flutter GPU] Add explicit draw counts (#186639)" (flutter/flutter#187170)
2026-05-27 engine-flutter-autoroll@skia.org Roll Dart SDK from 7dcea971af6b to f3db7b7d9801 (2 revisions) (flutter/flutter#187144)
2026-05-27 bdero@google.com [Flutter GPU] Add explicit draw counts (flutter/flutter#186639)
2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from f9db7748563e to fa944af10f91 (1 revision) (flutter/flutter#187139)
2026-05-27 bdero@google.com [Flutter GPU] Flutter GPU ShaderLibrary in-place reload for shader hot reload (flutter/flutter#186793)
2026-05-27 engine-flutter-autoroll@skia.org Roll Skia from a692cbf38952 to f9db7748563e (8 revisions) (flutter/flutter#187135)
2026-05-27 burak.karahan@mail.ru Use local semantics tester in Material button tests (flutter/flutter#186667)
2026-05-27 rmolivares@renzo-olivares.dev Filter out `a: text input` from `team-framework` PR triage  (flutter/flutter#187129)
2026-05-27 bkonyi@google.com [Engine] Fix silent buffer mismatch bug in FML Win32 CommandLineFromPlatform (flutter/flutter#187120)
2026-05-27 srawlins@google.com examples: Remove unused parameters (flutter/flutter#185819)
2026-05-27 116356835+AbdeMohlbi@users.noreply.github.com Remove another `String.valueOf` (flutter/flutter#186628)
2026-05-27 alex.medinsh@gmail.com Print trace when skipping flavor-specific and platform-specific assets (flutter/flutter#187045)
2026-05-26 adilhanney@disroot.org Fix `InteractiveViewer` memory leak from undisposed `CurvedAnimation`s (flutter/flutter#185826)
2026-05-26 meylis@divine.video Fix AccessibilityBridge startup crash on vendor-modified Android (flutter/flutter#184630)
2026-05-26 keertip@users.noreply.github.com Update data driven fixes docs (flutter/flutter#186217)
2026-05-26 ahmedsameha1@gmail.com Add more 0x0 size tests part10 (flutter/flutter#186201)
2026-05-26 46920873+gabrimatic@users.noreply.github.com Disable spell check for obscured text (flutter/flutter#186329)
2026-05-26 chris@bracken.jp [iOS] Migrate VSyncClient and DisplayLinkManager to Swift (flutter/flutter#187001)
2026-05-26 bkonyi@google.com [flutter_tools, devicelab] Add safety filesystem guard to tests (flutter/flutter#186748)
2026-05-26 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from Itd2Jq_ZIABH2rW7B... to k9EizfEGJO4WwQN9-... (flutter/flutter#187115)
2026-05-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 00e625453c43 to 7dcea971af6b (1 revision) (flutter/flutter#187117)
2026-05-26 rmacnak@google.com Remove use of deprecated copy_trees. (flutter/flutter#187091)
2026-05-26 15619084+vashworth@users.noreply.github.com Use resolved implementation plugins with SwiftPM (flutter/flutter#187097)
2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from 27a819894f7c to a692cbf38952 (3 revisions) (flutter/flutter#187109)
2026-05-26 engine-flutter-autoroll@skia.org Roll Packages from 69cf959 to fc795e5 (4 revisions) (flutter/flutter#187114)
2026-05-26 mvincentong@gmail.com Handle simctl process exceptions during discovery (flutter/flutter#186501)
2026-05-26 mvincentong@gmail.com Clarify precache enabled platforms help (flutter/flutter#186878)
2026-05-26 bkonyi@google.com [tool] Refactor artifacts.dart to use enhanced enums and reduce duplication (flutter/flutter#187063)
2026-05-26 jason-simmons@users.noreply.github.com Build script updates for syncing engine sources and building artifacts on linux-arm64 (flutter/flutter#186917)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC boetger@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
via-guy pushed a commit to via-guy/flutter that referenced this pull request Jun 26, 2026
…7001)

This also splits DisplayLinkManager to its own file.

`(void)` casts needed to be added to OCMock usages of DisplayLinkManager
because by default Swift warns on unused results and overall, it's safer
to keep that behaviour than marking the method results as okay to
discard.

This renames the `setMaxRefreshRate` `refreshRate` parameter to
`requestedRate` since it was shadowing `self.refreshRate`. Makes the
code a bit more readable and a bit more future-proof.

Finally, this also makes refreshRate a computed property, which ensures
that any callers users get a rate within a valid range. It also makes
the code in `setMaxRefreshRate` a little more readable.

Issue: flutter#112232

<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->


## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

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](https://developers.google.com/gemini-code-assist/docs/review-github-code).
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.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
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