Skip to content

[iOS] Migrate DisplayLinkManager to a shared instance#189492

Merged
cbracken merged 4 commits into
flutter:masterfrom
cbracken:ios-displaylink-cleanups
Jul 15, 2026
Merged

[iOS] Migrate DisplayLinkManager to a shared instance#189492
cbracken merged 4 commits into
flutter:masterfrom
cbracken:ios-displaylink-cleanups

Conversation

@cbracken

@cbracken cbracken commented Jul 15, 2026

Copy link
Copy Markdown
Member

Previously, DisplayLinkManager was a stateless static utility class that queried Bundle.main and UIScreen.main directly on every access. Under Swift 6 strict concurrency this produces data-race warnings, and nothing guaranteed these accessors weren't being called from a background engine thread, which would be a UIKit thread-safety violation.

This migrates DisplayLinkManager to a final class shared singleton conforming to @unchecked Sendable. All callers now go through FlutterDisplayLinkManager.shared.

Max refresh rate

maxRefreshRateEnabledOnIPhone is plist-derived and doesn't change during a process's lifetime, so we cache it once at initialisation in an immutable field.

Current refresh rate

displayRefreshRate is a bit more complex: VsyncWaiterIOS::AwaitVSync polls it on every vsync specifically to detect refresh-rate changes at runtime (e.g. Low Power Mode or thermal throttling limiting refresh rate to 60 Hz even on ProMotion devices), so caching it would break that detection. Instead, displayRefreshRate is a computed property backed by a lock-protected cache, following the same pattern already used in ResizeSynchronizer/FlutterRunLoop on macOS.

There's no documented, exhaustive list of everything that can change the device's effective max refresh rate, so we keep the cached value up to date in two ways:

  • by observing the specific triggers we know about (Low Power Mode via NSProcessInfoPowerStateDidChange, thermal throttling via ProcessInfo.thermalStateDidChangeNotification, and screen mode changes via UIScreen.modeDidChangeNotification); and
  • by re-checking on UIApplication.didBecomeActiveNotification as a sort of forcing function in case there's any other trigger we don't explicitly know about.

Background threads now get a cheap, lock-guarded read instead of touching UIScreen.main directly, while the cached value is kept up to date in a couple different ways.

The cache-update code lives in updateCachedDisplayRefreshRate(_:). This is internal since it's used both by the real notification handlers as well as in a unit test I've added to verify the locking/storage behaviour directly rather than requiring us to post real notifications and hope UIScreen.main's reported value happens to change in the test host.

Test updates

FlutterViewControllerTest previously stubbed displayRefreshRate as a class method via OCMockObject mockForClass:. Since it's now a property on the shared instance, those tests now use
OCMPartialMock([FlutterDisplayLinkManager shared]) instead.

Preferred frame rate probing cleanup

Finally, this deletes the CADisplayLink target-selector probing in the old displayRefreshRate.

That code created a throwaway CADisplayLink(target:selector:), set isPaused = true, then read back preferredFramesPerSecond before discarding it.

preferredFramesPerSecond is a property you set to request a rate; its documented default is 0 until something explicitly assigns it, and this link was never added to a run loop, so nothing ever could. The if preferredFPS != 0 branch was therefore unreachable, and the function always fell through to UIScreen.main.maximumFramesPerSecond regardless. As a result, the display link was constructed, queried, and thrown away on every single call for no effect on the result.

It's worth noting that this is a different property from CADisplayLink.timestamp/targetTimestamp, which do have documented first-frame-is-zero semantics and are already guarded independently in VSyncClient.onDisplayLink (see #186457) and again in the C++ code in VsyncWaiterIOS::SnapDuration.

preferredFramesPerSecond's zero-ness here had nothing to do with frame timing, only with the fact that nobody had ever set it. Since this was on the read path for a property that's polled up to ~120 times/second by VsyncWaiterIOS::AwaitVSync, it meant allocating and discarding a QuartzCore object on every vsync for a value that could never differ from a plain UIScreen.main.maximumFramesPerSecond read.

The new code does a single read, at construction, and lets the caching/observation machinery described above keep it current.

Configurable preferred-FPS support is tracked separately in #185759.

Swift exported symbols check

Also adds support for Swift static let properties (prefix _$s) to the (__DATA,__common) section of exported binaries.

Issue: #175879
Issue: #181684

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 July 15, 2026 07:38
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jul 15, 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 Jul 15, 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 refactors DisplayLinkManager into a thread-safe singleton (shared) that caches the display refresh rate and dynamically updates it by observing system notifications. It updates references across the iOS platform code to use this shared instance and introduces comprehensive unit tests. The review feedback suggests minor documentation corrections, using a more idiomatic Swift guard let self pattern in the notification handler, and utilizing ProcessInfo.powerStateDidChangeNotification for consistency in both the source and test files.

Previously, `DisplayLinkManager` was a stateless static utility class
that queried `Bundle.main` and `UIScreen.main` directly on every access.
Under Swift 6 strict concurrency this produces data-race warnings, and
nothing guaranteed these accessors weren't being called from a
background engine thread, which would be a UIKit thread-safety
violation.

This migrates `DisplayLinkManager` to a `final class` shared singleton
conforming to `@unchecked Sendable`. All callers now go through
`FlutterDisplayLinkManager.shared`.

### Max refresh rate

`maxRefreshRateEnabledOnIPhone` is plist-derived and doesn't change
during a process's lifetime, so we cache it once at initialisation in an
immutable field.

### Current refresh rate

`displayRefreshRate` is a bit more complex: `VsyncWaiterIOS::AwaitVSync`
polls it on every vsync specifically to detect refresh-rate changes at
runtime (e.g. Low Power Mode or thermal throttling limiting refresh rate
to 60 Hz even on ProMotion devices), so caching it would break that
detection. Instead, `displayRefreshRate` is a computed property backed
by a lock-protected cache, following the same pattern already used in
`ResizeSynchronizer`/`FlutterRunLoop` on macOS.

There's no documented, exhaustive list of everything that can change
the device's effective max refresh rate, so we keep the cached value up
to date in two ways:

* by observing the specific triggers we know about (Low Power Mode via
  `NSProcessInfoPowerStateDidChange`, thermal throttling via
  `ProcessInfo.thermalStateDidChangeNotification`, and screen mode
  changes via `UIScreen.modeDidChangeNotification`); and
* by re-checking on `UIApplication.didBecomeActiveNotification` as a
  sort of forcing function in case there's any other trigger we don't
  explicitly know about.

Background threads now get a cheap, lock-guarded read instead of
touching `UIScreen.main` directly, while the cached value still tracks
reality within a bounded staleness window.

The cache-update code lives in `updateCachedDisplayRefreshRate(_:)`.
This is `internal` since it's used both by the real notification
handlers as well as in a unit test I've added to verify the
locking/storage behavior directly rather than requiring us to post real
notifications and hope `UIScreen.main`'s reported value happens to
change in the test host.

### Test updates

`FlutterViewControllerTest` previously stubbed `displayRefreshRate` as a
class method via `OCMockObject mockForClass:`. Since it's now a property
on the shared instance, those tests now use
`OCMPartialMock([FlutterDisplayLinkManager shared])` instead.

### Preferred frame rate probing cleanup

Finally, this deletes the `CADisplayLink` target-selector probing in the
old `displayRefreshRate`.

That code created a throwaway `CADisplayLink(target:selector:)`, set
`isPaused = true`, then read back `preferredFramesPerSecond` before
discarding it.

`preferredFramesPerSecond` is a property you set to request a rate; its
documented default is `0` until something explicitly assigns it, and this
link was never added to a run loop, so nothing ever could. The `if
preferredFPS != 0` branch was therefore unreachable, and the function
always fell through to `UIScreen.main.maximumFramesPerSecond`
regardless. As a result, the display link was constructed, queried, and
thrown away on every single call for no effect on the result.

It's worth noting that this is a different property from
`CADisplayLink.timestamp`/`targetTimestamp`, which do have documented
first-frame-is-zero semantics and are already guarded independently in
`VSyncClient.onDisplayLink` (see flutter#186457) and again in the C++
code in `VsyncWaiterIOS::SnapDuration`.

`preferredFramesPerSecond`'s zero-ness here had nothing to do with frame
timing, only with the fact that nobody had ever set it. Since this was
on the read path for a property that's polled up to ~120 times/second by
`VsyncWaiterIOS::AwaitVSync`, it meant allocating and discarding a
`QuartzCore` object on every vsync for a value that could never differ
from a plain `UIScreen.main.maximumFramesPerSecond` read.

The new code does a single read, at construction, and lets the
caching/observation machinery described above keep it current.

Configurable preferred-FPS support is tracked separately in
flutter#185759.

Issue: flutter#175879
Issue: flutter#181684
@cbracken
cbracken force-pushed the ios-displaylink-cleanups branch from ae59254 to 504df89 Compare July 15, 2026 07:56
@cbracken

Copy link
Copy Markdown
Member Author

Review feedback applied for the cases where it made sense.

@cbracken

Copy link
Copy Markdown
Member Author

Video of manual test on simulator:
https://github.com/user-attachments/assets/fc70d111-5851-4e13-ad20-9b56c047a669

Will also upload a video of manual testing on ProMotion device.

@LongCatIsLooong LongCatIsLooong 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.

LGTM with nits. Once we turn on swift 6 language mode I'd assume this is going to stop compiling since the initializer isn't marked @MainActor but it's synchronously accessing UIScreen.main, but this class should be simple enough to migrate.

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.

Any chance these can be turned into swift testing tests? A few tests can be parameterized it looks like.

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.

Mind if I do that in a followup (in which I also want to do the tests for VSyncClient)?

@cbracken
cbracken force-pushed the ios-displaylink-cleanups branch from f5f193c to 57a1b81 Compare July 15, 2026 11:26
@cbracken
cbracken added this pull request to the merge queue Jul 15, 2026
Merged via the queue into flutter:master with commit 7b6f0d4 Jul 15, 2026
197 checks passed
@cbracken
cbracken deleted the ios-displaylink-cleanups branch July 15, 2026 22:41
@cbracken

Copy link
Copy Markdown
Member Author

Here's the physical device ProMotion On/Off recording I promised.

ProMotionOn_ProMotionOff.mov

cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to flutter#189492, which migrated `DisplayLinkManager` to a shared
singleton instance. `VsyncWaiterIOS` picked up `.shared` directly in
three places: once in the constructor when seeding the `FlutterVSyncClient`
and `max_refresh_rate_`, and again on every `AwaitVSync()` poll. That's a
hidden global dependency, and it meant the only way to exercise
`VsyncWaiterIOS` against a non-default refresh rate was to mutate the
real shared instance out from under whatever else might be touching it.

This threads a `FlutterDisplayLinkManager*` through the constructor
instead and stores it on `display_link_manager_`, so all three read
sites go through the injected instance. `PlatformViewIOS::CreateVSyncWaiter()`
is the only production callsite, and now passes `FlutterDisplayLinkManager.shared`
explicitly rather than leaving it implicit. That callsite runs
synchronously on the platform thread during `Shell::CreateShellOnPlatformThread`,
which is the main thread in standard embedder usage, so this doesn't
change anything about `DisplayLinkManager.init()`'s main-thread
assertion on first access.

While adding tests against the injected manager, found that `AwaitVSync()`
had been made `private` by mistake in 722be07 — a stray `private:`
label landed right above it while `SnapDuration` was being added,
silently undoing the "Made public for testing" it's commented with (and
had been since c05f179). Restored it to public.

