Skip to content

[WIP] Linux migration - #1054

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

QiE2035 wants to merge 165 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'.
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.
The Linux nvenc entry declared AV_PIX_FMT_NONE for both 4:4:4 slots and
carried no YUV444_SUPPORT flag, so 4:4:4 client requests were rejected at
prepare_encoder while Windows nvenc has always supported it.

- video.cpp: declare AV_PIX_FMT_YUV444P (8-bit) and AV_PIX_FMT_YUV444P16
  (10-bit shifted container, NV_ENC_BUFFER_FORMAT_YUV444_10BIT) plus the
  YUV444_SUPPORT flag; override profile=high with
  AV_PROFILE_H264_HIGH_444_PREDICTIVE in the h264 sdr444 option group since
  the plain SDR options apply to 4:4:4 sessions too and the av_dict value
  would win over ctx->profile. Support stays probe-gated by real test
  encodes (h264 444 SDR 8-bit, hevc/av1 444 HDR 10-bit): Ampere does
  h264/hevc 444, AV1 444 needs Ada.
- The CUDA=OFF production path needs no platform change: the generic
  avcodec_software_encode_device_t (swscale + av_hwframe_transfer_data) is
  format-agnostic.
- graphics: teach egl::create_target()/sws_t about planar 4:4:4 (three
  full-resolution single-channel planes). U/V planes reuse the ConvertY
  shader driven by two extra ColorMatrix UBOs with the UV range multiply
  and offset folded into the matrix (identity:
  (dot(cu,rgb)+cu.w)*ru.x+ru.y == dot(cu*ru.x,rgb) + (cu.w*ru.x+ru.y)),
  so no new shader assets; convert() renders 3 full-size passes.
- cuda.cpp: gl_cuda_vram_t registers/maps/copies N planes instead of the
  hardcoded Y+UV pair; per-plane WidthInBytes/Height still derive from the
  pixel-format descriptor.
- video.cpp analyzer: accept AV_PIX_FMT_YUV444P16LE as MSB-aligned (>>6)
  like P010, so 4:4:4 HDR sessions keep producing HDR10+/DV L1 statistics;
  planar P10 formats keep the LSB semantics from the P13 fix.
- The CUDA=ON ram fallback (cuda::sws_t RGBA_to_NV12 kernel) stays
  NV12-only; those capture backends fail 4:4:4 sessions via the probe.
  VAAPI 4:4:4 remains undeclared (LINUX_PORT_GAPS.md 2.12).
The hand-rolled sd_bus_new + set_address + start path never set the
bus-client flag, so sd-bus skipped the Hello() handshake: the socket
connected and authenticated but acquired no unique name, and dbus-broker
reset the connection on the first method call (ECONNRESET). Every klipper
clipboard call failed this way, and the KWin foreground tracker in
foreground_app.cpp (vtable registration + NameHasOwner probe) was silently
degraded by the same helper.

Setting sd_bus_set_bus_client(1) mirrors what sd_bus_open_user() does
internally; verified with a minimal repro against klipper on dbus-broker.
Port two acceptance bounds from the Rust agent (clipboard.rs): outbound
reads keep only non-empty text (klipper returns an empty string for
non-text content such as a copied image, which previously went out as an
empty frame and blanked the client clipboard), and inbound text carrying
NUL bytes or empty is dropped, matching the agent's
'!s.is_empty() && !s.contains(\0)' guard.

Report: entry 43 (round 20) documents the D-Bus handshake fix and this
parity pass with the repro evidence.
…on Linux

A client-side dynamic bitrate change reached rate control (the control-stream
handler, set_dynamic_param and set_bitrate all run) but only the bitrate fields
were updated, so the VBV window stayed sized for the previous rate: too small
for a higher rate (the encoder is starved), too large for a lower one (bursts).

The native Windows NVENC path already handles this -- nvenc_base::set_bitrate()
recomputes vbvBufferSize proportionally -- so mirror it here, with the same
100 kbit floor, and log the encoder-visible values (rate + VBV) so the log shows
what FFmpeg's nvenc reconfig_encoder() is about to apply (it picks the changed
fields up per frame via nvEncReconfigureEncoder with resetEncoder + forceIDR).
…ff at the DRM level

