Skip to content

Sync animation frame rate to the refresh rate of the menu's display - #71

Closed
danielchalmers wants to merge 1 commit into
mainfrom
sync-animation-framerate
Closed

Sync animation frame rate to the refresh rate of the menu's display#71
danielchalmers wants to merge 1 commit into
mainfrom
sync-animation-framerate

Conversation

@danielchalmers

@danielchalmers danielchalmers commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

WPF ticks its animation timing engine at roughly 60 fps by default regardless of the monitor's refresh rate, so on high refresh rate displays every animation in the app updates only every 2-3 refresh cycles, which reads as judder. This is most visible on the short interactions: hover (83 ms) and press (67 ms) transitions, drag-to-reorder rotation, and the window fades.

Each time the menu is shown, MenuService queries the refresh rate of the display the cursor is on (the same display both positioning modes open the menu on) and applies it with Timeline.SetDesiredFrameRate to the fade storyboards and, via PieControl, to every animation PieAnimationService creates.

Design notes

  • Per-monitor and per-show, not a startup global. An earlier revision of this PR queried the max rate across all displays once at startup and overrode the Timeline.DesiredFrameRate metadata default. That over-ticked slower monitors in mixed-rate setups and could never react to display hotplug, refresh rate changes, or the menu moving between monitors (metadata overrides are one-shot by design). Re-querying on every show costs one native call and handles all of those cases.
  • Minimal native surface. Reuses the monitor plumbing WpfUtil already had for cursor positioning: MONITORINFO becomes MONITORINFOEX (which carries the device name) and one EnumDisplaySettings import plus the DEVMODE struct is added. The earlier revision's separate DisplayUtil with adapter enumeration is gone.
  • Rendering is still vsynced, so the tick rate simply matches what the display can actually present. If the rate can't be determined, the WPF default is used and behavior is unchanged.

Verification

  • Full build and all 142 tests pass, rebased onto current main (which now includes Cache the surface ring's drop shadow to cut per-frame GPU cost聽#72).
  • GetCursorMonitorRefreshRate exercised on a dual-monitor setup with the cursor parked on each display in turn, including a secondary monitor at negative coordinates: both resolved to 165 Hz, confirming the device-name lookup works for non-primary displays.
  • Live smoke test of the app on a 165 Hz display: the menu renders and fades in with the new frame rate plumbing active, with a clean dismiss and shutdown.

@danielchalmers danielchalmers changed the title Sync animation frame rate to the monitor's refresh rate Sync animation frame rate to the refresh rate of the menu's display Jul 25, 2026
@danielchalmers
danielchalmers force-pushed the sync-animation-framerate branch from 6a94b2e to c098437 Compare July 25, 2026 21:16
@danielchalmers

Copy link
Copy Markdown
Owner Author

Closing: measurement shows this is a no-op.

I instrumented the actual animation tick rate on a 165 Hz display (a DoubleAnimation driving a DP callback, counting invocations per second):

                                    animation      render
layered, DesiredFrameRate=default    164.0/s      164.3/s
layered, DesiredFrameRate=165        161.6/s      162.0/s
layered, DesiredFrameRate=30          30.0/s       72.7/s
normal,  DesiredFrameRate=default    163.7/s      164.0/s
normal,  DesiredFrameRate=165        165.0/s      165.0/s

WPF already ticks at the display refresh rate without this change. The 30 Hz row confirms DesiredFrameRate is genuinely applied, so the property works, there is simply no headroom to reclaim. The default/165 comparison also holds with no CompositionTarget.Rendering subscription (163.3 vs 163.0), ruling out that the measurement hook was forcing the render passes.

The premise behind this PR, that WPF animates at 60 fps by default, is legacy .NET Framework behavior that no longer applies on modern .NET. Not worth ~100 lines and a DEVMODE P/Invoke struct for no measurable benefit.

Also measured along the way: a layered window (AllowsTransparency=true) presents at ~164/s versus ~165/s for a normal window, so that is not a composition bottleneck either.

@danielchalmers
danielchalmers deleted the sync-animation-framerate branch July 25, 2026 22:18
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