Added two tests to `VsyncWaiterIOSTest.mm` that construct a
`VsyncWaiterIOS` against an `OCMPartialMock` of
`FlutterDisplayLinkManager.shared`, matching the mocking pattern already
used in `FlutterViewControllerTest.mm`. Both assert against a new
`GetMaxRefreshRateForTesting()` accessor rather than the pre-existing
`GetRefreshRate()`: the latter forwards to `client_.refreshRate`, which
per `VariableRefreshRateReporter`'s contract reports the live *measured*
refresh rate from actual vsync ticks, not the polled ceiling
`AwaitVSync()` tracks. It's seeded from the constructor's `maxRefreshRate`
param (which is why the naive version of the constructor test happened
to pass), but `AwaitVSync()` never touches it — `[client_
setMaxRefreshRate:]` only reconfigures the display link's requested rate
for future frames, so asserting against `GetRefreshRate()` after
`AwaitVSync()` was asserting against a value nothing in this path
updates. `GetMaxRefreshRateForTesting()` exposes `max_refresh_rate_`
directly instead, following the same `GetXForTesting()` convention
already used in `android_shell_holder.h`. One test confirms the
constructor seeds `max_refresh_rate_` from the injected manager, the
other confirms `AwaitVSync()` picks up a refresh-rate change on it.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to flutter#189492, which migrated `DisplayLinkManager` to a shared
singleton instance. `VsyncWaiterIOS` picked up `.shared` directly in
three places: once in the constructor when seeding the `FlutterVSyncClient`
and `max_refresh_rate_`, and again on every `AwaitVSync()` poll. That's a
hidden global dependency, and it meant the only way to exercise
`VsyncWaiterIOS` against a non-default refresh rate was to mutate the
real shared instance out from under whatever else might be touching it.

This threads a `FlutterDisplayLinkManager*` through the constructor
instead and stores it on `display_link_manager_`, so all three read
sites go through the injected instance. `PlatformViewIOS::CreateVSyncWaiter()`
is the only production callsite, and now passes `FlutterDisplayLinkManager.shared`
explicitly rather than leaving it implicit. That callsite runs
synchronously on the platform thread during `Shell::CreateShellOnPlatformThread`,
which is the main thread in standard embedder usage, so this doesn't
change anything about `DisplayLinkManager.init()`'s main-thread
assertion on first access.

While adding tests against the injected manager, found that `AwaitVSync()`
had been made `private` by mistake in 722be07 — a stray `private:`
label landed right above it while `SnapDuration` was being added,
silently undoing the "Made public for testing" it's commented with (and
had been since c05f179). Restored it to public.

Added two tests to `VsyncWaiterIOSTest.mm` that construct a
`VsyncWaiterIOS` against an `OCMPartialMock` of
`FlutterDisplayLinkManager.shared`, matching the mocking pattern already
used in `FlutterViewControllerTest.mm`. Both assert against a new
`GetMaxRefreshRateForTesting()` accessor rather than the pre-existing
`GetRefreshRate()`: the latter forwards to `client_.refreshRate`, which
per `VariableRefreshRateReporter`'s contract reports the live *measured*
refresh rate from actual vsync ticks, not the polled ceiling
`AwaitVSync()` tracks. It's seeded from the constructor's `maxRefreshRate`
param (which is why the naive version of the constructor test happened
to pass), but `AwaitVSync()` never touches it — `[client_
setMaxRefreshRate:]` only reconfigures the display link's requested rate
for future frames, so asserting against `GetRefreshRate()` after
`AwaitVSync()` was asserting against a value nothing in this path
updates. `GetMaxRefreshRateForTesting()` exposes `max_refresh_rate_`
directly instead, following the same `GetXForTesting()` convention
already used in `android_shell_holder.h`. One test confirms the
constructor seeds `max_refresh_rate_` from the injected manager, the
other confirms `AwaitVSync()` picks up a refresh-rate change on it.

`testAwaitVSyncPicksUpRefreshRateChangesFromInjectedDisplayLinkManager`
still failed on a real device run even after the above fix, because of a
second, more subtle issue: re-stubbing `displayRefreshRate` on the same
OCMock partial mock (once with the initial rate, again with the updated
rate before calling `AwaitVSync()`) doesn't override the first stub.
OCMock keeps every recorder registered against a mock and answers with
the first one that matches, so the second `andReturnValue:` call was
silently ignored and `displayRefreshRate` kept returning the initial
value. This is made worse by the constructor itself reading
`displayRefreshRate` twice (once for `FlutterVSyncClient`'s
`maxRefreshRate:` param, once more for `max_refresh_rate_`), which also
rules out modeling this with paired `expect` calls instead of `stub`.
Replaced the two `stub`/`andReturnValue:` calls with a single
`OCMStub(...).andDo(...)` that reads a `__block double` variable on each
invocation, matching the pattern already used for dynamic return values
elsewhere in this codebase (e.g. `FlutterKeyboardManagerTest.mm`). The
test now mutates that variable directly between the "before" and "after"
assertions instead of re-stubbing, which correctly reflects a value that
changes over time.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous VsyncWaiterIOS change: FlutterKeyboardInsetManager read
DisplayLinkManager.shared directly in setUpKeyboardAnimationVsyncClient
when seeding its VSyncClient's isVariableRefreshRateEnabled and
maxRefreshRate, the same hidden-global pattern.

This threads a DisplayLinkManager through the initializer instead and
stores it on a new displayLinkManager property, so that read goes
through the injected instance. FlutterViewController.mm's
performCommonViewControllerInitialization is the only production
callsite, and now passes FlutterDisplayLinkManager.shared explicitly.

FlutterKeyboardInsetManager isn't part of the public framework headers,
so this is a self-contained change: the six construction sites in
FlutterViewControllerTest.mm (none of which stub a specific refresh
rate; they only assert on inset math or vsync-client presence) now pass
FlutterDisplayLinkManager.shared too, preserving existing behaviour.
FakeFlutterKeyboardInsetManager is untouched — it's an independent
NSObject conforming to FlutterKeyboardInsetManagerProtocol rather than a
subclass, so it never depended on this initializer.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the two previous changes in this series: createTouchRateCorrectionVSyncClientIfNeeded
read FlutterDisplayLinkManager.shared directly in three places when
deciding whether to create the touch-rate-correction VSyncClient and
seeding its isVariableRefreshRateEnabled/maxRefreshRate.

