Skip to content

[WIP] Linux migration - #1054

Draft
QiE2035 wants to merge 147 commits into
AlkaidLab:masterfrom
QiE2035:linux-migration
Draft

QiE2035 wants to merge 147 commits into
AlkaidLab:masterfrom
QiE2035:linux-migration

Conversation

@QiE2035

@QiE2035 QiE2035 commented Sep 10, 2026

Copy link
Copy Markdown

No description provided.

Survey of the fork's Linux readiness: build system, packaging template
gap, per-feature platform matrix, and an enhancement-porting roadmap.
Reintroduce the four UNIX configure_file inputs referenced by
cmake/prep/special_package_configuration.cmake (desktop entries,
appstream metainfo, systemd user service). Content taken verbatim from
LizardByte/Sunshine at b2d44f5, the last commit before upstream renamed
these files; all @var@ placeholders are defined by this fork's CMake.
- The pinned third-party/tray implements Linux with Qt + libnotify
  (src/tray_linux.cpp); the referenced tray_linux.c no longer exists.
  Link the submodule's tray::tray target like upstream does and gate on
  libnotify only (the appindicator defines were unused by src/tray).
- Remove the dead src/platform/linux/input.cpp entry; input lives in
  src/platform/linux/input/inputtino*.cpp picked up by the existing glob.
Platform fixes needed to build the fork's Windows-centric sources on
Linux (GCC 16, C++23):

- vdd_ioctl.cpp / vdd_utils.cpp: guard the Windows ZakoVDD transport and
  driver logic with _WIN32; add Linux stubs so the shared display
  session staging code links (driver reported absent/unsupported).
- vdd_utils.h: drop the unconditional windows.h include and add Linux
  no-op SessionEventListener / w_utils::is_any_rdp_session_active
  helpers used by session.cpp.
- globals.{h,cpp}: unguard VDD_NAME/ZAKO_NAME/is_running_as_system_user
  (plain constants; Linux reports the device as absent).
- entry_handler.cpp, session.cpp, system_tray.cpp: move Windows-only
  includes under _WIN32.
- config.cpp: add the fork's QVBR/HQVBR AMF rate-control constants
  (4/5/6, matching AMF 1.5.2 headers) to the non-Windows fallback table.
- video.cpp: use NVENC SDK numeric option values in the avcodec branch
  (the branch never compiled anywhere before; values from nvEncodeAPI.h).
- linux/display_device.cpp: update settings_t::apply_config stub to the
  fork's signature and stub the fork-added settings/device helpers.
- linux/misc.cpp: platf::adapter_names() stub (Windows-side concept).
- linux/vaapi.cpp: include libavutil/pixdesc.h explicitly.
- confighttp.cpp: include boost/regex.hpp explicitly and guard the
  GetConsoleWindow branch of the boom endpoint.
- Boost_Sunshine.cmake: add the regex component (used by confighttp).
- tests: exclude tests/unit/platform/windows from the aggregate suite on
  non-Windows hosts.
- packaging/arch-local/PKGBUILD packages the prebuilt build/ tree (the
  repo must be configured with CMAKE_INSTALL_PREFIX=/opt/sunshine so the
  compiled-in assets path matches) via cmake --install under DESTDIR.
- Restore the tray status icons (sunshine-playing/pausing/locked.svg)
  from upstream; their install rule survived but the source files were
  lost in the WebUI redesign.
Adapt upstream's sunshine.install for the /opt layout: grant
cap_sys_admin to the packaged binary on install and upgrade (so the cap
survives package updates), reload udev rules and trigger uinput/uhid.
The Linux tray_menus layout omits the two Windows-only entries (VDD
menu, advanced settings), so the fixed indices in update_menu_texts()
walked past the array and dereferenced a NULL submenu on Linux -
segfaulting at startup right after encoder probing. Give each platform
its own index block instead.
Implement the vdd_utils surface on Linux on top of the external
sunshineVD helper (sunshine-virt-display) instead of the Windows-only
ZakoVDD driver:

- create/destroy translate to the daemon's Unix socket commands
  (--connect with the cached session mode / --disconnect);
- find_device_by_friendlyname reports the daemon's live virtual
  connector (state file + /sys status), which is what display session
  staging waits for;
