Screens added / removed continually when display turned off

I have a function in my app to detect if screens are added or removed, watching for notifications from NSApplication.didChangeScreenParametersNotification. I am seeing some strange behavior when the screen attached to a Mac mini is turned off, macOS will spit out hundreds of the didChangeScreenParametersNotification, all relating to a 'ghost' screen being added and then subsequently replaced with the original screen a second later. This cycle will go on for hours until the screen is turned back on again.

I can confirm this also happens with the CoreGraphics equivalent, with flags .added and .removed being the only changes. I would imagine this creates immense churn for all apps watching for screen changes.

I've tried debouncing the notifications but even with a delay of 10 seconds this is still being called hundreds of times while the computer is idle and the screen is off.

One constant I can see is that the CGDisplayUnitNumber() for the 'ghost' display is always 0, while the logical unit number for the real screen is '1'. Is it safe to ignore screens with 0? I'm trying to find a reliable way to prevent heavy processing for 'false' screens. I'm afraid because this ghost screen has parameters so different to the actual screen, it's otherwise not possible to ignore it as it looks like a new screen.

See example below:

// Observe notification
NotificationCenter.default.addObserver(self, selector: #selector(displaysDidChange), name: NSApplication.didChangeScreenParametersNotification, object: nil)

// Function to update screens called from displaysDidChange
func updateScreens() {

  let screens = NSScreen.screens
  for screen in screens {
    guard let screenDisplayID = screen.displayID() else {
                        NSLog("Screen does not have a display ID: \(screen.localizedName)")
                        continue
                    }
    let screenIdentifier = "v\(CGDisplayVendorNumber(screenDisplayID)), m\(CGDisplayModelNumber(screenDisplayID)), sn\(CGDisplaySerialNumber(screenDisplayID)), u\(CGDisplayUnitNumber(screenDisplayID)), sz\(CGDisplayScreenSize(screenDisplayID))"
  }
  // -- Logic to determine if screen is new or already exists for window management --
  NSLog("Found new screen display ID \(screenDisplayID) (\(screenIdentifier)): \(screen.localizedName)")
}

And the logging I'll get:

Found new screen display ID 2 (v16652, m1219, sn16843009, u1, sz(1434.3529196346508, 806.823517294491)): Philips FTV

Found new screen display ID 10586 (v1970170734, m1986622068, sn0, u0, sz(677.3333231608074, 380.9999942779541)):

Answered by DTS Engineer in 842815022

One constant I can see is that the CGDisplayUnitNumber() for the 'ghost' display is always 0, while the logical unit number for the real screen is '1'. Is it safe to ignore screens with 0?

Yes. More specifically, the value is actually kCGNullDirectDisplay from CGDirectDisplay.h:

#define kCGNullDirectDisplay ((CGDirectDisplayID)0)
#define kCGDirectMainDisplay CGMainDisplayID()

...and means exactly what you think it does.

I'm afraid because this ghost screen has parameters so different to the actual screen, it's otherwise not possible to ignore it as it looks like a new screen.

Interestingly, after a bit of digging, the numbers are more meaningful than they look, though it is a bit obscure. Many of implementation details of this area of the system are extremely old, as they were implemented as part of MacOS X's original implementation (pre-10.0) and, as is the case here, basically "copied" from macOS classic. Sometimes that was because code was directly copied, sometimes it was simply a matter of reusing the same general approach because there wasn't really anything "wrong" with the previous approach.

In this case, one of those patterns from Classic MacOS is using encoding 4 ASCII characters as a single 32 bit value (the way type/creator codes were encoded). Applying that here gets you:

v1970170734-> 0x75 6E 6B 6E -> 'unkn' -> "Unknown"
m1986622068-> 0x76 69 72 74 -> 'virt' -> "Virtual"

In a similar vein, the resolutions is almost certainly derived from MacOS X's original resolution, 640x480*. I'm not sure how that lead to the specific values you're seeing, but my guess is that it's a case of having used the same value "forever" and simply not caring/noticing** what the resolution actually was, since the system was already going to ignore the display.

*I'm not sure the system will actually still run at that resolution but I am certain it won't work very well. It didn't work very well in 2001 and I doubt the situation has improved.

**Philosophically speaking, is it possible to incorrectly draw on a screen no one ever sees?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

One constant I can see is that the CGDisplayUnitNumber() for the 'ghost' display is always 0, while the logical unit number for the real screen is '1'. Is it safe to ignore screens with 0?

Yes. More specifically, the value is actually kCGNullDirectDisplay from CGDirectDisplay.h:

#define kCGNullDirectDisplay ((CGDirectDisplayID)0)
#define kCGDirectMainDisplay CGMainDisplayID()

...and means exactly what you think it does.

I'm afraid because this ghost screen has parameters so different to the actual screen, it's otherwise not possible to ignore it as it looks like a new screen.

Interestingly, after a bit of digging, the numbers are more meaningful than they look, though it is a bit obscure. Many of implementation details of this area of the system are extremely old, as they were implemented as part of MacOS X's original implementation (pre-10.0) and, as is the case here, basically "copied" from macOS classic. Sometimes that was because code was directly copied, sometimes it was simply a matter of reusing the same general approach because there wasn't really anything "wrong" with the previous approach.

In this case, one of those patterns from Classic MacOS is using encoding 4 ASCII characters as a single 32 bit value (the way type/creator codes were encoded). Applying that here gets you:

v1970170734-> 0x75 6E 6B 6E -> 'unkn' -> "Unknown"
m1986622068-> 0x76 69 72 74 -> 'virt' -> "Virtual"

In a similar vein, the resolutions is almost certainly derived from MacOS X's original resolution, 640x480*. I'm not sure how that lead to the specific values you're seeing, but my guess is that it's a case of having used the same value "forever" and simply not caring/noticing** what the resolution actually was, since the system was already going to ignore the display.

*I'm not sure the system will actually still run at that resolution but I am certain it won't work very well. It didn't work very well in 2001 and I doubt the situation has improved.

**Philosophically speaking, is it possible to incorrectly draw on a screen no one ever sees?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Wonderful answer - thank you very much for the detailed response! Very interesting :-)