FlutterViewController has too many public initializers to add a
constructor parameter without breaking API, unlike VsyncWaiterIOS and
FlutterKeyboardInsetManager. It already had the answer, though:
keyboardInsetManager and accessibilityFeatures are declared as internal
strong properties in FlutterViewController_Internal.h and assigned once
in performCommonViewControllerInitialization, the single method every
public initializer funnels through. This adds displayLinkManager
following that same pattern, seeded from FlutterDisplayLinkManager.shared
at init, and switches the three reads in
createTouchRateCorrectionVSyncClientIfNeeded to self.displayLinkManager.
_keyboardInsetManager's own construction now reuses the same captured
_displayLinkManager ivar instead of a second .shared read.

No test changes needed. The five existing tests that
OCMPartialMock([FlutterDisplayLinkManager shared]) construct that mock
before constructing the view controller, and OCMPartialMock swizzles the
live singleton's dispatch table in place rather than replacing the
object, so self.displayLinkManager — captured at construction time —
still points at the same (now-mocked) instance those tests stub.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous three changes in this series: FlutterMetalLayer read
FlutterDisplayLinkManager.shared directly in four places when seeding
and re-seeding its own CADisplayLink's preferred frame rate range.

FlutterMetalLayer can't take a constructor parameter at all: it's a
CALayer subclass, and every instance is created implicitly by UIKit via
+layerClass returning [FlutterMetalLayer class] (see
rendering_api_selection.mm and the various +layerClass overrides), which
always calls the parameterless -init. There's no callsite anywhere in
the tree that does [[FlutterMetalLayer alloc] init] directly, so unlike
VsyncWaiterIOS/FlutterKeyboardInsetManager there's no initializer to
extend, and unlike FlutterViewController there's no
common-initialization method to fall back on either — just -init
itself.

This adds a _displayLinkManager ivar, seeded from
FlutterDisplayLinkManager.shared at the top of -init, and switches the
four reads (the initial setMaxRefreshRate:forceMax: call, the
maxRefreshRateEnabledOnIPhone guard, and the two re-seeds in
onDisplayLink: and presentOnMainThread:) to read from it instead.

Deliberately did not add this as a property on the class, unlike
displayLinkManager on FlutterViewController. FlutterMetalLayer.h's
header comment states properties and methods "must exactly match those
in the CAMetalLayer interface declaration" — it deliberately masquerades
as CAMetalLayer (see -isKindOfClass:) for drop-in compatibility, so
growing its public-ish surface with an unrelated property would violate
that. The ivar lives in the private class extension in the .mm instead,
matching _preferredDevice and _drawableSize alongside it.

No test coverage previously existed for the DisplayLinkManager-derived
refresh rate logic in FlutterMetalLayerTest.mm, so there's nothing to
update there.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous VsyncWaiterIOS change: FlutterKeyboardInsetManager read
DisplayLinkManager.shared directly in setUpKeyboardAnimationVsyncClient
when seeding its VSyncClient's isVariableRefreshRateEnabled and
maxRefreshRate, the same hidden-global pattern.

This threads a DisplayLinkManager through the initializer instead and
stores it on a new displayLinkManager property, so that read goes
through the injected instance. FlutterViewController.mm's
performCommonViewControllerInitialization is the only production
callsite, and now passes FlutterDisplayLinkManager.shared explicitly.

FlutterKeyboardInsetManager isn't part of the public framework headers,
so this is a self-contained change: the six construction sites in
FlutterViewControllerTest.mm (none of which stub a specific refresh
rate; they only assert on inset math or vsync-client presence) now pass
FlutterDisplayLinkManager.shared too, preserving existing behaviour.
FakeFlutterKeyboardInsetManager is untouched — it's an independent
NSObject conforming to FlutterKeyboardInsetManagerProtocol rather than a
subclass, so it never depended on this initializer.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the two previous changes in this series: createTouchRateCorrectionVSyncClientIfNeeded
read FlutterDisplayLinkManager.shared directly in three places when
deciding whether to create the touch-rate-correction VSyncClient and
seeding its isVariableRefreshRateEnabled/maxRefreshRate.

FlutterViewController has too many public initializers to add a
constructor parameter without breaking API, unlike VsyncWaiterIOS and
FlutterKeyboardInsetManager. It already had the answer, though:
keyboardInsetManager and accessibilityFeatures are declared as internal
strong properties in FlutterViewController_Internal.h and assigned once
in performCommonViewControllerInitialization, the single method every
public initializer funnels through. This adds displayLinkManager
following that same pattern, seeded from FlutterDisplayLinkManager.shared
at init, and switches the three reads in
createTouchRateCorrectionVSyncClientIfNeeded to self.displayLinkManager.
_keyboardInsetManager's own construction now reuses the same captured
_displayLinkManager ivar instead of a second .shared read.

No test changes needed. The five existing tests that
OCMPartialMock([FlutterDisplayLinkManager shared]) construct that mock
before constructing the view controller, and OCMPartialMock swizzles the
live singleton's dispatch table in place rather than replacing the
object, so self.displayLinkManager — captured at construction time —
still points at the same (now-mocked) instance those tests stub.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous three changes in this series: FlutterMetalLayer read
FlutterDisplayLinkManager.shared directly in four places when seeding
and re-seeding its own CADisplayLink's preferred frame rate range.

FlutterMetalLayer can't take a constructor parameter at all: it's a
CALayer subclass, and every instance is created implicitly by UIKit via
+layerClass returning [FlutterMetalLayer class] (see
rendering_api_selection.mm and the various +layerClass overrides), which
always calls the parameterless -init. There's no callsite anywhere in
the tree that does [[FlutterMetalLayer alloc] init] directly, so unlike
VsyncWaiterIOS/FlutterKeyboardInsetManager there's no initializer to
extend, and unlike FlutterViewController there's no
common-initialization method to fall back on either — just -init
itself.