- set_vdd_session_mode / wait_for_mode_publication cache the client's
  requested mode and re-issue connect so the EDID matches the stream;
- get_vdd_status maps the helper socket to the driver status surface.

vdd_capability::query_state() now uses the same status mapping on every
platform, so clients may request a virtual display on Linux whenever the
helper daemon is running.
The serverinfo field and the display_control response hardcoded
capability_version=0 on non-Windows, so clients never offered the
virtual display option even though the Linux backend now reports ready.
Send the protocol version unconditionally - the socket backend
implements the same session semantics.
…n socket

Replace the sunshineVD socket backend with a direct implementation of
the fork's virtual display semantics:

- vdd_edid: byte-for-byte port of sunshineVD's EDID generator (base
  block with the requested preferred timing + CEA extension carrying
  BT.2020 colorimetry and an HDR static metadata block); locked to the
  reference implementation with byte-equality unit tests.
- vdd_utils (Linux): create/destroy override a spare disconnected
  connector's EDID via debugfs and force its status on, without touching
  physical displays; mode changes cycle the connector with a regenerated
  EDID; session teardown clears the override.
- kmsgrab: when a virtual display session is live, capture follows the
  plane backing the virtual connector instead of the enumeration order.
- packaging: grant cap_dac_read_search/cap_dac_override next to
  cap_sys_admin so the debugfs/sysfs writes succeed.
setcap grants permitted (not effective) capabilities, so the native
virtual display backend hit EACCES on /sys/kernel/debug/dri and the
filesystem_error took down the display_control endpoint. Raise the
granted caps into the effective set with a libcap RAII guard (same
pattern as the KMS capture path) around every privileged operation,
and use the non-throwing filesystem overloads in get_vdd_status.
…d output

When a virtual display session is live, kmsgrab must capture the plane
backing the virtual connector even if the client supplied a stale or
Windows-style display name (the previous index-fallthrough could select
a physical plane first).
- kmsgrab: name displays after their DRM connector (eDP-1, DP-1, ...) and
  resolve the configured/client-supplied name back at init; virtual
  display sessions still take priority over any supplied name.
- enum_available_devices (Linux): enumerate live connectors from sysfs so
  the headless-host guard stops skipping virtual display teardown after
  every stream, and stream display matching resolves by connector name.
- vdd_prep: implement display_off (exclusive mode) by powering off
  connected physical connectors for the session and restoring them on
  virtual display destruction; extend-style modes are the compositor's
  natural state.
- destroy on stream end now runs (unblocked by the enumeration fix),
  and create sweeps stale virtual connectors carrying our EDID signature.
- toggle_display_power toggles create/destroy, which is what the tray
  and input command expect; tray Foundation Display menu is enabled on
  Linux.
- main: SIGINT/SIGTERM wake the tray event loop so shutdown is graceful
  instead of hitting the 10s force-exit watchdog.
…uit wakes the loop

- A virtual connector goes live before the compositor assigns its CRTC,
  so encoder probes 200ms after creation found no matching plane and
  failed every encoder. Poll the connector map for up to 3s when a
  target connector is set.
- Tray Quit on Linux raised the shutdown event but left the main thread
  parked in the tray event loop, hanging the process forever (the 10s
  watchdog is only armed by signals). Wake the loop with end_tray().
…er CRTC wait

- find_one_of_the_available_devices() only ever matched the virtual
  display, so a client selecting the physical display (e.g. eDP-1) was
  rejected with 'display does not exist'. Match device id, display name
  and friendly name against the live connector enumeration instead.
- The compositor CRTC wait now bails out early when the target connector
  is no longer connected, instead of stalling every probe attempt.
NVIDIA does not emit a hotplug for a status-forced connector, so KWin
lists the output but leaves it disabled and never assigns a CRTC - every
encoder probe then failed with 'Couldn't find monitor'. After forcing
the connector on, run 'kscreen-doctor output.<connector>.enable' (the
session compositor on KDE) so KWin adopts and lights the display.
… compositor adoption observable

- resolve_display_intent: the fork client sends the fixed placeholder
  '23172' for its virtual display option; treat it (and ZAKO_NAME) as an
  explicit VDD target on Linux, same as the Windows VDD_NAME alias.
