e35dad529b316bb74bb56cb4f1cfd763d0f81328
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e35dad529b |
fix(vdisplay/gamescope): DM-flavor-aware session takeover — stop masking Nobara's plasmalogin to death
Field report (Nobara 43 HTPC): switching the host to Steam Game Mode mid-stream permanently black-screened the box. Live-proven root cause on a Nobara repro VM: our takeover masks the box's gamescope-session-plus unit, plasmalogin's Relogin=true then fails its session Exec repeatedly and trips systemd's start limit within ~1 s — the display manager dies, and our restore verb (unmask + user start) cannot bring a seatless gamescope back. Only 'reset-failed + restart' of the DM recovers. The takeover is now display-manager-flavor-aware: - SDDM (Bazzite/SteamOS) keeps the proven mask+SIGKILL path unchanged. - plasmalogin/unknown DMs never mask: with privilege (root or an operator polkit rule scoped to the DM unit — documented) the host stops the DM for the stream and restores it with reset-failed + restart (PUNKTFUNK_RECOVER_SESSION_CMD as fallback), recorded in the persisted takeover state so a host crash still restores; without privilege the managed takeover degrades to ATTACH and mirrors the box's live Game Mode instead of destabilizing the seat. Both legs of the privileged cycle live-verified on the repro VM (headless managed session works with zero login sessions; render nodes are 0666). A loaded-but-inactive leftover instance never triggers the DM stop. Companion fixes from the same triage: - ensure_box_gamescope_mode gains the attach-only rebuild-probe guard both managed paths already had (stale post-capture-loss detection restarted the box's unit), and no longer re-modes a box that drives a physical display — attach mirrors on-glass; re-mode is the headless-box model. - Capture-loss rebuilds targeting gamescope get a 100 s budget: the 40 s budget expired inside the first 45 s Steam-cold-start launch attempt, a guaranteed single-shot failure. - A PUNKTFUNK_COMPOSITOR pin now WARNs once per capture loss when the live session no longer matches it (the pin disables session-following — the reporter's original stream-death trigger). - A managed session that took nothing over (client gamescope pin beside a live desktop) is stopped on disconnect instead of being orphaned forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d5ae8dcc3e |
feat(capture): gamescope cursor via XFixes shape + QueryPointer position (Phase C)
gamescope excludes its pointer from the PipeWire node it feeds us and can't embed one either (`set_hw_cursor` is inert), so every gamescope stream was cursorless. Read the pointer from gamescope's nested Xwayland instead — XFixesGetCursorImage for shape/hotspot/visibility, core QueryPointer for position — and publish a CursorOverlay into the capturer's existing `cursor_live` slot, so the encoder blend composites it into the video exactly like the SPA_META_Cursor path. - pf-capture/src/linux/xfixes_cursor.rs (new): the XFixes reader. Connects to EVERY nested Xwayland (Gaming Mode runs one per --xwayland-count) and follows the focused one each tick (the display whose pointer moves), reading that display's own cursor shape. Un-premultiplies ARGB -> straight RGBA. Drop stops the thread. - Capturer::attach_gamescope_cursor + the PortalCapturer override spawn it into the same `cursor_live` slot; pf_vdisplay::gamescope_xwayland_cursor_targets discovers the (DISPLAY, XAUTHORITY) pairs via the GAMESCOPE_WAYLAND_DISPLAY scan. - host: SessionPlan.gamescope_cursor (set from the compositor at both resolve sites AND on a mid-stream Desktop->Gaming switch); the blend gate now builds the encoder blend for gamescope; a sibling composite arm attaches capturer.cursor() per tick for capture-mode clients (no channel needed). native NV12 is disabled for these sessions — that encode path can't blend the cursor (it assumes gamescope embeds the pointer), so we capture RGB and route to the proven CUDA VkSlotBlend / compute-CSC blend. - the pipewire thread no longer clobbers `cursor_live` with None on a buffer that carries no SPA_META_Cursor (gamescope) — that raced the XFixes writer and strobed the composited pointer on/off. On-glass (home-bazzite-2, RTX 5070 Ti, Gaming Mode): cursor visible + steady in Big Picture and CS2 menus, follows focus between the two Xwaylands, hidden in-game. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af87549052 |
fix(linux/vdisplay): KWin virtual outputs stream above 60 Hz via sacrificial-birth renegotiation
KWin's ScreenCastStream builds its PipeWire format offer — including the maxFramerate cap it actively throttles delivery to — ONCE at stream creation, when the virtual output sits at its hardcoded birth 60 Hz. A kscreen custom-mode change afterwards updates the OUTPUT (readback says 240) but never the offer, so every consumer negotiates max=60 and the stream delivers 60 (pw-dump-verified on KWin 6.6.4). The only path that rebuilds the offer is the stream's own resize handling: a source SIZE change while recording re-runs buildFormats — picking up the output's current refresh — and renegotiates the live stream. So above 60 Hz, birth the output at a sacrificial height (+16), then install + select the real WxH@hz custom mode: the first frame recorded after the consumer connects triggers KWin's resize path, which renegotiates the live stream to WxH@hz. The capturer holds (requeues) buffers until the negotiated size matches — new expect_exact_dims plumbing VirtualOutput → registry (fresh creates only) → open_virtual_output → a self-disarming gate in the process callback — bounded by a 3 s deadline that accepts the producer's dims rather than wedging the session into the first-frame retry loop. set_custom_refresh reads back the full active mode; a size reject (pre-6.6 KWin) recreates plain at the real size @60. Every kscreen operation now addresses the output by its NUMERIC id, resolved by matching the managed name-prefix AND the just-created birth size (newest id wins): a mode-switch supersede reuses the per-slot output NAME (deliberately, for KWin's per-name config persistence) while the superseded sibling is still alive, and name addressing hit the FIRST match = the OLD output — on-glass that resized the live session's display out from under it (wrong-res/black), read the old output back as 'mode applied', and set the old output primary while the replacement starved at its birth mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35923080fb |
fix(host/linux): scale degenerate-region absolute coords by the session's real output size
The raw-client-pixels fallback is exact only while the managed session runs at the client's mode. When they diverge — foreign-gamescope attach at its own resolution, supersample/under-render, transitions — raw pixels drift or land out of range. The EIS relay file now carries the session's current output size as a second "WxH" line (from current_gamescope_output_size()); the injector scales normalized client positions into it, keeping raw pixels only as the last resort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2562663fc6 |
fix(host/linux): survive and self-heal dead gamescope sessions on headless SteamOS boxes
Three fixes for the "most connects simply fail" trap on a SteamOS host with no physical display (VM testbox, panel-less mini PC): - restore path: skip the restore-to-physical-panel when no DRM connector reports a connected display — removing the headless drop-in and restarting gamescope-session.target on such a box just crash-loops gamescope and strands every later connect on "no usable compositor". Keep the headless session (and the takeover state) instead. - connect path: when no live graphical session exists but the MANAGED gamescope infra is present (SteamOS gamescope-session / Bazzite session-plus), route to the gamescope takeover — which rebuilds the session at the client's mode — instead of failing the connect. - error delivery: a session-setup failure used to just drop the QUIC connection, reaching the client as "control stream finished mid-frame" (indistinguishable from transport trouble). Close with a typed setup-failed code (0x68, RejectedSetupFailed = -29) carrying the error text, and give the client handshake/pairing error paths a short grace for the CONNECTION_CLOSE to beat the stream error it caused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
58b27acbd5 |
fix(vdisplay/session): scrub the dead desktop's WAYLAND_DISPLAY from the user-manager env
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m14s
apple / swift (push) Successful in 1m22s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
ci / bench (push) Successful in 5m44s
apple / screenshots (push) Successful in 6m18s
android / android (push) Successful in 13m0s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 13m36s
deb / build-publish-host (push) Successful in 14m44s
windows-host / package (push) Successful in 16m32s
arch / build-publish (push) Successful in 17m49s
ci / rust (push) Successful in 19m58s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m42s
settle_desktop_portal pushes the live desktop's WAYLAND_DISPLAY into the systemd --user manager (import-environment) so a re-activated portal inherits the right session — but the import PERSISTS after that desktop dies. Every later user unit then inherits the stale socket, including gamescope-session.target: gamescope sees WAYLAND_DISPLAY, runs NESTED, and aborts with "Failed to connect to wayland socket: wayland-0" — observed live on a Deck, where Game Mode could not start at all (autologin crash-looped) until the var was unset by hand. Two-layer fix, mirroring the protection launch_session already gives its transient unit: - observe_session_instance: when a desktop compositor instance goes away, unset WAYLAND_DISPLAY/DISPLAY from the manager env (a desktop bounce is harmless — the next portal settle re-imports). - write_steamos_dropin: UnsetEnvironment=DISPLAY WAYLAND_DISPLAY on the takeover drop-in, unit-scoped belt-and-suspenders for host-initiated restarts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
333c8979e9 |
fix(vdisplay/gamescope): capture-loss rebuild probes must not steal the box session
Observed live on a Deck host: the user switched Game Mode → Desktop, KDE came up in 0.8 s — and the capture-loss rebuild's FIRST detection ran inside that gap, still said Gaming, and its gamescope re-acquire restarted gamescope-session.target. On SteamOS that steals the seat: the user was yanked out of the KDE session they had just chosen, the two session managers fought (job canceled / dependency failed), no gamescope node appeared within 30 s, and the stream died at the 40 s rebuild budget. A rebuild probe acting on possibly-stale detection must never stop, relaunch, or take over box sessions. New pf_vdisplay::rebuild_probe_scope (RAII, counted): while held, the gamescope managed/takeover create paths only attach to a live node and fail fast otherwise — no stop_autologin_sessions, no session-plus relaunch, no gamescope-session.target restart. The capture-loss loop holds it for the first 4 s after a loss (the detection-ambiguity window); after that, destructive rebuilds are allowed again, so a genuine switch INTO Game Mode still gets its headless takeover. Probe failures are instant, so the loop now paces itself at ~2 Hz instead of spinning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b89dbfa979 |
feat(gamescope): end dedicated stream on Steam game exit + auto --steam
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / bench (push) Successful in 6m57s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
apple / screenshots (push) Successful in 6m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
deb / build-publish (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 29s
deb / build-publish-host (push) Successful in 9m29s
windows-host / package (push) Successful in 16m24s
arch / build-publish (push) Successful in 17m40s
android / android (push) Successful in 18m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m52s
ci / rust (push) Successful in 26m16s
Dedicated single-game sessions now end cleanly when the launched game quits, and Steam launches get gamescope's Steam integration without an operator env knob. End-on-exit: the existing APP_EXITED close only fired when gamescope's PipeWire node vanished, which never happens for Steam — the nested `steam` is the resident singleton client and stays up after a game quits, so gamescope (and its node) never die and the stream sat on a hidden Steam session forever. Add a Steam AppId reaper watcher (pf-vdisplay .../gamescope/discovery.rs): steam_appid_from_launch() parses steam://rungameid/<id>; wait_for_steam_game_exit() waits for the game to start (<=300s grace) then exit (3s confirm); steam_game_running() scans same-uid /proc for Steam's launch reaper matching both the `SteamLaunch` and `AppId=<id>` argv tokens (exact-match; reaper lifetime == game lifetime, so shader precompile can't false-trigger). The host spawns a pf1-gamewatch thread for nested Steam launches that closes the connection with APP_EXITED (launcher clients return to their library) and sets quit/stop; cancelled via stop if the session ends first. Non-Steam nested launches keep the node-death path (gamescope's child IS the game). Flags: auto-enable `--steam` whenever the launch is a Steam launch (was only the global PUNKTFUNK_GAMESCOPE_STEAM knob, default-off) — in-game overlay / Steam+X / gamepad-UI nav for Steam titles with no operator config. New opt-in PUNKTFUNK_GAMESCOPE_GRAB_CURSOR adds `--force-grab-cursor` for a real game launch (FPS mouselook); default OFF because it forces relative mode, which breaks absolute-pointer games/menus. Verified: fmt clean; clippy -D warnings clean on the three crates; pf-vdisplay 64/0 (incl. new steam-appid parse test); punktfunk-host builds + 186/0; reaper /proc detection smoke-tested (detect, exact non-match, gone-after-exit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
86d9f49473 |
style(pf-vdisplay): rustfmt under the pinned 1.96.0 toolchain
audit / bun-audit (push) Successful in 12s
windows-drivers / probe-and-proto (push) Successful in 31s
ci / web (push) Successful in 48s
apple / swift (push) Failing after 1m1s
ci / docs-site (push) Successful in 1m2s
decky / build-publish (push) Successful in 21s
windows-drivers / driver-build (push) Successful in 1m35s
windows-host / package (push) Failing after 16s
audit / cargo-audit (push) Successful in 2m46s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
release / apple (push) Successful in 3m56s
apple / screenshots (push) Has been skipped
ci / bench (push) Successful in 6m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m16s
flatpak / build-publish (push) Failing after 8m9s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 13m9s
android / android (push) Successful in 13m11s
deb / build-publish (push) Successful in 14m12s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m8s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m1s
The W6.2 pf-vdisplay extraction (
|
||
|
|
27a5d8daac |
refactor(host/W6.2): extract virtual-display orchestration into the pf-vdisplay crate
vdisplay.rs + vdisplay/* (the per-compositor Linux backends — KWin zkde-screencast,
wlroots swaymsg, Mutter RemoteDesktop, Hyprland — and the Windows IddCx/pf-vdisplay
driver backend, behind one VirtualDisplay trait; the mode-conflict admission
registry, the display policy/identity/custom-preset state, and the session-env /
gamescope routing) move into crates/pf-vdisplay (plan §W6). The DDC/CI panel-power
control (used only here) and the KWin zkde protocol XML move with it. This
completes the host-crate decomposition: capture, encode, inject, and vdisplay are
now four subsystem crates over the shared leaves, and punktfunk-host is the
orchestrator (serve/supervisor + native + gamestream + mgmt).
Coupling breaks (all down-only, cargo-tree acyclic):
- capture::dxgi identity -> pf_frame::dxgi; win_display/monitor_devnode/
console_session_mismatch -> pf-win-display leaf; can_open_another_session ->
pf-encode (the NVENC session-budget admission gate — acyclic peer edge).
- The registry's DisplayCreated/DisplayReleased emits into the host SSE event bus
invert to a leaf hook: pf-vdisplay emits a neutral DisplayEvent to a
host-registered DISPLAY_EVENT_SINK, so it never reaches the orchestrator's
events module.
- The IddCx driver module is renamed pf_vdisplay -> driver (its old name collided
with the crate name through the host's `mod vdisplay` shim glob).
The host keeps `mod vdisplay { pub use pf_vdisplay::* }` so every crate::vdisplay::*
path (serve/mgmt/native/the capture FrameChannelSender seam) is unchanged; the
heavy deps (wayland/ashpd/tokio + the zkde protocol) moved with the crate.
Co-authored: a fail-closed IOCTL-reply-length security fix (reject short/zeroed
pf-vdisplay driver replies before trusting protocol_version/target_id/wudf_pid/luid,
security-review 2026-07-17) rides this commit in the moved driver module.
Verified: Linux clippy -D warnings (pf-vdisplay + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-vdisplay 63/63 + host 167/167 tests; Windows clippy -D warnings
(pf-vdisplay --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|