Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/STREAMS-VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,91 @@ activity blocks restoration. Normal window-close handling is covered by the
controller tests; the live offline test exercised client termination. Pre-login,
lock-screen, sleep/wake, reboot and a physical network outage still require
separate validation.

## Mac pointer scaling after a mode change

Fixed and installed on 2026-09-05. Switching the Dell from 1920×1080 HiDPI to
1920×1080 without HiDPI left the stream with an incorrect pointer range.
[Sunshine caches the macOS input scale at startup](https://github.com/LizardByte/Sunshine/blob/v2026.516.143833/src/platform/macos/input.cpp#L525).
The adapter now verifies each changed mode and restarts Sunshine afterward,
including when the capture display ID is unchanged and when restoring a mode.

The regression test covers both HiDPI transition directions with an unchanged
capture output. All 37 stream tests and the Windows adapter, scene and quality
tests pass. The live Mac reconnected in its original quad tile with a 1:1 display
coordinate/pixel ratio; the user confirmed the pointer reaches the full window
and clicks correctly. This validates the desktop profile; HiDPI absolute input
and physical edge crossing in relative mode still need their own checks.

## Mac lid transitions

Opening the MacBook lid reset the captured Dell from 1920×1080 to its native
6144×2560 mode while the stream remained connected. The built-in panel became
the main display at 1728×1117. The Mac adapter now reports lid state and the
capture display's current numeric ID. The controller checks every five seconds
and reconnects after a detected lid transition, restoring the selected profile
with a fresh mouse scale. Capture selection still uses the persistent display
UUID; the built-in panel's mode is not changed.

Five additional regression tests cover lid recovery across controller restart,
preserving the original journal and assignment, numeric display-ID changes,
manual changes, compare-before-write conflicts and disconnect during recovery.
All 42 stream tests and the Windows adapter, scene and quality suites pass.
The explicit `reconnect --repair-display` path restored the live Dell to
1920×1080 with the lid open, leaving the built-in panel at 1728×1117 and retaining
the existing restoration baseline. A fresh physical close/open cycle is pending.


## Follow the Mac's main screen

Added and installed on 2026-09-05 after the user confirmed that capturing the
Dell's extended desktop was the wrong behavior with the MacBook lid open.
`display.follow_main` resolves the current primary screen through CoreGraphics.
Only the configured external UUID receives the profile's display mode; other
primary screens retain their existing mode. Restoration is bound to the physical
UUID, including when the external display is temporarily unavailable.

Six additional regression tests cover separate display baselines, switching in
both directions, unplugged-display restoration, ProMotion without an advertised
external mode, controller restart/disconnect during a main-screen change,
recovery after a capture-output write crash, and rejecting a changed main screen
before a write. All 48 stream tests pass; Windows adapter, scene and quality
suites also pass.

The old live session disconnected with its journal fully restored. All MacBook
profiles now enable main-screen following. With the lid open, the new session
selected built-in display 1 at its unchanged 1728×1117 HiDPI/ProMotion setting,
with Sunshine output 1 and a 2560×1440/60 HEVC stream. A physical lid-close
event was then detected automatically: the stream reconnected to the Dell at
1920×1080 without HiDPI in the same quad tile in 20.9 seconds. Reopening the
lid automatically reconnected to built-in display 1 at its unchanged
1728×1117 HiDPI/ProMotion mode, with no error. Both physical transitions are
verified; subjective pointer behavior still needs user confirmation.


## Basic desktop profiles and native macOS adapter

Installed on 2026-09-05. The local MacBook and work-laptop configurations now
contain only `desktop`, with host audio, ordinary absolute pointer input and
keep-awake enabled. The example configuration also starts with one desktop per
computer. Remote applications retain their own microphone/webcam selection;
this change does not forward or select those devices.

The MacBook uses the new `macos` adapter. CoreGraphics reads the main display,
logical and actual pixel dimensions, and nominal refresh. Display modes are
never changed. Only capture output is restored; existing BetterDisplay profiles
remain supported for users who explicitly configure them. Two new tests cover
native configuration without a UUID/mode and rejection of mode writes without
invoking BetterDisplay. All 50 stream, 8 Windows, 14 quality and 24 scene tests pass.

A live native probe returned built-in display 1, 1728×1117 logical / 3456×2234
pixels, with a nominal 120 Hz timing (the physical ProMotion setting was not
changed). The desktop connected with a 2560×1440/60 decoded stream and host audio
requested. No active local audio stream was available to verify playback muting;
actual speakers, microphone and webcam behavior remains a user/app check. The
previously verified main-screen lifecycle is shared by this adapter; a fresh
physical lid cycle with the native adapter has not yet been exercised.

The native Mac test disconnected and cleared its output journal. Windows SSH/helper
was unreachable during this change, so its updated profile was not live-tested;
its existing pending display recovery was preserved.
82 changes: 67 additions & 15 deletions docs/STREAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,77 @@ requires that title, Moonlight's class, its owned process and the compositor's
window identity. It does not adopt a manually launched stream. Close that view
before the first managed connection.

For automatic Mac preparation, BetterDisplay must already be running with CLI
integration enabled, and Sunshine must already have screen recording and input
permissions. Configure an approved SSH account with existing host-key trust and
The basic example has one `desktop` profile per computer with `audio: host`.
Sound plays through the remote computer's selected speakers/headset/dock, with
local Moonlight playback muted. Conferencing apps running on that computer use
its own selected microphone and webcam; Hypertile does not select those devices
or forward the Linux computer's microphone/webcam.

On macOS, `display: {"adapter": "macos"}` uses native CoreGraphics APIs to read
the main screen and its logical/pixel dimensions. It preserves macOS's resolution,
HiDPI and refresh settings, follows main-screen changes, and refreshes Sunshine's
capture/input context during reconnects. No BetterDisplay installation, display
UUID or forced mode is required. Sunshine still handles video capture/encoding
and Moonlight receives it. Only Sunshine's capture-output setting is journaled
and restored. The external screen may therefore use its normal ultrawide mode
with the lid closed; this basic profile does not force a 16:9 desktop.

Sunshine must already have screen recording and input permissions.
Configure an approved SSH account with existing host-key trust and
noninteractive authentication. An optional absolute `ssh.control_path` uses an
already authenticated multiplex connection; once it expires the adapter reports
SSH unavailable. SSH passwords and Sunshine admin credentials are not stored.

Find the physical display UUID on the Mac:
The optional `betterdisplay` adapter additionally manages a physical display's
mode. For that adapter, BetterDisplay must be running with CLI integration enabled.
Find its physical display UUID on the Mac:

```bash
/Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay \
get -type=Display -name='Your display' -identifiers
```

Preflight authenticates an app-list request through Moonlight. The Mac adapter
Preflight authenticates an app-list request through Moonlight. The BetterDisplay adapter
also checks Sunshine's stored computer UUID over the approved SSH connection
before any display operation. It resolves the display UUID to the current
CoreGraphics display ID and checks that it is active. It verifies the advertised
mode and, when `require_ac` is true,
AC power. `output_name` in `~/.config/sunshine/sunshine.conf` is mapped to that ID;
changing it restarts Sunshine. Display groups and virtual displays are excluded.
AC power. `output_name` in `~/.config/sunshine/sunshine.conf` is mapped to that ID.
Changing the display mode or capture output restarts Sunshine. Mode changes are
read back before restarting: Sunshine's macOS input context caches its pointer
scale at startup, so changing HiDPI afterward can leave mouse coordinates scaled
for the previous mode.
Display groups and virtual displays are excluded.
The BetterDisplay `connected` getter is not required: physical activity is read
through CoreGraphics. Permission status is reported as unknown until tested in
the stream. No lid/sleep settings are modified.

Set `display.follow_main: true` on a BetterDisplay profile to capture the Mac's
current main screen. Opening the lid can then switch the stream from the external
display to the built-in panel; closing it switches back when macOS makes the
external display primary. The configured `display.uuid` and `display.mode`
apply only when that physical display is primary. Other screens retain their
own resolution, HiDPI and refresh settings, including ProMotion. This follows
macOS's main-screen selection; it does not change which screen is primary or
move apps between extended desktops.

Main-screen changes trigger a reconnect in the same zone and refresh Sunshine's
input mapping. The previous display's mode is restored by its own UUID. If that
display is unplugged or changed independently, its restoration remains pending;
the built-in panel can still stream. Reconnect the missing display and use
`stream restore COMPUTER` after disconnect to retry pending restoration.
Disconnect an existing stream before enabling this policy in `computers.json`.

The three sizes are separate. `display.mode.resolution` is the logical desktop;
`hidpi: true` renders twice as many pixels per dimension. `stream_resolution` is
the encoded video size. Probe and status expose the resolved host mode. For
example, a 1920×1080 HiDPI desktop renders at 3840×2160 and streams at 2560×1440.

Sunshine 2026.516.143833 on the tested Mac produced a 2× absolute-pointer offset
with HiDPI capture. The example uses HiDPI off to keep ordinary absolute clicks
correct while retaining the same logical desktop size. For HiDPI, explicitly
choose `input: relative` and validate captured-pointer behavior; use
Ctrl+Alt+Shift+Z to release capture. `input: absolute` is the default. This is a
host/client compatibility limit, not a reason to silently select another display.
Sunshine 2026.516.143833 on the tested Mac produced an absolute-pointer offset
when HiDPI changed after Sunshine started. The adapter now restarts Sunshine
after changing modes or following the main screen to refresh that mapping.
`input: absolute` is the default; `input: relative` captures the pointer instead.
Use Ctrl+Alt+Shift+Z to release relative capture.

Use `display.adapter: external` for any host whose display settings
are managed elsewhere. Hypertile reports this explicitly and changes no host
Expand Down Expand Up @@ -201,8 +236,25 @@ ambiguous closes stop for attention. No scheduled retry survives a disconnect.
Use `hypertile-ctl stream retry macbook` for a source still assigned to its zone.
Individual SSH steps have a 40-second deadline; the single writer accepts the
next command between steps, so a stalled remote operation can delay a command.
Running Mac sources recheck display identity, capture output and power every
30 seconds. Losing the SSH observation channel marks the source degraded while
Running Mac sources recheck display identity, capture output, lid state and power
every five seconds. A detected lid transition reconnects the local view and
reapplies the selected display mode if macOS reset it. The original restoration
journal and tile assignment are retained. Recovery compares the observed mode
and lid state again before writing, so a later manual change is preserved.
Only the selected capture display is changed; the built-in panel keeps its mode.
With `follow_main`, a main-screen change also reconnects, and the profile mode
is applied only to the configured physical UUID when it is primary. Changes to
an unmanaged primary panel's mode trigger an input refresh without reverting
that mode.

A managed mode change without a detected lid transition is preserved and
reported as degraded. To explicitly restore the selected profile in a running Mac stream:

```sh
hypertile-ctl stream reconnect macbook --repair-display
```

Losing the SSH observation channel marks the source degraded while
its view keeps running; a confirmed missing display or power prerequisite stops
the view and requests restoration.

Expand Down
59 changes: 3 additions & 56 deletions docs/computers.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,12 @@
"hdr": false,
"yuv444": false,
"aspect": "fit",
"audio": "focus",
"audio": "host",
"input": "absolute",
"keep_awake": "visible",
"keep_awake": "always",
"display": {
"adapter": "betterdisplay",
"uuid": "00000000-0000-0000-0000-000000000002",
"mode": {
"resolution": "1920x1080",
"hidpi": false,
"refresh": 60
},
"require_ac": true
"adapter": "macos"
}
},
"desktop-capture": {
"stream_resolution": "2560x1440",
"fps": 60,
"bitrate": 60000,
"codec": "HEVC",
"decoder": "hardware",
"hdr": false,
"yuv444": false,
"aspect": "fit",
"audio": "focus",
"input": "absolute",
"keep_awake": "visible",
"display": {
"adapter": "betterdisplay",
"uuid": "00000000-0000-0000-0000-000000000002",
"mode": {
"resolution": "1920x1080",
"hidpi": false,
"refresh": 60
},
"require_ac": true
},
"system_keys": "always"
}
},
"platform": "macos"
Expand All @@ -65,17 +34,6 @@
"title": "WorkLaptop - Moonlight",
"profiles": {
"desktop": {
"stream_resolution": "2560x1600",
"fps": 60,
"bitrate": 60000,
"codec": "HEVC",
"audio": "focus",
"keep_awake": "visible",
"display": {
"adapter": "external"
}
},
"meeting-headset": {
"stream_resolution": "2560x1600",
"fps": 60,
"bitrate": 60000,
Expand All @@ -85,17 +43,6 @@
"display": {
"adapter": "external"
}
},
"meeting-audio": {
"stream_resolution": "2560x1600",
"fps": 60,
"bitrate": 60000,
"codec": "HEVC",
"audio": "continuous",
"keep_awake": "always",
"display": {
"adapter": "external"
}
}
},
"platform": "windows"
Expand Down
15 changes: 15 additions & 0 deletions plugin/Content.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ function inProgress(value) {
return ["connecting", "preflight", "preparing", "preparing-display", "startup-window", "reconnecting", "restoring", "stopping", "layout", "pending"].indexOf(value) !== -1
}