Reported: sometimes the physical screen did not come back after a session. The
display_off prep wrote sysfs `status = off` for every connected physical, a DRM
level force that (a) survives a crash, because the process that would have
restored it is gone, and (b) emits no hotplug on NVIDIA, so nothing recovers it.

The prep now asks the compositor to switch the physical outputs off (which owns
the layout and puts it back) and refuses to touch DRM: with no compositor output
backend (niri/wlroots/X11 today) it logs that the host screen stays on instead.

Because a forced-off connector is indistinguishable from an unplugged one, the
list of switched-off outputs is persisted to
<appdata>/vdd_offlined_physicals.txt, so a later process can restore them:
- teardown and the start of a non-exclusive session both restore,
- a fresh process restores as soon as it sees that no virtual display is live
  (the crashed-session case, without waiting for a stream),
- restore uses sysfs "detect" (clear any leftover force from an older build and
  let the kernel re-detect) plus the compositor's enable, never "on", which
  would fabricate a phantom output for a panel that has since been unplugged.

offlined_physical_connectors() now reports connector names, keeping the
enumeration's "powered down for a VDD session" view intact.
SUNSHINE_ENABLE_CUDA=ON never compiled: the fork's sync commit 468442f
("feat(video): use unified color conversion matrix generator (#4387)") merged
the new upstream file *after* the old one instead of replacing it, leaving two
copies of every helper and member function with unbalanced braces (final brace
depth -1). Nobody noticed because the local build always ran with CUDA off.

cuda.cu is now the last coherent version (468442f^, which matches this tree's
cuda.h: convert()/viewport/scale/threadsPerBlock) with the one-line change the
upstream PR actually made -- color_vectors_from_colorspace(colorspace, true).
Verified: the CUDA object compiles with CUDA 13.4 for sm_86 and the whole
CUDA-enabled binary links.

Building it also needs two CMake fixes:
- an explicit -DCMAKE_CUDA_ARCHITECTURES is now honoured (one real GPU builds
  far faster than the whole compatibility ladder),
- the ladder drops architectures the toolkit itself removed (CUDA 13 no longer
  accepts anything below Turing; passing sm_50 made nvcc abort with
  "Unsupported gpu architecture").
The Linux avcodec nvenc table overrode the YUV444 profile with
AV_PROFILE_H264_HIGH_444_PREDICTIVE (244). That is FFmpeg's *generic* profile
constant, but h264_nvenc's "profile" option takes NVENC's own enum (0-5), so
opening the encoder failed with

  Value 244.000000 for parameter 'profile' out of range [0 - 5]

which poisoned the 4:4:4 probe and forced the fallback path on every YUV444
session. Use nv::profile_h264_e::high_444p (3), the same enum the neighbouring
SDR entry already uses. The generic constant stays correct where it belongs:
ctx->profile, which FFmpeg maps internally.

Also make the NvFBC session failure readable: it now logs the status and says
the DRM/Wayland sources are used instead. The failure itself is expected on
consumer GeForce drivers (the privateData trick only works on some driver
versions), but it showed up as a bare "Failed to create session:" with an empty
driver message.
…ng its CRTC

A status-forced connector emits a DRM uevent and current KWin probes,
enables and CRTCs it itself within ~1.5s. That activation is one-shot:
racing it with the pidfd master-steal SETCRTC in force_crtc_assignment
fails KWin's atomic commit and it drops the output permanently, so
kscreen never lists the connector and the session dies on the mode set
(unknown output) with every encoder probe failing afterwards. Boot-flaky:
worked 9/20 and 9/23, failed 5/5 attempts on the 9/24 boot.

- creation and live mode-change now wait for kscreen to surface the
  connector first and skip the steal entirely when it does
- the steal demotes to a fallback for compositors that ignore
  status-forced connectors; when it lands but kscreen still cannot see
  the output, the connector is recycled (status off->on, EDID override
  survives) to hand the compositor a fresh activation
- on an observable compositor, creation fails fast when the connector
  never surfaces instead of reporting a bogus 'live'
- kscreen-doctor exits 0 on failures; display_off prep now parses
  'applying config failed!' instead of trusting the exit code
- AGENTS.md gotcha revised (no-hotplug claim disproven) + new
  exit-code-parsing gotcha; report entry 45 added
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