Skip to content

Host: virtual Desktop display fallback for headless Macs (#61) - #200

Open
addhumal wants to merge 6 commits into
Saffsanity:mainfrom
addhumal:headless-display
Open

addhumal wants to merge 6 commits into
Saffsanity:mainfrom
addhumal:headless-display

Conversation

@addhumal

@addhumal addhumal commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #61.

With a MacBook's lid closed and nothing attached (or a Mac mini with no monitor), Sill gives the Mac its own Retina display, the size of the MacBook's own screen, so the Desktop streams sharp. The lock screen streams there too, and the password typed on the iPad unlocks the Mac.

What it does

  • Sill's display in place of macOS's stand-in. With no screen at all, macOS 27 makes a 1× 1920×1080 "Display" (vendor unkn, model virt). Sill counts it as no real display and makes its own HiDPI display in its place, and macOS then removes the stand-in. The Desktop prefers a real display, then Sill's, then the stand-in (HeadlessDesktopPolicy.pool).
  • The MacBook's own size. The display is the built-in screen's size as Sill last saw it (1800×1169 pt here, made 1800×1168 @2×). Sill keeps that size in its preferences (lastBuiltInDisplayPoints), so a relaunch with the lid already closed still knows it. Never seen (a Mac mini): the device's viewport, as before.
  • Kept between connections. The display stays when the last device leaves: a device back after a blip finds the same screen, and no display change locks the Mac. It goes once a real display is back and either the lid opened or no device is connected (HeadlessDesktopPolicy.releases). A monitor that comes back while the lid stays shut and a device works (Sill's own wake of an asleep monitor, Title: Mac login screen rejects the correct password when the Mac has no active display #298) leaves it, and the Desktop stays on Sill's display, where macOS moved the windows: removing it then is a display change macOS may lock a closed-lid Mac on, which may be the nightly's relock after about 5 seconds in Title: Mac login screen rejects the correct password when the Mac has no active display #298. macOS's display reconfiguration callback tells Sill, with no device connected too. Sill looks at the displays and restarts the Desktop on the real one, then removes its display, and macOS moves the windows back.
  • Follows the display's size. When the Desktop's display changes size under it, the Desktop restarts at the new size. When the lid opens, the built-in screen comes back at 1512×982 for a moment, then at the person's own 1800×1169.
  • Sill.log says what displays a lock or an unlock finds (Title: Mac login screen rejects the correct password when the Mac has no active display #298): after every Locked: and Unlocked: line, Displays: N online, N active, N asleep, N Sill's, N macOS's stand-in.
  • Merged with main at c4a331f. At a locked screen main's Desktop captures the display, so the fallback display is made there too; off console, main's placeholder needs none.

Checks

  • virtual-display-api: 185 checks and 32 of 32 mutants (the stand-in, the pool, the built-in size, when it goes, and when it's kept). lock-policy: 536 checks and 140 of 140 mutants (the displays' line).
  • run-all.sh: all 63 checks pass on the merge. swift build -c release: only the CaptureProbe warning.

Live (14-inch MacBook Pro, M4 Pro, macOS 27, power in, nothing attached, 2026-11-02)

  • Lid closed: the Retina display in 0.5 s, the iPad at 3600×2336.
  • The password typed on the iPad unlocked the Mac, and it stayed unlocked.
  • Four disconnects and reconnects: the same display each time, and no lock from Sill.
  • The lid opened with the iPad connected: the built-in screen, the virtual display removed, and the Desktop restarted at 3600×2338, all in under 0.5 s.

Not checked

Problems found, and how they were fixed

  1. The password from the iPad didn't stick with the lid shut. Sill.log from 2026-11-01 showed two causes. The installed Sill (0.5.5, build 1232) predated main's locked-Mac work. And with the lid shut and nothing attached, the Mac at times had no display at all, so the stream stopped ("Failed to find any displays or windows to capture"). Fix: main merged in, plus Sill's own display when there's none. Live: unlocked from the iPad, and it stayed unlocked.
  2. The picture wasn't Retina. With no screen, macOS 27 makes its own 1× 1920×1080 stand-in display, which Host: virtual Desktop display fallback for headless Macs (#61) #200 first took for a real one. Fix: the stand-in counts as no display, so Sill makes its HiDPI display at the MacBook's own size (1800×1168 @2×). macOS then removes the stand-in.
  3. A brief disconnect locked the Mac again. The display went when the last device left, and making a new one 2 s later made macOS lock (11:33:43 → 11:33:46). Fix: it stays until a real display is back. Live: four disconnects and reconnects on the same display, with no lock from Sill.
  4. Opening the lid would have left the windows on a screen nobody sees. macOS moves every window onto the only display left when the lid closes. Fix: Sill removes its display as soon as a real one is back. macOS's display-change callback tells it, so this works with no device connected too.
  5. The iPad went black for 1.6 s as the lid opened. The Desktop restarted before Sill's look had seen the built-in screen, then lost the display being removed (11:38:45). Fix: look first, then restart, then remove.
  6. The stream stayed small after the lid opened. The built-in screen came back at 1512×982 for a moment, then at 1800×1169. Fix: the Desktop restarts when its display's size changes. Live: 3600×2338 within 0.4 s.
  7. From the review: opening the lid during a pick or a restart removed the display from under the capture that switch started. Fix: the release waits for the switch to end, at most 5 s.

…play goes when a real one is back

Conflicts: the last device leaving (main's fitScales reset kept beside the
fallback display's removal) and the Desktop's start (main's desktopCapture(),
with the fallback display made first when no display is online, so a locked
screen with the lid closed streams too: main captures the display at a
locked screen, and off console streams its placeholder without one); the
checks' README (main's rows, this branch's virtual-display-api counts).

With the lid closed macOS moves every window onto the fallback display, the
only one left. It stayed until the last device left, so opening the lid
showed an empty Mac. Now it goes at the first catalog poll that sees a real
display online (HeadlessDesktopPolicy.stillNeeded), after the Desktop
restarts on that display. virtual-display-api 166 checks, 23 of 23 mutants;
run-all.sh all 63; swift build -c release, only the CaptureProbe warning.
…id closed, kept between connections

Tested on a 14-inch MacBook Pro (M4 Pro, macOS 27) with the lid closed, power
in and nothing attached (2026-11-02, Sill.log):

- macOS's own stand-in display (a 1x 1920×1080 "Display", vendor 'unkn',
  model 'virt') counts as no real display (HeadlessDesktopPolicy.isStandIn):
  Sill makes its Retina one in its place, and macOS removes the stand-in. The
  Desktop prefers a real display, then Sill's, then the stand-in
  (HeadlessDesktopPolicy.pool).
- Its size is the built-in screen's as Sill last saw it (1800×1169 pt here →
  1800×1168 @2×), kept in the preferences (lastBuiltInDisplayPoints).
- It stays when the last device leaves: a device back after a blip finds the
  same screen, and no display change locks the Mac. It goes when a real
  display is back, told by CGDisplayRegisterReconfigurationCallback (the
  catalog's poll idles with no device), after a look and the Desktop's
  restart on the real display.
- The Desktop restarts when its display's size changes under it (the lid
  opened: the built-in screen at 1512×982 for a moment, then 1800×1169).

Live: lid closed → Retina 3600×2336 in 0.5 s; the password typed on the iPad
unlocked the Mac and it stayed unlocked; four disconnects and reconnects on
the same display; the lid opened with the iPad connected → the built-in
screen, the virtual display removed, restarted at 3600×2338, in under 0.5 s.
virtual-display-api 177 checks, 28 of 28 mutants; swift build -c release.
…t goes

From the review (2026-11-02): with a pick or a restart under way as the lid
opened, the release's select returned at once and the fallback display went
from under the capture that switch started (black until the device asked
again, up to 10 s). It now waits for the switch to end, at most 5 s.
run-all.sh all 63; swift build -c release.
@addhumal

addhumal commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Saffsanity this is ready for your review: a MacBook with its lid closed and nothing attached now streams a Retina Desktop, and the password can be typed from the iPad (#61).

Tested live (14-inch MacBook Pro, M4 Pro, macOS 27, power in, nothing attached):

  • Lid closed: Sill's 1800×1168 @2× display in 0.5 s, the iPad at 3600×2336.
  • The password from the iPad unlocked the Mac, and it stayed unlocked.
  • Four disconnects and reconnects landed on the same screen, with no lock from Sill.
  • The lid opened with the iPad connected: the built-in screen at full size within 0.5 s, and the windows back.

Decisions to confirm:

  • macOS's own 1× stand-in display ('unkn'/'virt') is treated as no display, so Sill's Retina one takes its place.
  • The display is the built-in screen's size as last seen, kept in lastBuiltInDisplayPoints.
  • It stays between connections and goes only when a real display is back.

Untested, for you:

@addhumal

addhumal commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Merging this beside #173 / #200: they conflict only in how the Desktop picks its display. A tested merge of the two is ready on addhumal:reconcile-173-200, for whichever lands second.

…ffsanity#298)

A Mac with no active display turns its password away (Saffsanity#298), and a closed-lid Mac whose display changes
may lock again: each 'Locked:' and 'Unlocked:' line is followed by 'Displays: N online, N active, N asleep,
N Sill's, N macOS's stand-in.' (LockPolicy.displaysLine, pure; WindowCatalog.displaysLine reads them).
Measured on a closed-lid MacBook Pro with two monitors: asleep, both are online and neither active, so
this branch's fallback display, which counts the active ones, is made then too. lock-policy 536 checks,
140 of 140 mutants (2 new); run-all.sh all 63; a --synthetic host locked by SILL_TEST_LOCKED_FILE prints
the line after its Locked line. The display helpers it calls are nonisolated (CoreGraphics only).
… shut (Saffsanity#298)

A closed-lid Mac whose monitor Sill's wake brought back removed its fallback display at once, a display
change macOS may lock a closed-lid Mac on: the nightly's 'unlocks, then the login screen again after about
5 seconds' (Saffsanity#298). Now it goes when a real display is active and the built-in screen is among them (the lid
opened, as before: the windows come back) or the last device left (HeadlessDesktopPolicy.releases), and
while it stays the Desktop streams it, where macOS moved the windows (pool's sillsFirst, the catalog's
keepsSills). virtual-display-api 185 checks, 32 of 32 mutants (4 new, 2 moved to pool's new line);
run-all.sh all 63; swift build -c release, only the CaptureProbe warning.

This branch has not been deployed

No deployments
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.

Feature Request: Ensure Sill can be used as a monitor for a headless Mac

1 participant