This adds a _displayLinkManager ivar, seeded from
FlutterDisplayLinkManager.shared at the top of -init, and switches the
four reads (the initial setMaxRefreshRate:forceMax: call, the
maxRefreshRateEnabledOnIPhone guard, and the two re-seeds in
onDisplayLink: and presentOnMainThread:) to read from it instead.

Deliberately did not add this as a property on the class, unlike
displayLinkManager on FlutterViewController. FlutterMetalLayer.h's
header comment states properties and methods "must exactly match those
in the CAMetalLayer interface declaration" — it deliberately masquerades
as CAMetalLayer (see -isKindOfClass:) for drop-in compatibility, so
growing its public-ish surface with an unrelated property would violate
that. The ivar lives in the private class extension in the .mm instead,
matching _preferredDevice and _drawableSize alongside it.

No test coverage previously existed for the DisplayLinkManager-derived
refresh rate logic in FlutterMetalLayerTest.mm, so there's nothing to
update there.

Issue: flutter#175879
Issue: flutter#181684
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Jul 18, 2026
Roll Flutter from fc1ad955f164 to 8005793c3562 (40 revisions)

flutter/flutter@fc1ad95...8005793

2026-07-18 [email protected] Remove `LineContents ` experimental AA line shader that is no longer used (flutter/flutter#189619)
2026-07-18 [email protected] Roll Skia from 9468e96cc40f to ba90f98535de (8 revisions) (flutter/flutter#189680)
2026-07-17 [email protected] Roll Dart SDK from 33e4b71e984b to 666e1e2133b7 (2 revisions) (flutter/flutter#189673)
2026-07-17 [email protected] [iOS] Fix potential use-after-free in a11y bridge channel handler (flutter/flutter#189637)
2026-07-17 [email protected] Migrate dev/tools skill validation to new dart_skills_lint API (flutter/flutter#189626)
2026-07-17 [email protected] Add validation for required fields during xcodebuild (flutter/flutter#187772)
2026-07-17 [email protected] Upgrade android_hardware_smoke_test CI to run instrumented tests (flutter/flutter#189390)
2026-07-17 [email protected] Roll Dart SDK from 0867cb1897b5 to 33e4b71e984b (1 revision) (flutter/flutter#189661)
2026-07-17 [email protected] Roll Skia from 702c3e790232 to 9468e96cc40f (1 revision) (flutter/flutter#189660)
2026-07-17 [email protected] Roll Packages from 9f95026 to 4fdc766 (11 revisions) (flutter/flutter#189659)
2026-07-17 [email protected] Roll Skia from 2e4a3ae035cd to 702c3e790232 (1 revision) (flutter/flutter#189655)
2026-07-17 [email protected] [macOS] Add FlutterPluginRegistrar.valuePublished(byPlugin:) just like iOS (flutter/flutter#189614)
2026-07-17 [email protected] Roll Dart SDK from c69d138c9646 to 0867cb1897b5 (2 revisions) (flutter/flutter#189649)
2026-07-17 [email protected] Roll Skia from 4f5aca109c87 to 2e4a3ae035cd (3 revisions) (flutter/flutter#189646)
2026-07-17 [email protected] [iOS] Fix missing nil checks and improve SemanticsObject bridge API (flutter/flutter#189630)
2026-07-17 [email protected] Roll Skia from 37c5e6b26aee to 4f5aca109c87 (19 revisions) (flutter/flutter#189638)
2026-07-17 [email protected] Roll Fuchsia Linux SDK from lLFbh5kFWbUGgC9Ek... to NL8xtzr8cxr5E8r8E... (flutter/flutter#189632)
2026-07-17 [email protected] Add blendMode parameter to RawImage and RenderImage (flutter/flutter#185938)
2026-07-17 [email protected] [web] Cache WASM network requests in flutter test (flutter/flutter#189623)
2026-07-17 [email protected] android_hardware_smoke_tests: Apply semantic line breaks to readme (flutter/flutter#189613)
2026-07-16 [email protected] ci(github-actions): resolve zizmor github-env findings in composite flutter actions (flutter/flutter#189602)
2026-07-16 [email protected] Remove obsolete packages analysis flag (flutter/flutter#189525)
2026-07-16 [email protected] Roll Dart SDK from e24870ff15bc to c69d138c9646 (2 revisions) (flutter/flutter#189597)
2026-07-16 [email protected] [fuchsia][iwyu] Remove transitive include of fuchsia.input.report. (flutter/flutter#188891)
2026-07-16 [email protected] Remove unnecessary value key hack in `key.dart`  (flutter/flutter#189291)
2026-07-16 [email protected] Roll Dart SDK from 81306a2ed317 to e24870ff15bc (1 revision) (flutter/flutter#189569)
2026-07-16 [email protected] [Impeller] Fix atlas growth test (flutter/flutter#189533)
2026-07-16 [email protected] Roll Dart SDK from d402ff7c9c84 to 81306a2ed317 (4 revisions) (flutter/flutter#189558)
2026-07-16 [email protected] Move shared darwin plugin tests into own builder (flutter/flutter#189411)
2026-07-16 [email protected] Roll Skia from ab2410bc857c to 37c5e6b26aee (19 revisions) (flutter/flutter#189549)
2026-07-16 [email protected] [ios] Fix //flutter:unittests build for physical devices (flutter/flutter#189543)
2026-07-16 [email protected] Fix lower DragTarget not being recognized in overlapping targets (flutter/flutter#188979)
2026-07-16 [email protected] Relocate cupertino samples that were under widgets/ (flutter/flutter#188876)
2026-07-15 [email protected] [iOS] Migrate DisplayLinkManager to a shared instance (flutter/flutter#189492)
2026-07-15 [email protected] [Impeller] Call glfwTerminate during global test environment teardown if a playground test called glfwInit (flutter/flutter#189523)
2026-07-15 [email protected] Roll pub packages (flutter/flutter#189515)
2026-07-15 [email protected] Roll Packages from ad2eab1 to 9f95026 (8 revisions) (flutter/flutter#189509)
2026-07-15 [email protected] add `@nonVirtual` to `RenderObject.attached`, fix `WidgetTester.hasRunningAnimations` (flutter/flutter#186832)
2026-07-15 [email protected] Roll vulkan-deps to 0582f446e54a (flutter/flutter#188524)
2026-07-15 [email protected] Roll Dart SDK from 0c408ff6dce9 to d402ff7c9c84 (1 revision) (flutter/flutter#189497)

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 [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.
...
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 19, 2026
Follow-up to flutter#189492, which migrated `DisplayLinkManager` to a shared
singleton instance. `VsyncWaiterIOS` picked up `.shared` directly in
three places: once in the constructor when seeding the `FlutterVSyncClient`
and `max_refresh_rate_`, and again on every `AwaitVSync()` poll. That's a
hidden global dependency, and it meant the only way to exercise
`VsyncWaiterIOS` against a non-default refresh rate was to mutate the
real shared instance out from under whatever else might be touching it.

This threads a `FlutterDisplayLinkManager*` through the constructor
instead and stores it on `display_link_manager_`, so all three read
sites go through the injected instance. `PlatformViewIOS::CreateVSyncWaiter()`
is the only production callsite, and now passes `FlutterDisplayLinkManager.shared`
explicitly rather than leaving it implicit. That callsite runs
synchronously on the platform thread during `Shell::CreateShellOnPlatformThread`,
which is the main thread in standard embedder usage, so this doesn't
change anything about `DisplayLinkManager.init()`'s main-thread
assertion on first access.

While adding tests against the injected manager, found that `AwaitVSync()`
had been made `private` by mistake in 722be07 — a stray `private:`
label landed right above it while `SnapDuration` was being added,
silently undoing the "Made public for testing" it's commented with (and
had been since c05f179). Restored it to public.

Added two tests to `VsyncWaiterIOSTest.mm` that construct a
`VsyncWaiterIOS` against an `OCMPartialMock` of
`FlutterDisplayLinkManager.shared`, matching the mocking pattern already
used in `FlutterViewControllerTest.mm`. Both assert against a new
`GetMaxRefreshRateForTesting()` accessor rather than the pre-existing
`GetRefreshRate()`: the latter forwards to `client_.refreshRate`, which
per `VariableRefreshRateReporter`'s contract reports the live *measured*
refresh rate from actual vsync ticks, not the polled ceiling
`AwaitVSync()` tracks. It's seeded from the constructor's `maxRefreshRate`
param (which is why the naive version of the constructor test happened
to pass), but `AwaitVSync()` never touches it — `[client_
setMaxRefreshRate:]` only reconfigures the display link's requested rate
for future frames, so asserting against `GetRefreshRate()` after
`AwaitVSync()` was asserting against a value nothing in this path
updates. `GetMaxRefreshRateForTesting()` exposes `max_refresh_rate_`
directly instead, following the same `GetXForTesting()` convention
already used in `android_shell_holder.h`. One test confirms the
constructor seeds `max_refresh_rate_` from the injected manager, the
other confirms `AwaitVSync()` picks up a refresh-rate change on it.

`testAwaitVSyncPicksUpRefreshRateChangesFromInjectedDisplayLinkManager`
still failed on a real device run even after the above fix, because of a
second, more subtle issue: re-stubbing `displayRefreshRate` on the same
OCMock partial mock (once with the initial rate, again with the updated
rate before calling `AwaitVSync()`) doesn't override the first stub.
OCMock keeps every recorder registered against a mock and answers with
the first one that matches, so the second `andReturnValue:` call was
silently ignored and `displayRefreshRate` kept returning the initial
value. This is made worse by the constructor itself reading
`displayRefreshRate` twice (once for `FlutterVSyncClient`'s
`maxRefreshRate:` param, once more for `max_refresh_rate_`), which also
rules out modeling this with paired `expect` calls instead of `stub`.
Replaced the two `stub`/`andReturnValue:` calls with a single
`OCMStub(...).andDo(...)` that reads a `__block double` variable on each
invocation, matching the pattern already used for dynamic return values
elsewhere in this codebase (e.g. `FlutterKeyboardManagerTest.mm`). The
test now mutates that variable directly between the "before" and "after"
assertions instead of re-stubbing, which correctly reflects a value that
changes over time.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 19, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous VsyncWaiterIOS change: FlutterKeyboardInsetManager read
DisplayLinkManager.shared directly in setUpKeyboardAnimationVsyncClient
when seeding its VSyncClient's isVariableRefreshRateEnabled and
maxRefreshRate, the same hidden-global pattern.

This threads a DisplayLinkManager through the initializer instead and
stores it on a new displayLinkManager property, so that read goes
through the injected instance. FlutterViewController.mm's
performCommonViewControllerInitialization is the only production
callsite, and now passes FlutterDisplayLinkManager.shared explicitly.

FlutterKeyboardInsetManager isn't part of the public framework headers,
so this is a self-contained change: the six construction sites in
FlutterViewControllerTest.mm (none of which stub a specific refresh
rate; they only assert on inset math or vsync-client presence) now pass
FlutterDisplayLinkManager.shared too, preserving existing behaviour.
FakeFlutterKeyboardInsetManager is untouched — it's an independent
NSObject conforming to FlutterKeyboardInsetManagerProtocol rather than a
subclass, so it never depended on this initializer.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 19, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the previous three changes in this series: FlutterMetalLayer read
FlutterDisplayLinkManager.shared directly in four places when seeding
and re-seeding its own CADisplayLink's preferred frame rate range.

FlutterMetalLayer can't take a constructor parameter at all: it's a
CALayer subclass, and every instance is created implicitly by UIKit via
+layerClass returning [FlutterMetalLayer class] (see
rendering_api_selection.mm and the various +layerClass overrides), which
always calls the parameterless -init. There's no callsite anywhere in
the tree that does [[FlutterMetalLayer alloc] init] directly, so unlike
VsyncWaiterIOS/FlutterKeyboardInsetManager there's no initializer to
extend, and unlike FlutterViewController there's no
common-initialization method to fall back on either — just -init
itself.

This adds a _displayLinkManager ivar, seeded from
FlutterDisplayLinkManager.shared at the top of -init, and switches the
four reads (the initial setMaxRefreshRate:forceMax: call, the
maxRefreshRateEnabledOnIPhone guard, and the two re-seeds in
onDisplayLink: and presentOnMainThread:) to read from it instead.

Deliberately did not add this as a property on the class, unlike
displayLinkManager on FlutterViewController. FlutterMetalLayer.h's
header comment states properties and methods "must exactly match those
in the CAMetalLayer interface declaration" — it deliberately masquerades
as CAMetalLayer (see -isKindOfClass:) for drop-in compatibility, so
growing its public-ish surface with an unrelated property would violate
that. The ivar lives in the private class extension in the .mm instead,
matching _preferredDevice and _drawableSize alongside it.

No test coverage previously existed for the DisplayLinkManager-derived
refresh rate logic in FlutterMetalLayerTest.mm, so there's nothing to
update there.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 19, 2026
Follow-up to the same DisplayLinkManager singleton migration (flutter#189492)
as the two previous changes in this series: createTouchRateCorrectionVSyncClientIfNeeded
read FlutterDisplayLinkManager.shared directly in three places when
deciding whether to create the touch-rate-correction VSyncClient and
seeding its isVariableRefreshRateEnabled/maxRefreshRate.

FlutterViewController has too many public initializers to add a
constructor parameter without breaking API, unlike VsyncWaiterIOS and
FlutterKeyboardInsetManager. It already had the answer, though:
keyboardInsetManager and accessibilityFeatures are declared as internal
strong properties in FlutterViewController_Internal.h and assigned once
in performCommonViewControllerInitialization, the single method every
public initializer funnels through. This adds displayLinkManager
following that same pattern, seeded from FlutterDisplayLinkManager.shared
at init, and switches the three reads in
createTouchRateCorrectionVSyncClientIfNeeded to self.displayLinkManager.
_keyboardInsetManager's own construction now reuses the same captured
_displayLinkManager ivar instead of a second .shared read.

No test changes needed. The five existing tests that
OCMPartialMock([FlutterDisplayLinkManager shared]) construct that mock
before constructing the view controller, and OCMPartialMock swizzles the
live singleton's dispatch table in place rather than replacing the
object, so self.displayLinkManager — captured at construction time —
still points at the same (now-mocked) instance those tests stub.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 20, 2026
This is a follow-up to flutter#189492, which migrated `DisplayLinkManager` to a
shared singleton instance. `VsyncWaiterIOS` was using `.shared` directly
in three places internally, which is effectively using a hidden global,
and it means the only way to exercise `VsyncWaiterIOS` against a
non-default refresh rate was to mutate the real shared instance out from
under whatever else might be touching it.

This wires a `FlutterDisplayLinkManager*` through the constructor
instead and stores a private reference.

I've also added two tests, which required making `AwaitVSync()`
accessible again; I accidentally marked it private in flutter#186935
(722be07) when adding SnapDuration (despite my comment saying it was
made public for testing), so restored it to public.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 20, 2026
This is a follow-up to flutter#189492, which migrated `DisplayLinkManager` to a
shared singleton instance. `VsyncWaiterIOS` was using `.shared` directly
in three places internally, which is effectively using a hidden global,
and it means the only way to exercise `VsyncWaiterIOS` against a
non-default refresh rate was to mutate the real shared instance out from
under whatever else might be touching it.

This wires a `FlutterDisplayLinkManager*` through the constructor
instead and stores a private reference.

I've also added two tests, which required making `AwaitVSync()`
accessible again; I accidentally marked it private in flutter#186935
(722be07) when adding SnapDuration (despite my comment saying it was
made public for testing), so restored it to public.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 21, 2026
This is a follow-up to the DisplayLinkManager singleton migration
(flutter#189492). `FlutterKeyboardInsetManager` was reading
`DisplayLinkManager.shared` directly in `setUpKeyboardAnimationVsyncClient`
when passing its VSyncClient's isVariableRefreshRateEnabled and
maxRefreshRate, which is effectively using a global.

This wires a DisplayLinkManager through the initializer instead and
stores it in an instance var, so that read goes through the injected
instance. 

I didn't need to touch FakeFlutterKeyboardInsetManager. It's an
independent NSObject that conforms to
FlutterKeyboardInsetManagerProtocol and not a subclass, so it never
depended on this initializer.

Issue: flutter#175879
Issue: flutter#181684
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 21, 2026
Follow-up to the DisplayLinkManager singleton migration (flutter#189492).
FlutterMetalLayer read FlutterDisplayLinkManager.shared directly in four
places when seeding and re-seeding its own CADisplayLink's preferred
frame rate range.

FlutterMetalLayer can't take a constructor parameter since it's a
CALayer subclass, and every instance is created implicitly by UIKit via
`+layerClass` returning `[FlutterMetalLayer class]` (see
rendering_api_selection.mm and the various +layerClass overrides), which
always calls the parameterless `-init`. There's no callsite anywhere in
the tree that does `[[FlutterMetalLayer alloc] init]` directly, so unlike
VsyncWaiterIOS/FlutterKeyboardInsetManager there's no initializer to
extend, and no common-initialization method to fall back on; just -init
itself.

This adds a _displayLinkManager ivar, which we wire up from
FlutterDisplayLinkManager.shared at the top of -init, and switches the
four reads (the initial setMaxRefreshRate:forceMax: call, the
maxRefreshRateEnabledOnIPhone guard, and the two re-seeds in
onDisplayLink: and presentOnMainThread:) to read from it instead.

I deliberately didn't add this as a property on the class:
FlutterMetalLayer.h's header comment states properties and methods "must
exactly match those in the CAMetalLayer interface declaration" — it
deliberately masquerades as CAMetalLayer (which you can see in
-isKindOfClass:), so there's no point in increasing its API surface with
this. Added the ivar in a private class extension alongside the existing
_preferredDevice and _drawableSize.

No semantic change, just a refactoring, so no change to the tests.

Issue: flutter#175879
Issue: flutter#181684
pull Bot pushed a commit to soloinovator/flutter that referenced this pull request Jul 21, 2026
This is a follow-up to flutter#189492, which migrated `DisplayLinkManager` to a
shared singleton instance. `VsyncWaiterIOS` was using `.shared` directly
in three places internally, which is effectively using a hidden global,
and it means the only way to exercise `VsyncWaiterIOS` against a
non-default refresh rate was to mutate the real shared instance out from
under whatever else might be touching it.

This wires a `FlutterDisplayLinkManager*` through the constructor
instead and stores a private reference.

I've also added two tests, which required making `AwaitVSync()`
accessible again; I accidentally marked it private in flutter#186935
(722be07) when adding SnapDuration (despite my comment saying it was
made public for testing), so restored it to public.

Issue: flutter#175879
Issue: flutter#181684

<!--
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
pull Bot pushed a commit to edisplay/flutter that referenced this pull request Jul 21, 2026
…tter#189751)

This is a follow-up to the DisplayLinkManager singleton migration
(flutter#189492). `FlutterKeyboardInsetManager` was reading
`DisplayLinkManager.shared` directly in
`setUpKeyboardAnimationVsyncClient` when passing its VSyncClient's
isVariableRefreshRateEnabled and maxRefreshRate, which is effectively
using a global.

This wires a DisplayLinkManager through the initializer instead and
stores it in an instance var, so that read goes through the injected
instance.

I didn't need to touch FakeFlutterKeyboardInsetManager. It's an
independent NSObject that conforms to
FlutterKeyboardInsetManagerProtocol and not a subclass, so it never
depended on this initializer.

This is a refactoring so introduces no semantic changes. I've updated
tests to use the new pattern but there's no new functionality to test.

Issue: flutter#175879
Issue: flutter#181684

<!--
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
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 21, 2026
Follow-up to DisplayLinkManager singleton migration (flutter#189492).
createTouchRateCorrectionVSyncClientIfNeeded read
FlutterDisplayLinkManager.shared directly in three places when deciding
whether to create the touch-rate-correction VSyncClient and seeding its
isVariableRefreshRateEnabled/maxRefreshRate.

FlutterViewController has too many public initializers to add a
constructor parameter without breaking API. However, we can wire this up
similarly to how we declare keyboardInsetManager and
accessibilityFeatures as internal strong properties in
FlutterViewController_Internal.h and assign in
performCommonViewControllerInitialization. We now set
FlutterDisplayLinkManager.shared at init, and migrate the existing uses
of the static shared singleton to reads of this property.

No changes to tests because there are no semantic changes; this is
covered by existing tests.

Issue: flutter#175879
Issue: flutter#181684
pull Bot pushed a commit to Klomgor/flutter that referenced this pull request Jul 21, 2026
Follow-up to the DisplayLinkManager singleton migration (flutter#189492).
FlutterMetalLayer read FlutterDisplayLinkManager.shared directly in four
places when seeding and re-seeding its own CADisplayLink's preferred
frame rate range.

FlutterMetalLayer can't take a constructor parameter since it's a
CALayer subclass, and every instance is created implicitly by UIKit via
`+layerClass` returning `[FlutterMetalLayer class]` (see
rendering_api_selection.mm and the various +layerClass overrides), which
always calls the parameterless `-init`. There's no callsite anywhere in
the tree that does `[[FlutterMetalLayer alloc] init]` directly, so
unlike VsyncWaiterIOS/FlutterKeyboardInsetManager there's no initializer
to extend, and no common-initialization method to fall back on; just
-init itself.

This adds a _displayLinkManager ivar, which we wire up from
FlutterDisplayLinkManager.shared at the top of -init, and switches the
four reads (the initial setMaxRefreshRate:forceMax: call, the
maxRefreshRateEnabledOnIPhone guard, and the two re-seeds in
onDisplayLink: and presentOnMainThread:) to read from it instead.

I deliberately didn't add this as a property on the class:
FlutterMetalLayer.h's header comment states properties and methods "must
exactly match those in the CAMetalLayer interface declaration" — it
deliberately masquerades as CAMetalLayer (which you can see in
-isKindOfClass:), so there's no point in increasing its API surface with
this. Added the ivar in a private class extension alongside the existing
_preferredDevice and _drawableSize.

No semantic change, just a refactoring, so no change to the tests.

Issue: flutter#175879
Issue: flutter#181684

<!--
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
bkonyi pushed a commit to Vonarian/flutter that referenced this pull request Jul 22, 2026
…89764)

Follow-up to DisplayLinkManager singleton migration (flutter#189492).
createTouchRateCorrectionVSyncClientIfNeeded read
FlutterDisplayLinkManager.shared directly in three places when deciding
whether to create the touch-rate-correction VSyncClient and seeding its
isVariableRefreshRateEnabled/maxRefreshRate.

FlutterViewController has too many public initializers to add a
constructor parameter without breaking API. However, we can wire this up
similarly to how we declare keyboardInsetManager and
accessibilityFeatures as internal strong properties in
FlutterViewController_Internal.h and assign in
performCommonViewControllerInitialization. We now set
FlutterDisplayLinkManager.shared at init, and migrate the existing uses
of the static shared singleton to reads of this property.

No changes to tests because there are no semantic changes; this is
covered by existing tests.

Issue: flutter#175879
Issue: flutter#181684

<!--
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.

3 participants