Did I miss something in the documentation around this particular behavior with the virtual screen notification churn? If not, perhaps I'll open a feedback request to improve the documentation. I'm sure a few apps utilise this particular notification, and could unknowingly be doing something inefficiently.

I only discovered this behavior in my own app after having recently purchased a Mac mini. My laptop does not exhibit this behaviour for example, so this wouldn't immediately be obvious to a developer.

Thanks again!

Sorry to continue, but I'm just stepping back slightly.

Thanks to your explanation I'm now able to identify this virtual screen and ignore it, however this doesn't solve the underlying issue that I'm getting hundreds of these notifications while the screen is turned off. These notifications cycle between replacing the primary screen with this virtual screen and back again, so half of the notifications I'm still needing to process further to check if the physical screens have changed. This seems somewhat inefficient.

This behavior is reproducible via two Mac minis of different generations, with different screens. I'm unsure if this is a macOS display detection bug or not, so I've submitted it via Feedback Assistant.

Any further context here would be greatly appreciated!

Wonderful answer - thank you very much for the detailed response! Very interesting :-)

You're very welcome.

Did I miss something in the documentation around this particular behavior with the virtual screen notification churn?

No, nor is this the kind of detail we've ever documented (or are likely to). The documentation describes how are APIs "work" (or are supposed to), but the actual, detailed, behavior of any given system basically "emerges" from the details of how all of the software and hardware components interact with each other.

I'll admit that the "why" of an issue like yours is often quite complicated. It might just be a bug on our side but, from past experience, what's more common is that there are a collection of secondary factors interacting with each other.