- create_vdd_monitor now retries 'kscreen-doctor output.<conn>.enable'
  and polls DRM until the compositor hands out a CRTC (up to 4s), with
  the command output captured into the log; mode switches re-run it.
- kmsgrab probe wait reduced to fast-fail (the real wait lives in
  creation), so failed probes no longer stall for seconds.
…down restore

- Port sunshineVD's force_crtc_assignment to the native backend: borrow
  the compositor's DRM master via pidfd_getfd (candidates found by
  scanning /proc fds for the card device), drop their master, create a
  dumb framebuffer in the connector's preferred mode and run the modeset,
  then restore compositor master. Replaces the KDE-specific
  kscreen-doctor enable (kept as a fallback). Needs cap_sys_ptrace in the
  packaging hook.
- enum_available_devices reports connectors we forced off for an
  exclusive session as inactive devices, so the session teardown's
  headless-host guard no longer skips virtual display destruction - the
  physical screen is restored when the stream ends.
…ctors

NVIDIA reports a status-forced-off connector as plain 'disconnected' in
sysfs, so the display enumeration dropped it and the session teardown's
headless-host guard concluded 'VDD is the only display' and skipped
virtual display destruction - leaving the physical screen dark after
exclusive streams and rejecting client selections of it afterwards.
The backend now reports its offlined physical connectors (by name) and
the Linux enumeration includes them as inactive devices; enumeration
contents are logged for diagnosis.
The helper split the status file path after the last '/' - yielding
'status' instead of the connector name. That poisoned everything built
on it: the offlined-physical list reported 'status' instead of 'eDP-1'
(so the display enumeration missed the forced-off screen and teardown
skipped restoring it), and the mode-switch kscreen-doctor call targeted
'output.status'. Take the parent directory name first.
The client's screen combination modes map through to_vdd_prep to
vdd_as_primary / vdd_as_secondary / display_off. Windows realizes the
primary/secondary distinction through topology order; KWin's equivalent
is the kscreen output priority, so apply_vdd_prep now sets the virtual
display to priority 1 (primary mode) or 2 behind the physical screens
(secondary mode) via kscreen-doctor, and teardown hands priority 1 back
to the physical screens. display_off keeps the power-off behavior.
Compositors without kscreen-doctor degrade to 'virtual display enabled'.
Gap backlog: D17 leaves the open list and D18 becomes a recorded
decision; §5.15 documents the EDID reader, the verified fallback and the
warning-level degradation message. The report gains progress item 35.
Covers EDID-based display friendly names and the explicit
compositor-unavailable warning.
GET /api/runtime/hdr answered "available: false" on Linux, so the WebUI
could never show which dynamic metadata a stream carries even though the
analyzer now runs there; Windows registers its pipeline from the capture
device (audit item F11).

The avcodec session now registers the same shared status on Linux:
hdr_mode (pq/hlg/sdr), the configured analysis mode, whether this session
actually has a statistics producer, and - only when analysis is active -
the formats the stream can really carry (hdr10_plus, hdr_vivid), matching
the Windows rule. scene_metadata_active flips on the first frame with
usable statistics and the entry is removed when the session ends; SDR
sessions register nothing, so an empty pipeline list means "no HDR
session". conversion_path is left empty because Linux converts no HDR
(KMS delivers PQ/HLG directly); the frontend hides that line for an empty
value instead of labelling it as a D3D11 path. A failure reason is set
when analysis was wanted but no producer exists.

The endpoint's available flag is no longer platform-hardcoded. Windows is
untouched: the registration block is Linux-only and its capture pipeline
keeps registering as before. The shared registry already has coverage in
test_video.cpp (an initially duplicated test of mine was removed).
Gap backlog: F11 leaves the open list and §5.16 documents what the Linux
session publishes, why conversion_path stays empty and how Windows is
unaffected. The report gains progress item 36.
Covers the Linux HDR runtime status publication.
is_topology_valid accepted multi-device groups while set_topology
rejects them, so a caller could pass validation and then fail inside the
setter with a different message (audit item D16). Reject them in the
validator with the same reasoning: this backend cannot express a
duplicated group at all until a compositor-side backend exists, whereas
Windows accepts up to two devices per group. The VDD prep paths only
build singleton groups, so behavior there is unchanged.

