Skip to content

fix(mac): preserve virtual-display arrangement after delayed wake recovery - #28

Draft
raiseCatError wants to merge 1 commit into
release/app-store-rcfrom
integration/upstream-392-origin-restore
Draft

raiseCatError wants to merge 1 commit into
release/app-store-rcfrom
integration/upstream-392-origin-restore

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

A virtual display rebuilt right after the Mac wakes no longer loses its saved position. Before this change, a stalled first check could save macOS's default spot as if the user had dragged the display there. Every later session then placed the display in that wrong spot.

This ports upstream OpenDisplay's fix onto MeowDisplay's VirtualDisplay. It is a manual adaptation, not a cherry-pick.

Upstream source

Issue peetzweg/opendisplay#390: "Display arrangement lost when a post-sleep rebuild stalls the restore window"
Pull request peetzweg/opendisplay#392: "fix(mac): start the arrangement restore window on the first tick"
Source commit 1522cd22699b21932d69c718b4afc18bee45d546, by peetzweg, GPL-3.0 (the same licence as this repository)

The upstream references are in code spans on purpose, so this PR adds no cross-reference events to the upstream tracker.

The bug (present here at cbbad7f)

  • Mac/VirtualDisplay.swift started the 6 s arrangement restore window in init.
  • The display's tick task runs ensureNotMirrored(), then selectHiDPIMode(), then manageOrigin().
  • After sleep, a mode change can block the main actor for about 10 s. Upstream's field log shows exactly this.
  • In that case the first manageOrigin() runs after the window has already closed. It sees macOS's default origin and reports it through onOriginChange.
  • DisplayArrangement.save then overwrites the device's saved placement.
  • resize(…, movingTo:) started its new window from the wall clock in the same way.

What changes (4 files, +244 / −47)

  • Mac/OriginRestore.swift (new): upstream's pure restore/observe policy. The window now starts on the first tick, not when it is armed. A window that closes before any check succeeds starts observing instead of saving wherever macOS left the display. On first contact (no saved placement), the initial spot is still recorded.
  • Mac/VirtualDisplay.swift: three restore fields become one originRestore.
    • init arms the restore.
    • resize calls rearm(at:).
    • manageOrigin switches on tick(origin:).
  • MacTests/OriginRestoreTests.swift (new): upstream's 9 tests, unchanged, plus 2 tests for this fork.
  • project.yml: adds Mac/OriginRestore.swift to the hostless OpenSidecarMacTests sources. The app picks it up through - Mac.

Why not a cherry-pick

A dry-run cherry-pick of 1522cd2 conflicts in Mac/VirtualDisplay.swift and project.yml. Upstream's VirtualDisplay has diverged from this fork's: this fork has its own HiDPI enforcement and a headless-main path that upstream doesn't have.

Fork-specific handling: headless main display

This fork's repositionForHeadlessMain(to:) moves the virtual display to (0,0) when no usable physical display exists. This gives macOS's login and lock UI a main display to render on. It used to re-arm the same restore window.

  • It now calls a new OriginRestore.retarget(to:). This restarts the window on the next tick without marking (0,0) as observed.
  • Result: if every attempt to apply the move fails, nothing is reported and the saved placement stays untouched.
  • Using upstream's rearm(at: .zero) instead would have saved a spurious drag when the window closed. A dedicated test covers this case.
  • The method's doc comment had absorbed manageOrigin's; it is split back into two.

Not touched: sender session management, transport, pairing/TLS, DisplayArrangement storage, the wire protocol, and the existing sleep/lock capture recovery. OriginRestore is a value type mutated only inside VirtualDisplay, through the same main-thread call sites as the fields it replaces, so there is no new concurrency surface.

Verification

Not verified

  • No physical sleep/wake validation. The field failure needs a sender whose WindowServer stalls a mode change for more than 6 s after wake. Only the policy is unit-tested here, not CoreGraphics behaviour.
  • Suggested hardware check:
    1. Extend to a device and drag its display to a non-default spot. Disconnect and reconnect, and confirm it comes back there.
    2. Let the Mac sleep for several minutes while the session is up, then wake it.
    3. Confirm the display returns to the saved spot. The log should show origin … → restored, followed by no arrangement save of macOS's default.
    4. Repeat with no usable physical display (headless).

Attribution

The original change is by peetzweg in upstream OpenDisplay. The commit message credits peetzweg and records (adapted from commit 1522cd22699b21932d69c718b4afc18bee45d546). The commit is authored with this repository's maintainer identity because it contains fork-specific code beyond the upstream change.

Draft. Not for merge until the hardware check above is done.

After sleep, a mode change that blocked the main actor for ~10 s let the
first manageOrigin() tick run after the 6 s restore window armed at
creation had already closed. That tick reported macOS's default origin
as a user drag, and DisplayArrangement saved it over the device's
placement, so every later session put the display back there.

The restore/observe policy moves into a pure OriginRestore struct whose
window starts on the first tick. A window that closes without one
observation starts observing instead of saving wherever macOS left the
display; first contact still records the initial spot.

Ported onto this line's VirtualDisplay rather than cherry-picked (the
original conflicts). The fork-only repositionForHeadlessMain gets
retarget(to:), which re-arms the window without marking the new origin
observed, so a headless move whose applies all fail never saves the
display's current spot. Upstream's nine OriginRestore tests are kept;
two more cover the headless retarget.

Original change by peetzweg in upstream OpenDisplay (GPL-3.0), PR 392,
closing upstream issue 390.
(adapted from commit 1522cd2)
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.

1 participant