macOS 26 Stop Extending Blanked My Monitor: Fixing WindowServer IndependentOutput

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_profiler and 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 carried IndependentOutput = 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 a CGBeginDisplayConfiguration / 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 SPDisplaysDataType listed only the DELL.
  • BetterDisplay's get -identifiers returned 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.

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:

swift
import CoreGraphics
for 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:

  1. The state lands in the persisted plist, so a reboot does not clear it.
  2. System Settings → Displays does not show displays in Independent Output, and I found no UI to flip it back.
  3. BetterDisplay's perform -reconfigure and perform -connectAllDisplays are 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:

swift
import Foundation
import CoreGraphics
let h = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_NOW)
typealias ConfIndep = @convention(c) (CGDisplayConfigRef?, UInt32, Bool) -> Int32
guard 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 Output
var 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))")
shell
swift 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 -displayInformation reports RGB 10bit full, SDR, and a full connectionModeList.
  • The IndependentOutput key 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:

  1. 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.
  2. "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.
  3. 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.
  4. 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