Case in point, my home office machine is a macBook Pro with a 42" 4k TV (best idea I ever had!) and LG Thunderbolt display attached. In any case, periodically at night the LG display will wake for no apparent reason. At one point, I dug into it a bit and, from what I can tell, what's actually going on is:

  • The 3rd party USB mouse plugged into a hub on the other side of the device both interferes with sleep and seems to send occasional, minor, "phantom" motion.

  • That activity sometimes ends up reseting all of the USB ports negotiating ends up reseting the thunderbolt connection to the monitor.

  • I think the monitor would have stayed asleep, except it has it's own USB controller in it and THAT controller is already awake (because the monitor is self powers and it's charging device connected to).

  • ...so the monitor resets it's own USB controller and that ends up waking the display.

The final behavior here is obviously a bug, but there isn't any any point in that change where the system has any clear/easy fix which doesn't carry a very high risk of creating other problems.

NOW, I don't know what's going on in your particular case but I hope this illustrates why this sort of thing happens.

Looking at a few other details:

This behavior is reproducible via two Mac minis of different generations, with different screens.

Was there any common hardware between those two cases, particularly things like hubs or 3rd party peripherals? Similarly, were both of these "pure" screens or did they have USB hubs in them?

What you're seeing here:

These notifications cycle between replacing the primary screen with this virtual screen and back again, so half of the notifications

Is, by definition, a symptom not a "cause". The system disconnected the display because it was moving toward sleep and didn't want anything drawing to the screen. The system reconnected it because it was moving toward wake and was preparing to draw again.

That leads me back to here:

I am seeing some strange behavior when the screen attached to a Mac mini is turned off,

Is the machine supposed to be asleep? Or have you intentionally set it up to state awake when headless? Similarly, what's the rate of these cycles:

macOS will spit out hundreds of the didChangeScreenParametersNotification, all relating to a 'ghost' screen being added and then subsequently replaced with the original screen a second later.

You said each cycle takes ~1s, but how long is the gap between cycles?

One other comment here:

I'm still needing to process further to check if the physical screens have changed.

Minor optimization to look at here is what CGDisplayIsActive(_:)/CGDisplayIsOnline(_:)/CGDisplayIsAsleep(_:) return about the display. My guess that one or all of them is returning "false", in which case you should be able to immediately return without doing any further processing. I think transitioning out of those state will trigger another notification and that's the point you'd actually "do" something.

I'm unsure if this is a macOS display detection bug or not, so I've submitted it via Feedback Assistant.

Bug number?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for your continued exploration.

The screens are both HDMI TV's, no USB hub. Both have different sets of USB / Bluetooth devices attached.

The gap between these events is every 2-10 seconds. CGDisplayIsActive and Online returns true and Asleep returns false for all screen representations, physical or virtual.

Bug report number is FB17969822.

The screens are both HDMI TV's, no USB hub.

Interesting. How "similar"* are the two TVs?

*Age, manufacturer, etc.

The gap between these events is every 2-10 seconds. CGDisplayIsActive and Online returns true and Asleep returns false for all screen representations, physical or virtual.

Huh. Further clarifying, what "state" is the machine tending to "live" in? In other words, which screen is it claiming to be attached to "most" of the time (while the display is off)?

Also, the big question here is still this:

Is the machine supposed to be asleep?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Interesting. How "similar"* are the two TVs? *Age, manufacturer, etc.

Besides resolution (4k), the screens are from different manufacturers and are 3-5 year manufacturing dates apart.

Huh. Further clarifying, what "state" is the machine tending to "live" in? In other words, which screen is it claiming to be attached to "most" of the time (while the display is off)?

The physical screen is the one that is 'final' after the notification cycle.

Is the machine supposed to be asleep?

I suppose it should be! The only thing to note is that the 'turn display off after inactivity' setting is disabled, along with disabling screensaver.

Screens added / removed continually when display turned off
 
 
Q