Skip to content

fix(scroll): recover correct refresh rate after sleep/wake - #1022

Open
ismailcolakk13 wants to merge 1 commit into
Caldis:masterfrom
ismailcolakk13:master
Open

fix(scroll): recover correct refresh rate after sleep/wake#1022
ismailcolakk13 wants to merge 1 commit into
Caldis:masterfrom
ismailcolakk13:master

Conversation

@ismailcolakk13

Copy link
Copy Markdown

fix(scroll): recover correct refresh rate after sleep/wake (late VBL)

Motivation

After waking from sleep, smooth scrolling runs at a low frame rate (e.g. 60 Hz) on a high-refresh display. The only workaround is manually switching the display refresh rate in System Settings and switching back — that forces a fresh CVDisplayLink at the correct rate.

The WindowServer message Bad CurrentVBLDelta for display N is zero, defaulting to 60Hz is visible in the system console immediately after wake. This is the root cause: the display's Vertical Blank (VBL) signal has not been re-established yet, so the OS defaults to 60 Hz. At that moment CVDisplayLink is created with the wrong rate, and the existing rate-verify mechanism (link Hz < display Hz × 0.7) fails silently — because CGDisplayCopyDisplayMode also returns 60 Hz in this transitional state, so both sides of the ratio agree and the check exits early.


What changed

Mos/ScrollCore/ScrollPoster.swift

  • lastLinkHz — records the CVDisplayLink nominal Hz at creation time.
  • verifyRateAndFixIfNeeded gains a second trigger condition: if the display Hz has risen more than 5% since the link was created (nominal > lastLinkHz × 1.05), the link is rebuilt. This catches the VBL-late case where both sources agreed on the wrong transitional value.
  • wakeRateVerifyDelays: [1.0, 3.0, 6.0, 12.0] — a longer verify sequence for the post-wake window, replacing the normal [2, 4, 8] s delays.
  • scheduleRateVerify(isWake:) — routes to the wake or normal delay array.
  • startKeeper / stopKeeper register and deregister a NSWorkspace.didWakeNotification observer (matching the existing lifecycle pattern) that triggers scheduleRateVerify(isWake: true) 0.5 s after wake.

Mos/AppDelegate.swift

  • screenChangeLateTimer — fires a second recreateDisplayLink() call 6 s after didChangeScreenParametersNotification. The existing 1 s debounce can still land inside the VBL transition window; 6 s reliably clears both the window and the 3 s recreateCooldown.

Validation

  • Ran the app from Xcode, let the Mac sleep for 2+ minutes, woke and scrolled immediately — smooth scrolling was fluid at native Hz without the refresh-rate toggle.
  • Console confirmed the fix firing:
    ScrollPoster: display link rate rose 60Hz -> 120Hz (was in transition at create), recreating
    
  • No changes to event-interception logic, persisted options, or permission flow.

Possible risks

  • The wake observer adds one more NSWorkspace notification listener, tied to the same startKeeper/stopKeeper lifecycle as the existing keeper timer — deregistered cleanly on disable().
  • The rateRoseAfterCreate condition uses a 5% tolerance to avoid spurious rebuilds from measurement jitter; normal steady-state operation won't trigger it.

Related

Extends the existing rate-verify mechanism introduced for issue #958. Same pattern, additional trigger condition and a dedicated wake path.

CVDisplayLink was created during the display's post-wake transition
window, where CGDisplayCopyDisplayMode and CVDisplayLink both report
the same temporary Hz (e.g. 60 Hz on a 120 Hz panel). The existing
ratio check (link < display × 0.7) passed trivially, so the verify
loop exited early and left smooth scrolling stuck at the wrong rate
until the user manually toggled the refresh rate setting.
Three layered fixes:
1. Track lastLinkHz at create() time. verifyRateAndFixIfNeeded now
   also triggers a rebuild when the display Hz has risen more than 5%
   since the link was created, catching the case where both sources
   agreed on the transitional value.
2. Add a wake-specific verify sequence (1 s, 3 s, 6 s, 12 s) via
   scheduleRateVerify(isWake:), longer than the normal [2, 4, 8] s
   window, to cover the full display stabilisation period.
3. Register NSWorkspace.didWakeNotification inside startKeeper /
   stopKeeper (matching existing lifecycle pattern) to kick off the
   wake verify sequence 0.5 s after wake, before the first scroll
   event arrives.
AppDelegate gains a screenChangeLateTimer that fires 6 s after
didChangeScreenParametersNotification as a final safety net: the
existing 1 s debounce can still land inside the transition window,
while 6 s reliably clears both the window and the 3 s recreate
cooldown.
Co-Authored-By: Antigravity <noreply@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant