Skip to content

Display configuration: verify a resolution's scan-out timing after apply, from the display controller's active timing id #88

Description

@Rydersel

The gap

A revealed HiDPI resolution is a scaled mode, so the display controller has to bind it to a real wire timing. When the panel advertises no full-width timing at that refresh, the controller binds it to an unrelated timing at the same refresh. Measured on an MSI MAG 341C on 2026-08-07: the 3440x1440 HiDPI mode at 120 Hz scanned out as 2560x1440, with the rightmost quarter of the desktop missing from the glass, while CGDisplayCopyDisplayMode, CGDisplayBounds, the capture size, backingScaleFactor and Candela's own post-commit achieved-state check all reported the requested mode.

Candela currently defends against this in two ways only. The unsupported-timing check withholds a revealed resolution whose refresh has no full-width timing (a prediction), and the confirmation countdown lets a person revert what they see (detection by eye). The code and the docs state that no IORegistry property records the driven timing, so software could not detect the fault after the fact.

What was measured on 2026-09-09

That statement is wrong. Every display has an AppleCLCD2 node in the IORegistry (one per display-controller pipe: disp0 for the built-in, dispext0, dispext1 for externals), and each node carries DPTimingModeId, an integer that is the ID of exactly one entry in that same node's TimingElements array. The entry's HorizontalAttributes.Active, VerticalAttributes.Active and VerticalAttributes.PreciseSyncRate (16.16 fixed point) describe the timing the controller is driving on the wire. The node's DisplayWidth and DisplayHeight follow the same entry. The Asahi Linux DCP experiments poll this property after setting a timing mode until the firmware reports the requested one, and PortScope reads it as the active timing.

Readings in correct modes, where the property already differs from the framebuffer:

Display DPTimingModeId Entry named CoreGraphics at the time
Built-in (M1 Pro) 2 3024x1964 @ 120 Hz logical 1800x1169, framebuffer 3600x2338 @ 120
Dell U2725QE (rotated 270) 76 3840x2160 @ 120 Hz logical 1440x2560, framebuffer 2880x5120 @ 120
MSI MAG 341C 86 3440x1440 @ 144 Hz 3440x1440, framebuffer 3440x1440 @ 144

Then three modes were applied at preview scope for a 25 s hold each, with a reading during the hold and after the revert:

Applied DPTimingModeId before, during, after Node DisplayWidth x Height during CoreGraphics during
Dell, 1440x2560@2x at 60 Hz (published, correct) 76, 78 (3840x2160 @ 60), 76 3840x2160 1440x2560 fb 2880x5120 @ 60
MAG, native 3440x1440 at 175 Hz (correct) 86, 87 (3440x1440 @ 175), 86 3440x1440 3440x1440 fb 3440x1440 @ 175
MAG, 3440x1440@2x at 120 Hz (the broken one) 86, 84 (2560x1440 @ 120), 86 2560x1440 3440x1440 fb 6880x2880 @ 120, reported clean

The two controls moved as predicted, the other nodes never moved, and during the broken mode the controller reported the same 2560x1440 @ 120 the monitor's OSD showed on 2026-08-07 while CoreGraphics read the requested mode on every axis. system_profiler during that hold said "3440 x 1440 @ 120.00Hz": it takes the refresh from the timing and the size from CoreGraphics, so it is blind on the width too.

Proposed guard

Add a third layer after the existing prediction and the countdown: verify the scan-out timing after every mode commit.

  1. After apply commits and the existing achieved-state check passes, locate the display's AppleCLCD2 node. IOMFBUUID on the node encodes the EDID vendor (first four hex digits) and product (next four, byte-swapped against CGDisplayModelNumber); the display's existing IORegistry service match is another route.
  2. Read DPTimingModeId, find the TimingElements entry with that ID, and take its active width, height and sync rate.
  3. Compare the driven active size with the panel's native pixel size (the size of the mode carrying the native flag). On a fixed panel every mode CoreGraphics drives, scaled or not, is downscaled by the controller onto the native timing, which is what the built-in, the Dell and both controls show. Any other size means the mode is bound to a foreign timing and the desktop is cropped or letterboxed.
  4. On a mismatch, treat the commit as unhonoured through the same path the achieved-state check already uses: revert, tell the person which timing the display actually took ("the display took 2560x1440 at 120 Hz"), and remember the mode as unsafe on that display so the picker withholds it afterwards. Keep the prediction guard and the countdown as they are; the reading lands after the mode has already been scanned out, so it cannot replace either.
  5. Report the driven timing in the diagnostics report, next to the row that already says how many modes the unsupported-timing check withheld.

Rules the implementation has to keep:

  • The check must be able to fail. Read it on a known-correct mode first and assert equality against the native size; a reading that cannot resolve (no node, no matching entry) is "not verifiable", never "clean".
  • The property is undocumented IOKit on the Apple Silicon display-controller path, on macOS 26. It has not been checked on Intel or on a virtual display; the absence case above covers both until measured.
  • IDs are per node. 86 names 3440x1440 @ 144 on the MAG's node and 1920x1080 @ 120 on the Dell's, so never compare ids across displays.
  • Resolve the sync rate with a half-hertz tolerance, the same as the existing refresh comparison; the entries carry 143.993 and 174.962 for what CoreGraphics rounds to 144 and 175.
  • Never weaken the confirmation countdown on the strength of this check.

Fit

Pillar: Controls, the display-configuration surface. It passes the feature filter because it explains what the display actually did with a configuration change instead of adding a setting to twiddle: a resolution change that reports success while a quarter of the desktop is off the glass is the display lying about its state, and this reads the state from the controller and says so. Checkup's refresh sweep excludes revealed modes today for exactly this reason and could admit them once the sweep can read the driven timing.

Hardware verification

Rig: an Apple Silicon Mac on macOS 26 with the MSI MAG 341C (a panel with revealed 3440x1440 HiDPI rungs at 120 Hz and 75 Hz that have no full-width timing) and a second external that answers readbacks. The app quit for the run.

  1. Positive control, correct mode: apply a published full-width mode at another refresh on each external (the Dell at 60 Hz, the MAG at 175 Hz) with the confirmation countdown armed. The check must read the display's active timing entry as the panel's native size at the new refresh, report the apply as honoured, and the countdown must revert as usual. The reading must change with the mode and change back after the revert; a reading that does not move is a broken instrument, not a pass.
  2. The fault: with the unsupported-timing check turned off, apply the MAG's 3440x1440 HiDPI mode at 120 Hz. Expected: the display's active timing entry reads 2560x1440 at 120 Hz while CoreGraphics reports 3440x1440 with a 6880x2880 framebuffer, the guard reports the commit unhonoured, names the timing the display took, reverts, and the mode is withheld from the picker afterwards. The person at the display confirms the desktop was pillarboxed and cropped during the hold.
  3. Degradation: run the check against a display with no readable timing entry (a virtual display, or a synthetic absent-property case in a test). Expected: the verdict is "not verifiable" and the existing countdown behaviour is unchanged; never "clean".
  4. Diagnostics: the report shows the driven timing for each display next to the withheld-modes row, and the values match step 1's readings.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions