When an external monitor goes black, the usual suspects line up fast: bad cable, cheap panel, unsupported refresh rate, "have you tried rebooting". I walked every one of those paths. All dead ends.
The real cause was one mouse click of my own. In the macOS 26 menu bar Screen Mirroring panel, this monitor was listed as "Extending" with a Stop Extending button next to it. I clicked it. The monitor went black instantly, and after that System Settings, system_profiler, and BetterDisplay all stopped listing it. A reboot did not bring it back.
This post covers three things: what Stop Extending actually does to the system, how to dig it out from under a pile of plausible-looking hypotheses with a proper evidence chain, and how to undo it precisely without reinstalling anything or resetting NVRAM.
TL;DR
- Setup: MacBook Pro (Apple M4 Pro), macOS 26.6.2 (25G83), clamshell mode. Two external displays: a DELL U2723QX (3840×2160, primary) and a no-name USB-C monitor whose EDID name is literally "Monitor" (2560×1600). BetterDisplay 5.1.1.
- Trigger: Menu bar Screen Mirroring → Monitor shown as "Extending" → clicked Stop Extending.
- Symptoms: Monitor black (backlight on, no content); gone from System Settings → Displays; absent from
system_profilerand BetterDisplay; survives reboot. - Ruled out: USB-C link, EDID, DisplayPort modeset, refresh rate (260Hz → 60Hz made no difference), BetterDisplay
-reconfigure/-connectAllDisplays. - Root cause: In WindowServer's persisted layout,
com.apple.windowserver.displays.plist, this display's entry carriedIndependentOutput = True. That flag pulls the display out of desktop compositing, and CoreGraphics reports it offline. - Fix: Call the SkyLight private symbol
SLSConfigureDisplayIndependentOutput(cfg, displayID, false)inside aCGBeginDisplayConfiguration/CGCompleteDisplayConfiguration(.permanently)transaction. Back in seconds, persists across reboot.
The Scene
Both external displays had been working fine in clamshell mode for weeks. At 23:45 on October 15 I opened the Screen Mirroring item in the top-right menu bar to look at casting options. Next to "Monitor" it said "Extending", and below that was a Stop Extending button.
I clicked it without thinking.
Then:
- The no-name monitor went black immediately. Backlight still on (dark grey, not the pure black of a powered-off panel), but no content, and the cursor could not move onto it.
- System Settings → Displays showed only the DELL.
system_profiler SPDisplaysDataTypelisted only the DELL.- BetterDisplay's
get -identifiersreturned only the DELL and the Default Group. - Rebooted. Still black.
At this point BetterDisplay had zero awareness of the display; its CLI could not even assign it a tagID. That is the important clue: if BetterDisplay cannot see it, the problem lives below BetterDisplay.
Investigation: Three Layers of Evidence
The display pipeline, bottom to top: physical link (USB-C / DisplayPort) → kernel DCP driver (modeset, HPD) → WindowServer / SkyLight (compositing, display configuration) → application layer (System Settings, BetterDisplay). I collected evidence one layer at a time, from the bottom.
Layer 1: Kernel and Link — Healthy
First, is the monitor actually connected?
IORegistry had the full EDID. DisplayAttributes read ProductName "Monitor", Year 2025, MaximumRefreshRate 240, PortID 32 under dispext1, with an IOMobileFramebufferShim present. The hardware stack knew exactly what this was.
Kernel DCP logs (DCPEXT1 is this monitor):
mode_set_gated 2560x1600@259Hz ... switch to normal mode succeeded
HPDMonitor off -> on
Link training succeeded, modeset succeeded, hot-plug detect was asserted, and there was no unplug or link-loss anywhere. A few recurring warnings (vblank<300us, EDT ERROR getClockFrequency(0), pmu ram read error) appeared on every modeset, on the DELL's path too, so I classified them as noise.
Conclusion: not the cable, not the cheap panel, not DisplayPort negotiation. The monitor was lit and online. Nobody was drawing on it.
Layer 2: CoreGraphics — Offline
If the kernel says online, ask WindowServer what it thinks. A few lines of Swift against CoreGraphics:
swiftimport CoreGraphicsfor d: CGDirectDisplayID in [1, 2, 3, 4] {print("display \(d): online=\(CGDisplayIsOnline(d)) active=\(CGDisplayIsActive(d)) bounds=\(CGDisplayBounds(d)) vendor=\(CGDisplayVendorNumber(d))")}var cnt: UInt32 = 0; var ids = [CGDirectDisplayID](repeating: 0, count: 16)CGGetOnlineDisplayList(16, &ids, &cnt); print("online list:", ids[0..<Int(cnt)])
Output:
display 2: online=0 active=0 bounds=(-2560.0, 345.0, 2560.0, 1600.0) vendor=19083
display 3: online=1 active=1 bounds=(0.0, 0.0, 3840.0, 2160.0) vendor=4268
online list: [3]
The interesting part: display 2 (the no-name monitor) is online=0, yet CG still remembers its bounds, vendor, and even its current mode (mode 115, 2560×1600@260Hz). WindowServer knows everything about this display and is deliberately keeping it offline.
While there, I killed the refresh-rate hypothesis: switched display 2 to mode 120 (2560×1600@60Hz), CGCompleteDisplayConfiguration(.permanently) returned success, DCPEXT1 logged a fresh modeset — and it stayed offline. Refresh rate was not the problem.
Layer 3: WindowServer Logs and Persisted Config — Found It
log show --predicate 'process == "WindowServer"' filtered on Display:Config:
2026-10-15 23:45:43.296 client api - independent output change for display 2
...
2026-10-16 02:40:11.050 client api - Enable display: 0x2
2026-10-16 02:40:11.050 config_via_client_api - no independent output work
23:45:43 is the exact moment I clicked Stop Extending — Control Center went through the "client api" and performed an independent output change on display 2. Later, when BetterDisplay launched and tried Enable display: 0x2, WindowServer replied "No change between prior and requested configuration". From its point of view the display was already enabled; it was simply in Independent Output mode, and enable/disable is an orthogonal axis.
Then the persisted config. WindowServer stores display layout in two places:
~/Library/Preferences/ByHost/com.apple.windowserver.displays.<UUID>.plist/Library/Preferences/com.apple.windowserver.displays.plist
Key path DisplaySets/Configs[0]/DisplayConfig[n]. The two displays sit side by side. Exactly one key differed, present only on the no-name monitor:
IndependentOutput = True
The DELL had no such key. That was the entire diff.
Chain the three layers: kernel online → CG offline → the only differing attribute is IndependentOutput. Root cause confirmed. The reboot survival is explained too — the flag is persisted.
What Stop Extending Actually Does
Inferring from behaviour: in the macOS 26 Screen Mirroring panel, "Stop Extending" does not disconnect or disable the display. It switches it to Independent Output. The display remains enabled and keeps its modeset, but it is no longer part of the extended desktop and WindowServer composites nothing onto it. This state exists for dedicated content output (presentation-style, non-desktop surfaces). A regular user seeing a "Stop" button in a quick-access menu has no way to know that is the semantic.
What makes it worse:
- The state lands in the persisted plist, so a reboot does not clear it.
- System Settings → Displays does not show displays in Independent Output, and I found no UI to flip it back.
- BetterDisplay's
perform -reconfigureandperform -connectAllDisplaysare no-ops here, because they operate on enable/disable and connection state, not on IndependentOutput.
The Fix: Clear the Flag via SkyLight
Public CoreGraphics has no API for IndependentOutput, but SkyLight (WindowServer's client framework) does. dlsym against /System/Library/PrivateFrameworks/SkyLight.framework/SkyLight finds SLSConfigureDisplayIndependentOutput, with signature (CGDisplayConfigRef, UInt32 displayID, Bool enabled) -> Int32.
Wrap it in a standard CG configuration transaction and commit with .permanently so the plist is updated too:
swiftimport Foundationimport CoreGraphicslet h = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_NOW)typealias ConfIndep = @convention(c) (CGDisplayConfigRef?, UInt32, Bool) -> Int32guard let sym = dlsym(h, "SLSConfigureDisplayIndependentOutput") else { print("no symbol"); exit(1) }let f = unsafeBitCast(sym, to: ConfIndep.self)let displayID: UInt32 = 2 // the display stuck in Independent Outputvar cfg: CGDisplayConfigRef?var e = CGBeginDisplayConfiguration(&cfg); print("begin:", e.rawValue)let r = f(cfg, displayID, false); print("SLSConfigureDisplayIndependentOutput:", r)e = CGCompleteDisplayConfiguration(cfg, .permanently); print("complete:", e.rawValue)sleep(2)print("online=\(CGDisplayIsOnline(displayID)) active=\(CGDisplayIsActive(displayID))")
shellswift indep.swift
Output: begin: 0, SLSConfigureDisplayIndependentOutput: 0, complete: 0, and two seconds later online=1 active=1. WindowServer logged a matching client api - independent output change for display 2 — the same code path Stop Extending took, in the opposite direction.
Verification:
CGGetOnlineDisplayList→[3, 2].system_profiler SPDisplaysDataType→ Monitor 2560×1600@60Hz, Online: Yes.- BetterDisplay
get -identifiers→ "Monitor" appears with a tagID;get -displayInformationreports RGB 10bit full, SDR, and a full connectionModeList. - The
IndependentOutputkey is gone from both plists. - The monitor shows a desktop.
Retrospective
This is not a bug. It is a semantic trap. Stop Extending does exactly what the words say — stop treating this display as a desktop extension. The problem is that it drops the display into a state that is invisible to the user, irreversible from System Settings, and persisted. A destructive action sitting in a menu bar quick panel with no confirmation and no undo.
Lessons from the investigation:
- Locate the failing layer before forming hypotheses. Ten minutes of kernel logs eliminated cable, panel, and link in one go — cheaper than swapping cables and rebooting.
- "Invisible" and "broken" are different things. CG retained every attribute of display 2 while reporting it offline. That one detail turned the investigation from hardware to configuration.
- When a tool cannot see something, look beneath the tool. BetterDisplay is excellent, but it sits on top of WindowServer. WindowServer's own persisted config and logs are the final authority.
- Diffing beats reading. Two plist entries side by side, one differing key. No need to understand every key first.
If this happens to you: try the Screen Mirroring menu first and click the display again — in theory it should re-enter Extend mode (I had already fixed it the hard way and did not go back to test this). If there is no entry for it, the Swift script above fixes it in seconds; swap in your displayID. If CGGetOnlineDisplayList does not show it, iterate IDs 1 through 10 and look for the one with online=0 but a non-zero vendor.
Collateral: during the investigation I permanently switched this display from its default 260Hz to 60Hz to eliminate a variable. After the fix, go back to System Settings and set the refresh rate you actually want.
One last thing: the next time a menu bar button says "Stop", figure out what exactly it stops before you click.
Comments