Also hoist the virtual microphone's description into a named constant
next to its sink name, and note that Windows has no counterpart to align
with - its capture device is named by the VB-Cable driver (audit item
A6).
Gap backlog: D16 becomes §5.17 R29 with the A6 constant, and the items
that were re-checked and kept as-is move to §5.18 with their reasons
(empty-container contract, microphone contract edges, log language,
foreground exe semantics, histogram estimator, and the replication
topology whose kscreen-doctor limitation was verified against the
installed tool). The report gains progress item 37.
Covers the topology validator fix and the microphone naming constant.
D7, F3' and F9 are recorded decisions now (§5.18), so their entries in
the §5.2 backlog were duplicates. §5.2 now lists only what is actually
open: F4 (HLG analysis) at medium and C5 (cross-language constant sync)
at low.
Add §5.19: every file in this workstream that compiles on Windows, with
the evidence that its Windows behaviour is unchanged (verbatim-moved VDD
helpers, the Windows capture device always setting `data` so the CPU
analyzer paths are unreachable there, platform-gated hardware download /
Vivid / status publication, numerically identical shared constants,
byte-identical Windows tray statements and strings, additive shared
headers) and the single deliberate exception (HDR10+ metadata is now
attached on the first frame with real statistics instead of shipping
fabricated placeholders).

Add §5.20: the honest remaining list - HLG-domain analysis (F4), the
niri / wlr-output-management output backend, the replication topology,
and the recorded deliberate divergences in §5.18. C5 becomes a decision
record (no cross-language codegen for five protocol constants; the C++
mirror is covered by the wire tests), which clears the low-priority
section. The report gains progress item 38 with the final verification.
Creating a virtual display from the tray crashed with SIGSEGV on KDE.
systemd-coredump put the top frame in
display_device::vdd_utils::(anonymous)::enable_output_via_compositor with
frames AlkaidLab#1-AlkaidLab#12 at one single address: the KDE branch of the new dispatcher
called enable_output_via_compositor(connector) instead of running the
command it replaced, so every VDD creation and live mode switch on Plasma
overflowed the stack. The commit that introduced it (ff16fde) also
claimed the KDE path was unchanged byte-for-byte; it was not.