function streamControls(runtime) {
var r = runtime || {}, desired = r.desired === true
var journal = !!r.journal && Object.keys(r.journal).length > 0
var pending = inProgress(r.observed)
var connected = desired && !!r.window
return {
focus: connected,
disconnect: desired,
reconnect: connected && ["window-ready", "degraded"].indexOf(r.observed) !== -1 ? "reconnect"
: !desired && !r.pid && !r.window && !journal && !pending ? "connect" : "",
retry: desired && !r.window && !pending,
restore: !desired && journal && !pending
}
}

function audio(value) {
return value === "continuous" ? "Audio continues when you use local apps" :
value === "host" ? "Use the host headset; local stream playback is muted" :
Expand Down
20 changes: 15 additions & 5 deletions plugin/ContentPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Column {
readonly property var sel: overlay.selectedZone
readonly property var source: overlay.contentFor(overlay.selected)
readonly property var runtime: (source && source.runtime) ? source.runtime : ({})
readonly property var controls: Content.streamControls(runtime)
readonly property bool isStream: source !== null && source.type === "stream"
readonly property var quality: runtime.quality || ({})
readonly property var measurement: (quality.current || {}).measurement || ({})
Expand Down Expand Up @@ -433,11 +434,20 @@ Column {
visible: pane.isStream
width: pane.width
spacing: Style.spacing.sm
Action { text: "Focus"; tooltipText: "Focus the remote desktop and close"; enabled: !pane.overlay.busy && !!pane.runtime.window; onClicked: pane.overlay.streamAction("focus", pane.source.computer, true) }
Action { text: "Disconnect"; tooltipText: "Close the view; the zone goes back to local windows"; onClicked: pane.overlay.streamAction("disconnect", pane.source.computer) }
Action { text: "Reconnect"; tooltipText: "Restart the view in the same zone"; enabled: !pane.overlay.busy && !!pane.runtime.window && pane.runtime.observed === "window-ready"; onClicked: pane.overlay.streamAction("reconnect", pane.source.computer) }
Action { text: "Retry"; visible: !pane.runtime.window && pane.runtime.desired === true; onClicked: pane.overlay.streamAction("retry", pane.source.computer) }
Action { text: "Restore display"; visible: !!pane.runtime.journal && pane.runtime.desired !== true; tooltipText: "Put the host's display settings back"; onClicked: pane.overlay.streamAction("restore", pane.source.computer) }
Action { text: "Focus"; tooltipText: "Focus the remote desktop and close"; enabled: !pane.overlay.busy && pane.controls.focus; onClicked: pane.overlay.streamAction("focus", pane.source.computer, true) }
Action { text: "Disconnect"; visible: pane.controls.disconnect; tooltipText: "Close the view; the zone goes back to local windows"; onClicked: pane.overlay.streamAction("disconnect", pane.source.computer) }
Action {
text: "Reconnect"
tooltipText: "Open the remote desktop in this zone"
enabled: !pane.overlay.busy && pane.controls.reconnect !== ""
onClicked: {
if (pane.controls.reconnect === "connect")
pane.overlay.assignContent("stream", pane.source.computer, pane.source.profile)
else pane.overlay.streamAction("reconnect", pane.source.computer)
}
}
Action { text: "Retry"; visible: pane.controls.retry; onClicked: pane.overlay.streamAction("retry", pane.source.computer) }
Action { text: "Restore display"; visible: pane.controls.restore; tooltipText: "Put the host's display settings back"; onClicked: pane.overlay.streamAction("restore", pane.source.computer) }
}

Disclosure {
Expand Down
Loading
Loading