Skip to content

qemu: tell the guest the host display's refresh rate - #7982

Merged
osy merged 2 commits into
utmapp:mainfrom
dragoshont:qemu/host-refresh-rate
Oct 29, 2026
Merged

osy merged 2 commits into
utmapp:mainfrom
dragoshont:qemu/host-refresh-rate

Conversation

@dragoshont

@dragoshont dragoshont commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Guests in UTM always get a 75 Hz display, whatever the Mac's display runs at.

QEMU only puts the host refresh rate in the guest's EDID when the display frontend reports it, and SPICE can't, so the guest gets a made-up 75 Hz mode. Windows 11 ARM64 on viogpu3d then lists only 75 Hz modes:

  • On a 60 Hz display about one frame in five is rendered and never shown. Hollow Knight: 70 fps in the guest, 57 reaching the window.
  • On a 120 Hz display the guest can't go above 75.

This passes the main screen's refresh rate to virtio GPU devices through QEMU's new refresh_rate property.

Depends on utmapp/qemu#7; without it QEMU rejects the argument. Related: #7820.

Testing: macOS 27.0.1, MacBook Pro M5 Max, external 60 Hz display plus the built-in 120 Hz panel. Debug build with utmapp/qemu#7, disposable Alpine VM with virtio-gpu-gl-pci:

VM started on QEMU argument Guest EDID
60 Hz display refresh_rate=60000 60.00 Hz
120 Hz display refresh_rate=120000 119.99 Hz
(current release) none 75.00 Hz

Windows 11 Pro ARM64 (build 26300, viogpu3d 100.6.101.58000, D3DMetal) on the same build:

VM started on Windows display modes
60 Hz display 60 Hz only
120 Hz display 120 Hz only, desktop composed at 120 Hz
(current release) 75 Hz only

The rate comes from NSScreen.main, the screen with the key window when the VM starts. Moving the window to another display later doesn't change it until the next start.

Test setup: an ad-hoc signed build has no app group, so UTM sets no APP_SANDBOX_GROUP_ID, Neptune can't create shared memory and Windows shows "Display output is not active". For these runs I set the group by hand; release builds aren't affected.

An AI agent ran these tests on my Mac.

I have read the AI contribution guidelines and have followed them to the best of my ability.

@osy

osy commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

The changes are tested on a MacBook Air (M3) running macOS 27.0.1.

@osy
osy force-pushed the qemu/host-refresh-rate branch 2 times, most recently from 2395b15 to a412978 Compare October 28, 2026 21:03
osy and others added 2 commits October 28, 2026 18:16
SPICE cannot report the host refresh rate to QEMU, so the EDID of every
virtio GPU advertises a synthesised 75 Hz mode. Pick up a patch adding a
refresh_rate property (mHz) that is used whenever the display reports
none, so UTM can pass the rate itself.

Drop the EDID patch that widened the range limits, put the rate in the
standard timings and added a DisplayID 2.0 section. The viogpu3d driver
takes the rate from the preferred timing for every mode, and hands
Windows an EDID of its own, and Linux uses the preferred timing as it
is, so Windows and Linux guests both resize up to 6016x3384 at 120 Hz
without it.

Related: utmapp#7820
Related: utmapp#7830

Assisted-by: GitHub Copilot:claude-opus-5.5
Assisted-by: Claude:claude-opus-5-5
The guest learns its refresh rate from the EDID that QEMU generates.
SPICE has no field for the host rate, so with UTM every guest gets a
made-up 75 Hz mode. Windows then lists only 75 Hz modes and composes at
75 Hz, so on a 60 Hz display about one frame in five is rendered and
never shown, and a 120 Hz (ProMotion) display cannot be used at all.

When a VM starts, ask its display delegate for the rate the display will
be rendered at and pass it to virtio GPU devices with the refresh_rate
property. That is the refresh rate of the screen the VM window is on at
launch, capped by the FPS Limit preference, so the guest and the renderer
agree. On macOS the renderer is updated too in case the window moved
screens while the VM was stopped. On iPhone the renderer stays at 60 Hz
unless a limit is set, so that is what the guest gets. Remote sessions
have no local display and keep QEMU's default.

Related: utmapp#7820

Assisted-by: GitHub Copilot:claude-opus-5.5
Assisted-by: Claude:claude-opus-5-5
@osy
osy force-pushed the qemu/host-refresh-rate branch from a412978 to b652538 Compare October 29, 2026 01:50
@osy
osy merged commit f98752c into utmapp:main Oct 29, 2026
27 checks passed
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.

2 participants