Both call sites (create_vdd_monitor, wait_for_mode_publication) keep using
the same helper, but the desktop -> command mapping is now a pure function,
platf::compositor_output::enable_command() in a new Linux-only module, and
the dispatcher only reads the probes and hands the string to run_logged.
The KDE command is the exact pre-ff16fdeb one ("kscreen-doctor
output.<name>.enable") and stays ungated on tool_available, as before. The
niri_session()/tool_available() probes move to that module too, so
hint_primary_output() shares one implementation.

tests/unit/test_compositor_output.cpp pins every branch (KDE/plasma, niri,
wlr-randr, xrandr, unknown session) plus the probe semantics, so a mapping
regression cannot hide behind the env-dependent dispatch again.
Both documents now describe the SIGSEGV, its root cause (a self-recursive
KDE branch in the compositor-output dispatcher) and the fix, including the
correction of the "KDE path unchanged" claim made when the dispatcher was
introduced. LINUX_PORT_GAPS.md gains section 5.20 for this round and the
remaining-work list moves to 5.21; the test baseline is updated to the
current 528 cases / 515 passed / 0 assertion failures, with the one
sandbox-caused network failure called out explicitly.
With a virtual display the session's mode and HDR maps were built for the
physical screen and then dropped by filter_stale_devices(): config.device_id
is resolved in make_parsed_config(), which runs *before* prepare_vdd()
creates the VDD, so it still names the panel the client was looking at (or
is empty). apply_config() therefore targeted eDP-1 while the VDD-only
topology contained only DP-2, both maps came out empty, set_display_modes()
no-opped on the empty map and the "guaranteed by the EDID" tolerance hid
the rest: the resolution and HDR state the client asked for never reached
the virtual display, while the panel kept whatever it had.

In VDD mode the streamed display is the virtual one and the session's VDD
stage already owns the topology, so resolve the target to the live VDD
connector instead, and log the substitution when it differs from the
requested id.
Around every debugfs probe the connector scan used the throwing
std::filesystem overloads, so a caller without the file capabilities (a
plain ./sunshine run, or a build whose capabilities were dropped) died with
an uncaught std::filesystem_error as soon as a connector was connected and
adopt_orphan_vdd_locked()/edid_override_path() looked for the override node.
The aggregate test suite hit exactly that, aborting
DisplayDeviceEnum.ActiveRequiresAnEnabledConnector.

All four probes now use the error_code overloads and treat an unreadable
directory as "no override node", which is what the capability-equipped
service sees for a card without one.
The client's requested resolution could silently fail to take effect on the
virtual display. Three assumptions caused it, all of them wrong:

- is_mode_advertised() was a stub that answered "yes" whenever a VDD was
  live, so wait_for_mode_publication() never verified anything and the
  session proceeded no matter what the display advertised. It now asks the
  compositor (the Wayland analogue of EnumDisplaySettings) through a new
  platf::kscreen::advertised_modes() accessor that reuses the existing
  kscreen-doctor parser, matches with the shared +/-1 Hz semantics, polls
  for kModePublicationTimeout and, on failure, logs the modes that *are*
  published before reporting the session mode as unpublished. Non-KDE
  sessions keep the previous live-connector answer: an unavailable query is
  "unknown", never "not advertised".
- The generator saturates the DTD's 16-bit pixel clock, so a mode above
  655.35 MHz (3840x2160@120, 3440x1440@120, ...) was written as a *different*
  refresh (4K120 came out as 3840x2160@71.4). Such a mode is now rejected
  with an explicit error -- at creation, when the live list is updated, and
  in the publication check -- instead of replacing a valid EDID with a lying
  one. The limit is a named constant (vdd_edid::kMaxDtdPixelClockHz) because
  the DTD field size is a format property, not a tunable.
- The EDID built at creation used the configured mode lists only, while
  Windows' SETMODES list carries the client's resolution and refresh rate
  too (cross product with every configured rate/resolution). The session's
  prepared settings now survive from set_vdd_session_mode() to
  create_vdd_monitor() for that purpose, and the stale cached_from_session
  flag is cleared on destroy so a later tray creation goes back to the
  configured preferred mode instead of reusing a finished session's mode.

set_display_modes() failures on the virtual display are still tolerated, but
only while the mode really is advertised; otherwise the session reports
modes_fail like Windows does.

Tests: EDID feasibility boundaries and the saturation failure mode
(test_vdd_edid), plus the accessor contract (test_kscreen_modes, Linux-only).
New round section in LINUX_PORT_GAPS.md (5.21) and report item 40 describe the
three defects behind the report, with the log evidence (empty "Changing display
modes to:"/"Changing HDR states to:" for VDD sessions), the correction of the
earlier "the blanking model is more conservative than CVT-R" claim -- cvt -r
needs 658.25 MHz for 3440x1440@120, so those modes are simply not expressible
in a base-block DTD -- and an honest DisplayID follow-up. The remaining-work
list moves to 5.22 and the stale lock-granularity hazard is marked fixed. Test
baseline updated to 532 cases / 519 passed / 0 assertion failures.
The client asked for 2400x1080@60 and got 2400x1080@144: the EDID's
preferred timing carried the requested mode (so the resolution was right),
but filter_stale_devices() erased every VDD entry from the mode and HDR maps
by design -- the original backend assumed the EDID made compositor-side mode
changes unnecessary. KDE therefore kept the mode it picked when the connector
came up, which is the highest refresh rate the EDID offers.

Windows applies the client's requested mode and HDR state to the VDD through
CCD, so the maps now preserve the virtual display being configured while
still dropping virtual displays that belong to another session or to no
topology at all (the original/rollback bookkeeping keeps stripping them: a
mode restore must not touch a VDD that is about to be destroyed).

The "EDID guarantees the mode" tolerance after a failed set_display_modes()
now checks the *current* mode instead of the advertised list: KDE lists every
rate the EDID carries, so an unapplied 60 Hz request would otherwise pass
while the display ran at 144 Hz. Only a display that already runs the session
mode is tolerated.
Adds packaging/aur/sunshine-foundation-git (PKGBUILD, .SRCINFO, install
script, README) so the fork can be built from source with makepkg instead
of only via the local prebuilt-tree package in packaging/arch-local.

makepkg neither checks out gitlinks nor has network access in build(), so
every submodule the build consumes - including the nested ones under
third-party/{doxyconfig,moonlight-common-c,tray} - is declared as a pinned
source entry and restored into its gitlink path in prepare(). The two
oversized Windows/platform checkouts are sparse-checked out: third-party/AMF
(answer: none, Windows-only) and third-party/build-deps (this machine's
dist/Linux-<arch> slice only).

Boost is pinned to an exact version by Boost_Sunshine.cmake, so the package
depends on that exact boost/boost-libs and documents the coupling; without it
CMake silently fetches and builds Boost from GitHub inside build().

options=(!strip) keeps the file capabilities the install script sets
(same set as packaging/arch-local), which Sunshine needs for KMS capture,
connector EDID/status access and the DRM-master borrow used by the virtual
display.
…SCII name

`makepkg -si` died in package() with

  CMake Error at build/cmake_install.cmake:118 (file):
    file INSTALL cannot find "build/sunshine-2026.0914.211531.376057a2"

The executable name carries the configure timestamp plus the current commit
(cmake/prep/build_version.cmake) and cmake bakes it into cmake_install.cmake,
so re-running cmake after a commit without relinking -- the normal slip after
a pkgrel-only bump -- leaves the install rules naming a binary that was never
built. prepare() now detects that (install rules and the sunshine symlink
disagree) and finishes the job with cmake + ninja, or fails with the actual
configure command when build/ is not configured at all. package() no longer
hides the CMake error behind >/dev/null and explains the fix.

A dirty tree appends the "杂鱼" marker to the stamp, which libarchive cannot
translate ("bsdtar: Can't translate ... to UTF-8") and which leaves pacman
with a path it cannot reliably match; the staged binary is now renamed to an
ASCII stamp (…dirty) and the sunshine symlink re-pointed at it. The version
string compiled into the binary is unchanged.

The install hook also refuses to run setcap on a dangling symlink (a package
whose stamped binary is missing would otherwise install a capability-less
binary that cannot capture or create virtual displays).
A test box whose session is niri (Plasma available but niri starts) and a
service started from SSH both blocked for a long time on client connection,
with kscreen-doctor hitting the 10 s run_logged timeout.

kscreen-doctor drives org.kde.KScreen over the session bus. Where there is no
KScreen, asking anyway makes D-Bus activate a KScreen service that waits for a
Plasma session, so the tool never returns by itself and only dies at the kill
timeout; the previous code spawned it for every display query, and a session
start issues dozens (enumeration, topology, modes, HDR). The "unavailable"
static only suppressed the log line, not the spawn.

platf::kscreen now owns both the availability decision and the runner:

- session_supports_kscreen() reads NIRI_SOCKET first (niri has no KScreen even
  when XDG_CURRENT_DESKTOP claims KDE for toolkit compatibility) and otherwise
  requires a Plasma desktop string. It is evaluated per call, never cached, so a
  service that starts before the desktop still picks it up afterwards. A session
  that cannot have KScreen is never probed: 0 ms per query instead of 10 s.
- queries use a 1.5 s probe timeout, and a failed probe arms a 20 s cooldown, so
  a broken KScreen costs at most one short probe per window rather than a 10 s
  hang per query. A successful query clears the cooldown.
- change commands keep the 10 s bound and are only reached after a successful
  query (or a session that supports KScreen).

Measured with the real code: KDE session 699 ms / 1 mode (unchanged), niri 0 ms,
no desktop 0 ms, KDE-with-unreachable-compositor 807 ms once then 0 ms.
The VDD layer's kscreen commands got the same guard: a niri session is routed to
its own IPC instead of being sent a command that can only hang.

tests/unit/test_kscreen_backend.cpp pins the gate (env combinations) and the
cooldown transitions; it is Linux-only like the module.
package() could ship a binary whose version stamp names an older commit: the
stamp is fixed at configure time, so a fresh commit does not change it, and
the install-rules check alone only caught the case where cmake named a binary
that was never linked. `makepkg` now compares the stamp's embedded commit (and
its dirty marker) with the tree and re-configures plus relinks when they
disagree, so the `Sunshine version:` line the user checks always identifies
the tree that was packaged. The check is free when they already match.
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