diff --git a/crates/pf-inject/src/lib.rs b/crates/pf-inject/src/lib.rs index ab817b03..3dde4586 100644 --- a/crates/pf-inject/src/lib.rs +++ b/crates/pf-inject/src/lib.rs @@ -58,7 +58,8 @@ pub enum Backend { /// KWin `org_kde_kwin_fake_input` — direct injection, no RemoteDesktop portal / approval dialog /// (authorized by the host's `.desktop`). The headless KDE-Desktop path; what krdpserver uses. KwinFakeInput, - /// libei via `reis` — Wayland-native (RemoteDesktop portal). + /// libei via `reis` — Wayland-native. Reaches EIS through the RemoteDesktop portal, or on + /// GNOME through Mutter's direct RemoteDesktop API (see `libei_ei_source`). Libei, /// libei directly against gamescope's own EIS socket (no portal): input lands in the /// nested game — the SteamOS-like session. @@ -173,9 +174,10 @@ pub fn absolute_anchor() -> Option { /// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the /// ScreenCast portal (no RemoteDesktop), so libei can't run there — use the wlr virtual-input /// protocols. **KWin** exposes `org_kde_kwin_fake_input` (direct injection, no portal / approval -/// dialog — the only headless-capable path; what krdpserver uses), so prefer it there. **GNOME** -/// has neither fake_input nor the wlr protocols, so it uses libei via the RemoteDesktop portal -/// (which needs a user to approve, or a pre-seeded grant — not truly headless). +/// dialog — authorized by the host's `.desktop`; what krdpserver uses), so prefer it there. +/// **GNOME** has neither fake_input nor the wlr protocols, so it uses libei — reaching EIS through +/// Mutter's *direct* `org.gnome.Mutter.RemoteDesktop` API rather than the portal +/// (`libei_ei_source`), so it is headless-capable too: no interactive approval to answer. /// `PUNKTFUNK_INPUT_BACKEND=wlr|kwin|libei|gamescope` overrides the auto-detection. #[cfg(target_os = "linux")] pub fn default_backend() -> Backend { diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index 02243889..bef9b31e 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -212,8 +212,10 @@ pub fn capture_virtual_output( }, ); // IDD direct-push is the sole Windows capture path: consume frames straight from the pf-vdisplay - // driver's shared ring (in-process, Session 0 — it captures the secure desktop too; no Desktop - // Duplication, no WGC helper). A FRESH monitor + ring is created per session. `want.hdr` + // driver's shared ring (in-process — no Desktop Duplication, no WGC helper). The host itself runs + // as SYSTEM in the active interactive console session (1+), spawned there by the session-0 SCM + // supervisor (`windows/service.rs`), which is what lets it capture the secure desktop too. + // A FRESH monitor + ring is created per session. `want.hdr` // proactively enables advanced color and selects the per-frame conversion. There is NO fallback: // if it can't open or the driver doesn't attach, the session fails cleanly and the client // reconnects. diff --git a/crates/punktfunk-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs index 71225a0f..147e4cf0 100644 --- a/crates/punktfunk-host/src/gamestream/mod.rs +++ b/crates/punktfunk-host/src/gamestream/mod.rs @@ -52,8 +52,8 @@ pub const SCM_AV1_MAIN10: u32 = 0x0002_0000; /// The **SDR baseline** codec mask: H.264, HEVC Main, AV1 Main 8-bit (= 65793). HEVC Main10 (HDR) is /// layered on top of this at runtime by `serverinfo::codec_mode_support` when — and only when — the /// host can actually deliver it ([`host_hdr_capable`]); it is never a static claim, because a non-HDR -/// host (a host without the `PUNKTFUNK_10BIT` opt-in, or a Linux host whose video source / encoder -/// can't do Main10) must not invite a client into an HDR mode it can't produce. (The previous placeholder 3843 = 0xF03 wrongly claimed HEVC Main10 + +/// host (a host where `PUNKTFUNK_10BIT` was explicitly turned off, or a Linux host whose video +/// source / encoder can't do Main10) must not invite a client into an HDR mode it can't produce. (The previous placeholder 3843 = 0xF03 wrongly claimed HEVC Main10 + /// 4:4:4 and *no* AV1.) 4:4:4 stays off entirely on GameStream: stock Moonlight is 4:2:0 — /// full-chroma is a punktfunk/1-native negotiation only (`crate::capture::capturer_supports_444`). pub const SERVER_CODEC_MODE_SUPPORT: u32 = SCM_H264 | SCM_HEVC | SCM_AV1_MAIN8; @@ -62,8 +62,9 @@ pub const SERVER_CODEC_MODE_SUPPORT: u32 = SCM_H264 | SCM_HEVC | SCM_AV1_MAIN8; /// `IsHdrSupported` per app, for layering the 10-bit codec bits in serverinfo, and (together with /// the live capture-side check and the session's own codec at RTSP time) for honoring a client's /// `dynamicRangeMode` request. Host-wide and codec-agnostic on purpose: the per-codec depth -/// question belongs to whoever knows which codec is in play. Behind the operator's `PUNKTFUNK_10BIT` opt-in — the same policy gate the native -/// punktfunk/1 plane honors — on both OSes. +/// question belongs to whoever knows which codec is in play. Behind the host's `PUNKTFUNK_10BIT` +/// policy gate — **default ON**, explicit-off grammar (`=0`/`false`/`off`/`no` disables), the same +/// gate the native punktfunk/1 plane honors — on both OSes. /// /// **Windows**: the IDD-push capturer streams HEVC Main10 PQ whenever the desktop is HDR, and a /// client HDR request proactively enables advanced color on the per-session virtual display so PQ diff --git a/crates/punktfunk-host/src/session_plan.rs b/crates/punktfunk-host/src/session_plan.rs index d5a24897..0709015d 100644 --- a/crates/punktfunk-host/src/session_plan.rs +++ b/crates/punktfunk-host/src/session_plan.rs @@ -26,7 +26,8 @@ pub enum CaptureBackend { /// Linux: the xdg ScreenCast portal → PipeWire (the only Linux capture path). Portal, /// Windows: IDD direct-push — frames pulled straight from the pf-vdisplay driver's shared ring - /// (in-process, Session 0; captures the secure desktop too). The sole Windows capture path — + /// (in-process; the host runs as SYSTEM in the interactive console session, so it captures the + /// secure desktop too). The sole Windows capture path — /// DXGI Desktop Duplication (DDA) and the WGC two-process relay were removed. IddPush, } @@ -55,7 +56,8 @@ impl CaptureBackend { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SessionTopology { /// One process captures + encodes. The only topology: Linux (portal) and Windows (in-process - /// IDD-push in Session 0). The SYSTEM-host + user-session WGC relay was removed with DDA/WGC. + /// IDD-push, in the host's SYSTEM process in the interactive console session). The SYSTEM-host + /// + user-session WGC relay was removed with DDA/WGC. SingleProcess, } @@ -237,7 +239,8 @@ impl SessionPlan { } /// Process topology. Single-process is the only topology now: Linux (portal) and Windows (in-process -/// IDD-push in Session 0). The Windows SYSTEM-host + user-session WGC relay was removed with DDA/WGC. +/// IDD-push, in the host's SYSTEM process in the interactive console session). The Windows +/// SYSTEM-host + user-session WGC relay was removed with DDA/WGC. pub(crate) fn resolve_topology() -> SessionTopology { SessionTopology::SingleProcess } diff --git a/crates/punktfunk-host/src/windows/game_term.rs b/crates/punktfunk-host/src/windows/game_term.rs index dcd95d7e..d402c3ff 100644 --- a/crates/punktfunk-host/src/windows/game_term.rs +++ b/crates/punktfunk-host/src/windows/game_term.rs @@ -7,8 +7,10 @@ //! ### Why the desktop dance //! //! The polite half is the reason this module is not three lines. `EnumWindows` enumerates the windows -//! of the **calling thread's desktop** — and the host runs as SYSTEM in session 0, whose desktop has -//! none of the interactive user's windows on it. Without first binding the thread to the input desktop +//! of the **calling thread's desktop** — and the host runs as SYSTEM. It is in the right *session* +//! (the supervisor launches it into the active interactive console session — see [`super::service`]), +//! but a SYSTEM process does not start on the interactive user's *desktop* within it, so the +//! enumeration sees none of their windows. Without first binding the thread to the input desktop //! the enumeration comes back empty and the ladder silently degrades to killing every game outright, //! which is exactly the unsaved-progress outcome the grace window exists to avoid. //! diff --git a/crates/punktfunk-host/src/windows/service.rs b/crates/punktfunk-host/src/windows/service.rs index f7e635f4..79d203e9 100644 --- a/crates/punktfunk-host/src/windows/service.rs +++ b/crates/punktfunk-host/src/windows/service.rs @@ -861,9 +861,9 @@ fn ensure_default_host_env() -> Result<()> { PUNKTFUNK_ENCODER=auto\n\ PUNKTFUNK_VIDEO_SOURCE=virtual\n\ # Virtual display = the bundled pf-vdisplay driver; capture is IDD-push from its shared ring\n\ - # (the sole capture path — zero-copy, includes the secure desktop; DDA/WGC were removed).\n\ + # (the sole capture path — zero-copy; DDA/WGC were removed). The secure desktop (UAC / lock /\n\ + # login) is always captured — there is no setting for it.\n\ PUNKTFUNK_VDISPLAY=pf\n\ - PUNKTFUNK_SECURE_DDA=1\n\ RUST_LOG=info\n\ \n\ # The host subcommand the service launches (default: serve --gamestream = native + Moonlight\n\ diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index c03df0c4..7a26a5cc 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -168,8 +168,8 @@ the host needs a clipboard backend underneath. Both, and what a greyed-out toggl Capture of the **secure desktop** — UAC prompts, the lock screen, the login screen — is always on and has no setting: the host reads the pf-vdisplay driver's ring directly, and those surfaces are in -it. The service still writes a `PUNKTFUNK_SECURE_DDA=1` line into a freshly generated `host.env` and -nothing reads it — leave it or delete it, it makes no difference. +it. If an older `host.env` on your machine still carries a `PUNKTFUNK_SECURE_DDA` line, nothing reads +it — leave it or delete it, it makes no difference. | Setting | Values | Meaning | |---|---|---| diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index a7d90eca..1cc85229 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -148,11 +148,20 @@ sudo rpm-ostree update \ systemctl reboot ``` -Or just run the helper, which detects what's layered and does the above: +Or just run the helper, which detects what's layered and does the above. The `punktfunk` RPM +installs it, so on a layered box it's already there — no checkout needed (it's the same command the +web console's update hint prints): ```sh -sudo bash packaging/bazzite/update-punktfunk.sh # stage; reboot when ready -sudo bash packaging/bazzite/update-punktfunk.sh --reboot # stage + reboot now +sudo /usr/share/punktfunk/update-punktfunk.sh # stage; reboot when ready +sudo /usr/share/punktfunk/update-punktfunk.sh --reboot # stage + reboot now +``` + +From a repo checkout (e.g. before the first install, or to run a newer helper than the layered RPM +carries) the same script runs directly — it only shells out to `rpm-ostree`/`rpm`/`systemctl`: + +```sh +sudo bash packaging/bazzite/update-punktfunk.sh --reboot ``` > **Channel gotcha:** the re-resolve picks the highest version across **every enabled** @@ -231,8 +240,10 @@ KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", GROUP="input", MODE="0660", TAG+="uac ## 4. Configure `host.env` The systemd user unit reads its environment from **`~/.config/punktfunk/host.env`** -(`EnvironmentFile=%h/.config/punktfunk/host.env` in `scripts/punktfunk-host.service`). The RPM -ships a Bazzite-tuned template at `/usr/share/punktfunk/host.env.bazzite`. Copy it into place: +(`EnvironmentFile=-%h/.config/punktfunk/host.env` in `scripts/punktfunk-host.service` — the `-` +makes it optional, since no package creates the file; absent just means every knob at its default). +The RPM ships a Bazzite-tuned template at `/usr/share/punktfunk/host.env.bazzite`. Copy it into +place: ```sh mkdir -p ~/.config/punktfunk diff --git a/packaging/bazzite/update-punktfunk.sh b/packaging/bazzite/update-punktfunk.sh index a82f97ab..ed5eb79c 100755 --- a/packaging/bazzite/update-punktfunk.sh +++ b/packaging/bazzite/update-punktfunk.sh @@ -39,7 +39,7 @@ if [[ ${#layered[@]} -eq 0 ]]; then mapfile -t layered < <(rpm -qa --qf '%{NAME}\n' 'punktfunk' 'punktfunk-web' 2>/dev/null | sort -u) fi if [[ ${#layered[@]} -eq 0 ]]; then - echo "no punktfunk packages are layered — install first (see packaging/bazzite/README.md)" >&2 + echo "no punktfunk packages are layered — install first (see https://docs.punktfunk.unom.io/docs/bazzite)" >&2 exit 1 fi echo "layered punktfunk packages: ${layered[*]}" diff --git a/packaging/rpm/punktfunk.spec b/packaging/rpm/punktfunk.spec index 5fe3f972..43dcfdc4 100644 --- a/packaging/rpm/punktfunk.spec +++ b/packaging/rpm/punktfunk.spec @@ -393,6 +393,15 @@ install -Dm0644 packaging/kde/host.env %{buildroot}%{_datadir}/% # screencast/virtual-output grant ships as io.unom.Punktfunk.Host.desktop, installed above). install -d %{buildroot}%{_datadir}/%{name}/bazzite install -Dm0755 packaging/bazzite/kde-desktop-setup.sh %{buildroot}%{_datadir}/%{name}/bazzite/kde-desktop-setup.sh +# Layered-update helper for rpm-ostree hosts: `rpm-ostree upgrade` only re-resolves layered +# packages when the BASE changes, so a frozen Bazzite base pins punktfunk forever. The script +# forces a re-resolve of just this layer (--uninstall + --install of the same names in one +# transaction). It is exactly the command pf-update-check hands an rpm-ostree host +# (`sudo /usr/share/punktfunk/update-punktfunk.sh`, crates/pf-update-check/src/detect.rs), so it +# has to exist at that path — an ostree box has no repo checkout to run it from. It only shells +# out to rpm-ostree/rpm/systemctl, so the installed copy is self-contained. Top level, not +# bazzite/, because the hint (and any Fedora-Atomic host) names that path. +install -Dm0755 packaging/bazzite/update-punktfunk.sh %{buildroot}%{_datadir}/%{name}/update-punktfunk.sh # Headless GAME-mode fix: a gamescope-session-plus sessions.d drop-in that falls back to gamescope's # headless backend when no display is connected (so "Switch to Game Mode" works on a display-less # streaming host instead of crashing + 5-striking back to desktop). No-op on display-attached boxes. diff --git a/scripts/host.env.example b/scripts/host.env.example index 95393cf5..abd3e9a3 100644 --- a/scripts/host.env.example +++ b/scripts/host.env.example @@ -55,7 +55,10 @@ PUNKTFUNK_VIDEO_SOURCE=virtual # mid-stream. Fine for a dedicated headless appliance (punktfunk-kde-session.service, a pure # gamescope box) or a CI run; wrong for any box that switches sessions. #PUNKTFUNK_COMPOSITOR=kwin # kwin | mutter | gamescope | wlroots | hyprland -#PUNKTFUNK_INPUT_BACKEND=libei # wlr | libei | gamescope | uinput (auto-routed per connect) +#PUNKTFUNK_INPUT_BACKEND=kwin # wlr | kwin | libei | gamescope — anything else logs "unknown + # PUNKTFUNK_INPUT_BACKEND" and auto-detects instead: + # PUNKTFUNK_COMPOSITOR above wins (except mutter), else + # XDG_CURRENT_DESKTOP — KDE -> kwin, GNOME -> libei, else wlr #WAYLAND_DISPLAY=wayland-kde # headless-KDE appliance socket; retargeted per connect otherwise #XDG_CURRENT_DESKTOP=KDE diff --git a/scripts/punktfunk-host.service b/scripts/punktfunk-host.service index bd1a5c16..e437e438 100644 --- a/scripts/punktfunk-host.service +++ b/scripts/punktfunk-host.service @@ -45,7 +45,13 @@ After=pipewire.service punktfunk-kde-session.service PartOf=punktfunk-kde-session.service [Service] -EnvironmentFile=%h/.config/punktfunk/host.env +# '-' optional: NO package creates this file — deb/RPM/Arch ship only host.env.* templates under +# /usr/share and the postinst merely prints the copy command — so a mandatory read would fail a +# fresh install outright ("Failed to load environment files"), before the host ever ran. Absent +# just means every knob at its default (pf-host-config has one for each), which is exactly what +# `punktfunk-host serve` by hand already does. Contrast punktfunk-web.service, which marks +# mgmt-token mandatory ON PURPOSE so Restart waits for the host to write it — this is not that. +EnvironmentFile=-%h/.config/punktfunk/host.env ExecStart=%h/punktfunk/target/release/punktfunk-host serve --gamestream Restart=on-failure RestartSec=2 diff --git a/scripts/steamdeck/install.sh b/scripts/steamdeck/install.sh index 11e6295b..0af0f61a 100755 --- a/scripts/steamdeck/install.sh +++ b/scripts/steamdeck/install.sh @@ -319,7 +319,7 @@ Description=punktfunk host (GameStream + punktfunk/1) After=pipewire.service [Service] -EnvironmentFile=%h/.config/punktfunk/host.env +EnvironmentFile=-%h/.config/punktfunk/host.env Environment=XDG_RUNTIME_DIR=$XRD Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=$XRD/bus ExecStart=$BIN $SERVE_ARGS diff --git a/scripts/windows/host.env.example b/scripts/windows/host.env.example index f7471eb7..f8985aaf 100644 --- a/scripts/windows/host.env.example +++ b/scripts/windows/host.env.example @@ -23,12 +23,10 @@ PUNKTFUNK_VIDEO_SOURCE=virtual # backend now (the legacy SudoVDA backend was removed). This is informational; leave it as `pf`. PUNKTFUNK_VDISPLAY=pf -# Capture is IDD-push: straight from the pf-vdisplay driver's shared ring — zero-copy, includes the -# secure desktop. It is the SOLE capture path (DDA/WGC were removed; the former PUNKTFUNK_IDD_PUSH -# knob is gone — a stale setting is ignored). - -# Capture the secure desktop (UAC / lock / login) so the stream survives those transitions. -PUNKTFUNK_SECURE_DDA=1 +# Capture is IDD-push: straight from the pf-vdisplay driver's shared ring — zero-copy, and it always +# includes the secure desktop (UAC / lock / login), so the stream survives those transitions. It is +# the SOLE capture path (DDA/WGC were removed; the former PUNKTFUNK_IDD_PUSH and PUNKTFUNK_SECURE_DDA +# knobs are gone — secure-desktop capture has no setting, and a stale line is ignored). # The name this host shows up under in Moonlight and the Punktfunk clients. Defaults to the # machine's computer name; set it to give the box a friendly name without renaming Windows. @@ -48,8 +46,11 @@ RUST_LOG=info # custom value you write here by hand is never overwritten by a reinstall/upgrade. #PUNKTFUNK_HOST_CMD=serve --gamestream -# Multi-GPU boxes only: force the NVENC/Desktop-Duplication GPU by Description substring. Leave -# unset on single-GPU machines (the default auto-picks the discrete adapter). +# Multi-GPU boxes only: force the render GPU by adapter Description substring. One pick drives the +# whole pipeline — pf-vdisplay's IddCx render adapter, the IDD-push capture ring, and the encoder +# (NVENC/AMF/QSV) that reads it. A manual GPU preference in the web console outranks this; with +# neither set the host auto-picks the adapter with the most dedicated VRAM. Leave unset on +# single-GPU machines. #PUNKTFUNK_RENDER_ADAPTER=4090 # Keep a per-client virtual display alive briefly after disconnect so a quick reconnect reuses it