cdbdc078d603d8025fa2a567194bfc34b65e01b0
411 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cdbdc078d6 |
perf(core): ref-based FEC encode — packetize shards reference the frame in place
Stage A of the zero-copy host packetize path (networking-audit deferred plan §1): ErasureCoder::encode now takes &[&[u8]], so Packetizer::packetize builds each block's data shards as slices straight into the frame buffer instead of allocating + copying a Vec per data shard. Only the frame's final (possibly partial) shard is staged in a reusable zero-padded scratch; blocks are consecutive shard ranges, so every other shard is a full payload-sized slice. - gf8: encode_sep() over the same Cauchy codec — parity byte-identical to nanors/Moonlight (nanors_exact_parity_vectors unchanged and green) - gf16: reed_solomon_simd::encode is already generic over AsRef<[u8]> - loss-harness sweep: recovery rates identical before/after - bench pipeline (end-to-end, host+client): gf8/64K -3.0%, gf16/64K -2.2%, gf16/1M -3.4%, gf8/1M -0.7% Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dd73ae2469 |
fix(host): fresh random per-session nonce salt instead of the static "pkf1"
Every session sealed with the literal salt b"pkf1", so GCM nonce uniqueness (nonce = salt || sequence) rested ENTIRELY on the per-session key being fresh — correct today, but a single key-reuse bug anywhere in the handshake path would have meant immediate catastrophic nonce reuse instead of merely a wrong key. Random salt per session keeps the documented second line of defense real. The salt is negotiated via Welcome, so every deployed client just follows — no wire or compat change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cca5008805 |
feat(host,web): experimental DDC/CI monitor power-off for Exclusive sessions
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 19s
apple / swift (push) Successful in 1m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
apple / screenshots (push) Successful in 5m28s
ci / bench (push) Successful in 6m40s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 8m45s
android / android (push) Successful in 12m43s
deb / build-publish (push) Successful in 13m1s
arch / build-publish (push) Successful in 14m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m20s
ci / rust (push) Successful in 22m12s
The sole-virtual-display stutter investigation's active experiment: when the Exclusive isolate deactivates a physical monitor, the dark-but-connected head keeps getting serviced (monitor standby auto-input-scan / DP link churn) at a seconds-scale cadence — the leading suspect for the periodic double-jolt. A panel commanded off over DDC/CI (the VESA monitor-control channel in the video cable) believes it has an owner and, on cooperating firmware, stops probing. - New `ddc_power_off` display-policy axis (default off): orthogonal to presets like game_session, stored in display-settings.json, surfaced in GET/PUT /display/settings + the enforced list, carried through the layout transform. - windows/ddc.rs: VCP 0xD6 power-mode control via the dxva2 Physical Monitor API. Deliberately DPMS-off (0x04, DDC stays responsive, signal return wakes) and never power-button-off (0x05, bricks-until-button on many monitors). Probe-before-write; every failure is skip-and-log — monitors without DDC/CI, OSD-disabled, or behind docks/KVMs degrade to a logged no-op. - Manager wiring: panels commanded off immediately BEFORE the Exclusive CCD isolate (an HMONITOR — and with it the DDC channel — only exists while the display is active); teardown wakes them right after the CCD restore, where returning signal alone already wakes most firmware. - Web console: an Experimental-badged on/off control on the display card, applied immediately like the game-session axis and preserved across preset switches; EN/DE strings incl. the wake-failure escape hatch (press the monitor's power button once, turn the option off). Diagnostic value on top of the fix: if this kills a reporter's stutter, the churn is monitor-firmware-initiated; if only topology=primary/extend does, the driver services dark heads regardless — the two remaining root-cause classes. Verified: Linux 258 tests + clippy + fmt clean; Windows (RTX box) 220/220 + clippy clean; web tsc + production build clean; openapi.json regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f68f6bc590 |
fix(linux): zerocopy worker survives the host binary being replaced on disk
apple / swift (push) Successful in 1m10s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m29s
apple / screenshots (push) Successful in 5m39s
windows-host / package (push) Successful in 7m45s
ci / bench (push) Successful in 5m30s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m4s
arch / build-publish (push) Successful in 11m21s
docker / deploy-docs (push) Successful in 11s
android / android (push) Successful in 15m23s
deb / build-publish (push) Successful in 13m39s
ci / rust (push) Successful in 17m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m56s
A pacman canary upgrade under a running host (0.5284→0.5338, 09:24 today) unlinked /usr/bin/punktfunk-host; current_exe() then readlinked to "<path> (deleted)", every worker spawn failed ENOENT, and each session silently fell back to the CPU linear-copy capture — observed as the box "regressing" to ~90 fps at 5-7 MP until a service restart. - RemoteImporter::spawn now pins a read fd to /proc/self/exe (once, kept for the process lifetime) and execs the worker via /proc/self/fd/<n>. The magic link names the running image's inode, not its path, so the spawn survives replacement/deletion — and the worker is always byte-for-byte the host's own build, so a mid-upgrade spawn can't hit a worker-protocol skew either. If the fd draws number 3 (the worker's socket slot — the pre-exec dup2 would clobber it) it is re-numbered; if /proc is unavailable the old path-based spawn remains as fallback. - argv[0] is set to "punktfunk-host" and the worker prctl-renames its comm to "pf-zerocopy" — exec-by-fd-path would otherwise show a bare fd number in ps and top. - zerocopy-probe now also spawns the worker (handshake + modifier query), so the probe catches spawn-level breakage, not just FFI/GPU bring-up. Verified end-to-end on the dev box: probe with the binary unlinked mid-run (/proc/self/exe → "(deleted)") still spawns the worker and reports all 13 modifiers. New unit tests cover the pinned spawn and the deleted-file exec; the latter retries ETXTBSY (fs::copy's write fd leaks into other tests' forked children until their execs clear it — a copy-then-exec harness artifact, not a production concern). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7ab97bb1a3 |
feat(host): capture-stall watch — DWM-level self-diagnosis for the Exclusive-topology stutter
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m18s
ci / bench (push) Successful in 5m17s
decky / build-publish (push) Successful in 36s
windows-host / package (push) Successful in 8m13s
arch / build-publish (push) Successful in 11m2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
apple / screenshots (push) Successful in 5m38s
android / android (push) Successful in 16m12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Successful in 15m13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m33s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13m43s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m46s
ci / rust (push) Successful in 22m46s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m45s
docker / deploy-docs (push) Successful in 22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m10s
Field repro (Mounjay, still present on 0.9.0): the ~4 s double-jolt stutter appears ONLY while the virtual display is the sole active display (Exclusive topology) and stops the instant Windows switches to Extend — live, both ways. Cross-project research (Apollo #179/#358/#368/#563/#776, VDD #36, Tom's HW) points at the display/present path BELOW capture: an inactive-but-connected DisplayPort head being periodically serviced (standby HPD/AUX/link events), with a DWM software-vsync clock beat as the secondary (different-signature) class. Neither ends in anything our recovery-side detector can see unless the client actually loses data — so give the HOST a direct sensor at the ring: - StallWatch (idd_push.rs): a >150 ms hole in DWM frame delivery counts as a capture stall only when the 8 preceding frames arrived within 400 ms — sustained >=20 fps flow, so an idle desktop, a caret blink, or a paused video can never trip it. Per-stall debug line; when stalls settle into an evenly-spaced multi-second cycle, one rate-limited WARN names the class: 'capture stalls are METRONOMIC', with the topology=primary/extend and refresh-rate leads. Ring-recreate recovery gaps reset the watch (self- inflicted, already logged by the recreate path). - The evenly-spaced-cycle detector moves out of punktfunk1.rs into metronome.rs (RecoveryCadence -> Metronome, unchanged logic + tests) so the IDR-serve detector and the stall watch share one implementation; the recovery WARN now cross-references the capture-stall lines. Diagnosis map for an Exclusive-mode stutter log: 'slow display-descriptor poll' = something holds the win32k display lock; 'capture stalls are METRONOMIC' without it = DWM stopped composing (DP servicing / present clock, below us); recovery-IDR METRONOMIC alone = frames flowed but clients lost data. Verified: Linux tests+clippy+fmt clean; Windows (RTX box) 220/220 + clippy clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ef39050dbc |
docs: repo-wide housekeeping — sync README & docs with the code as shipped
windows-drivers / probe-and-proto (push) Successful in 47s
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m13s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
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 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
windows-drivers / driver-build (push) Successful in 1m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 55s
ci / bench (push) Successful in 7m18s
flatpak / build-publish (push) Successful in 6m18s
docker / deploy-docs (push) Successful in 25s
windows-host / package (push) Successful in 8m31s
release / apple (push) Successful in 11m41s
android / android (push) Successful in 13m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m57s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m16s
arch / build-publish (push) Successful in 16m24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
deb / build-publish (push) Successful in 17m54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m27s
ci / rust (push) Successful in 18m23s
apple / screenshots (push) Successful in 5m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m43s
Six parallel audits swept the root docs, docs-site, every per-directory README, and the packaging docs; every claim below was verified against the source before editing. - README: Layout gains the six missing crates (pf-client-core, pf-presenter, pf-console-ui, pf-ffvk, pf-driver-proto, punktfunk-tray), clients/session, api/ and ci/; Linux/Windows client rows reflect the shell + Vulkan-session split and the Vulkan Video -> VAAPI/D3D11VA -> software decode chains; the "every client over a C ABI" claim is corrected (Rust clients link the core directly); tiered stats overlay + console shell noted; Apple row mentions AV1. - CONTRIBUTING: drop the dead CLAUDE.md link (deliberately untracked); point at the README's build/invariants sections. SECURITY: 0.9.0. - host-cli/pairing: --allow-pairing/--require-pairing are no-op legacy names — pairing is required by default, --allow-tofu is the real flag; document --data-port and --idle-timeout-ms. - configuration: document PUNKTFUNK_RECOVER_SESSION_CMD (session-crash recovery hook), PUNKTFUNK_MDNS, PUNKTFUNK_DATA_PORT. - virtual-displays/gnome: GNOME per-client scaling shipped (host- persisted) — flip the ❌ to ✅ and describe how it works. - stats: new "Detail levels" section (Off/Compact/Normal/Detailed + per-platform cycle gestures); retire the GTK hand-off note. - clients/install-client/status/roadmap: decode chains, Windows client validation narrowed to HDR-only pending, adaptive bitrate, console shell, Apple AV1, Windows host vendor list. - Sub-READMEs: clients/linux rewritten for the re-architecture; session Windows decode rung + d3d11va knob; Windows tiered overlay; Android minSdk 28; decky file table; host zerocopy/ path; scripts port 47992 and steamos-host.md; pf-dualsense source path. - packaging: canary version bases are tag-derived (<next-minor> via pf-version.sh/.ps1), codecs-extra not ffmpeg-full, document the pinned offline-Skia tarball + SKIA_BINARIES_URL and vulkan-headers. - Convert 15 dangling design/*.md links to the punktfunk-planning prose convention (those docs live in the private planning repo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b7cb75a48a |
fix(host): dual identical GPUs stream again — self-heal the IDD-push ring onto the driver's real render adapter
ci / web (push) Successful in 50s
apple / swift (push) Successful in 1m13s
ci / docs-site (push) Successful in 1m5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 20s
apple / screenshots (push) Successful in 5m35s
windows-host / package (push) Successful in 7m57s
deb / build-publish (push) Successful in 12m0s
arch / build-publish (push) Successful in 14m51s
android / android (push) Successful in 16m21s
ci / rust (push) Successful in 17m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m0s
A box with two identical GPUs (twin RTX 4090s in the field report) died out of pipeline retries with driver_status=2 (DRV_STATUS_TEX_FAIL, detail 0x80070057): SET_RENDER_ADAPTER pinned the driver to one twin at monitor ADD, but the ring open re-ran the GPU picker, whose max-VRAM tie is settled by DXGI enumeration order — which follows the primary display that the vdisplay flow itself moves mid-session. Ring on one twin, swap-chain on the other → the driver can't open the shared textures, and every retry repeats the same wrong pick. - wait_for_attach: TEX_FAIL becomes a typed AttachTexFail carrying the render LUID the driver reports in the shared frame header (written before the texture opens, so valid on failure) — the error now names both adapters instead of guessing "mismatch?". - open_inner: on AttachTexFail with a different, non-zero driver LUID, rebind the ring there and reopen ONCE. Both candidates are real GPU adapters, so NVENC keeps getting a device it accepts. - gpu::enumerate (Windows): sort the inventory by LUID so the max-VRAM tie, the env-substring first match, and twin occurrence numbering are stable for the boot instead of tracking the primary display. - manager: record the ADD-time render pin on Monitor and warn on reuse when the current pick has moved away from it (the pick only takes effect on the next monitor create; /display/release forces one). - pf_vdisplay: drop the "ADD render adapter DIFFERS from pinned" warn — the ADD reply carries IDARG_OUT_MONITORARRIVAL.OsAdapterLuid (the IddCx DISPLAY adapter, verified on-glass), not the render GPU, so the check compared unrelated LUIDs and fired on every ADD. Verified on the RTX 4090 box: single-GPU streaming is unchanged (IddPush + NVENC AV1 session, no new warnings). The rebind path needs a twin-GPU box — until validated there, /display/release remains the manual recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7b25868a19 |
fix(input): rock-solid held gamepad state — Android device pinning + seq'd snapshots
apple / swift (push) Successful in 1m12s
release / apple (push) Successful in 8m54s
windows-host / package (push) Successful in 7m35s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m49s
ci / web (push) Successful in 1m16s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m8s
ci / docs-site (push) Successful in 1m21s
android / android (push) Successful in 12m25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 56s
decky / build-publish (push) Successful in 18s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m23s
apple / screenshots (push) Successful in 5m45s
arch / build-publish (push) Successful in 11m9s
docker / build-push (., web/Dockerfile, punktfunk-web) (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 10s
ci / bench (push) Successful in 5m26s
flatpak / build-publish (push) Failing after 33s
ci / rust (push) Successful in 17m39s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m38s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m39s
docker / deploy-docs (push) Successful in 22s
deb / build-publish (push) Successful in 11m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m8s
Two causes behind one field report (a held trigger jittering mid-game, Android client → Windows host): Android folded joystick ACTION_MOVEs from EVERY device into one axis state. A controller's joystick-classified sibling node (DualSense/DS4 motion sensors) or a second/drifting pad reports every pad axis as 0, so a held trigger flapped value→0→value on each event interleave. The mapper now qualifies the source DEVICE (its source classes must include GAMEPAD — a joystick event's own source is always plain JOYSTICK), pins to one deviceId until that device disconnects, and merges LTRIGGER/BRAKE (and RTRIGGER/GAS) with max, the same fold as the Controllers probe. Underneath, gamepad input rode per-transition events over unreliable, unordered QUIC datagrams — no sequence numbers, sharing the 4 KiB oldest-first-shed send buffer — so one dropped or reordered event corrupted held pad state until the NEXT change. Gamepad state now travels the way rumble already does: idempotent state, refreshed. InputKind::GamepadState packs the whole pad + a wrapping u8 seq into the existing 18-byte layout; the host advertises HOST_CAP_GAMEPAD_STATE (Welcome trailing byte, offset 67) and applies snapshots through a per-pad stale-seq gate, skipping frame emits for unchanged refreshes; the client folds embedder events into snapshots inside NativeClient's input task (send on change + 100 ms refresh of touched pads), so the SDL clients (Linux/Windows/session), Android, and Apple (C ABI) are all covered with zero capture-code changes. Either end older ⇒ the legacy per-transition path runs unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
47d22b6082 |
fix(host): admit exactly ONE parked knock per Approve — stop crashing gnome-shell, and self-heal dead sessions
ci / docs-site (push) Successful in 55s
ci / web (push) Successful in 1m11s
apple / swift (push) Successful in 1m12s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
ci / bench (push) Successful in 7m3s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m57s
windows-host / package (push) Successful in 8m20s
arch / build-publish (push) Successful in 11m48s
deb / build-publish (push) Successful in 12m57s
apple / screenshots (push) Successful in 5m42s
android / android (push) Successful in 17m23s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m32s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m28s
ci / rust (push) Successful in 25m57s
docker / deploy-docs (push) Failing after 5m44s
A retrying unpaired client parks one QUIC connection per knock, but the delegated-approval waiters were keyed on fingerprint alone — one console Approve resolved ALL of them. Observed live (2026-07-10): an iPad knocked 3x, one Approve admitted three full sessions, three Mutter virtual monitors were created within ~200us plus an ApplyMonitorsConfig, and gnome-shell SIGSEGV'd inside meta_monitor_manager_rebuild — dropping the box to the GDM greeter, unreachable until reboot (GDM auto-login runs once per boot) while the lingering host spammed "RemoteDesktop ... not activatable" libei errors. Four fixes, outermost symptom inward: - Knock generations (native_pairing): note_pending returns a per-knock generation; a re-knock bumps it and wakes the previous waiter, which resolves the new PairingDecision::Superseded (connection closes; the console list is unchanged). An approval records WHICH generation it admitted, so a stale waiter polling only after the pending entry is cleared still loses the tie — exactly one admission, no matter the interleaving. - TOPOLOGY_LOCK (vdisplay/mutter): every Mutter monitor mutation (pre-snapshot -> RecordVirtual -> ApplyMonitorsConfig, and the teardown Stop) is serialized process-wide. Concurrent rebuilds are what segfault the shell (second on-glass crash of this class — the teardown race is already documented in-file), and serialization also keeps wait_virtual_connector's snapshot diff from attributing a sibling's connector. Create timeout 20s -> 45s for lock queueing. - Session-env hygiene (vdisplay): when detection finds NOTHING live, clear the previous connect's XDG_CURRENT_DESKTOP/WAYLAND_DISPLAY retarget. A stale GNOME value kept mutter::is_available() true after the crash, routing explicit-backend connects into the dead session (create timeouts + libei error loops) instead of the crisp "no live graphical session" handshake error. - Opt-in recovery hook (config + punktfunk1): PUNKTFUNK_RECOVER_SESSION_CMD fires (detached via sh -c, debounced to one launch/min) when a client connects while no graphical session is live — e.g. `sudo -n systemctl restart gdm` re-runs auto-login and the client's retry lands in the recovered desktop. The handshake error tells the client to retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
968a9c66c1 |
fix(host): NVENC AV1 sessions actually open — stop stamping HEVC config onto the AV1 union
Every AV1 session died at initialize_encoder with NV_ENC_ERR_INVALID_PARAM
("rejected even at the floor bitrate") while negotiation and the session plan
looked healthy. Live-bisected on the RTX 4090 box against a 10-bit macOS AV1
session; three independent poisons, all HEVC-isms applied across the codec
union:
- Unconditional `hevcConfig.tier = 1`: NVENC's AV1 encoder is Main-tier only,
and the same write scribbled HEVC offsets into h264Config too. Tier/level
are now per-codec; AV1 deliberately writes NEITHER — its preset defaults are
the only accepted values, and `level = 0` is NOT autoselect on AV1 (that's
Level 2.0 in the AV1 enum — it pins the stream to the smallest level and
rejects any real mode). Explicit `idrPeriod = INFINITE` is likewise rejected;
with PTD enabled the driver follows the infinite gopLength, matching the
proven HEVC pattern of never touching idrPeriod.
- The 10-bit branch was gated on bit depth only, never codec: it stamped
NV_ENC_HEVC_PROFILE_MAIN10_GUID + hevcConfig bitfields onto AV1 (the killer
for HDR/10-bit clients). AV1's Main profile covers 10-bit natively; it now
sets output/input depth on its own config.
- The split-encode fallback only retried FORCED modes, so an AUTO-mode
rejection masqueraded as a bitrate cap and failed the whole clamp search.
Any non-disabled mode now retries once with split disabled. (Also corrects
the split-mode log legend: NV_ENC_SPLIT_DISABLE_MODE is 15, not 0.)
The AV1 RFI DPB write (maxNumRefFramesInDPB = 5) survives the bisect and
stays. Verified end to end: 10-bit AV1, macOS client → RTX 4090 host.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a1285dfa13 |
fix(host): IDD capture immune to display-config stalls/flaps + metronomic-recovery self-diagnosis
ci / docs-site (push) Successful in 1m7s
ci / web (push) Successful in 1m8s
decky / build-publish (push) Successful in 32s
android / android (push) Failing after 2m38s
windows-host / package (push) Failing after 11m13s
ci / rust (push) Successful in 22m2s
ci / bench (push) Successful in 5m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12m15s
deb / build-publish (push) Successful in 15m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m41s
apple / swift (push) Successful in 1m12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
arch / build-publish (push) Successful in 14m26s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 12m11s
apple / screenshots (push) Successful in 5m30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m52s
docker / deploy-docs (push) Failing after 6m14s
Field report (0.8.2→0.8.4): metronomic stutter every ~3.7-4 s, each event a double-jolt, on ALL clients — and the same box stuttered on Apollo (virtual display) but not Sunshine (physical). The disturbance class is host-side display-topology events / display-poller software (SteelSeries-GG class, Apollo #480/#358/#368) hitting the virtual-display path. Three changes make the IDD path resilient to it and self-diagnosing: - DescriptorPoller: the display-descriptor CCD queries (QueryDisplayConfig twice per sample) serialize on the session-global display-config lock, which topology churn and display-poller software can hold for tens-to- hundreds of ms. Polled inline, that stall landed ON the capture/encode thread 4x/s — a periodic frame hitch, invisible in any log. A dedicated thread now samples every 250 ms and publishes a snapshot; the capture loop pays one uncontended mutex read, and a sample slower than 50 ms logs a rate-limited warn — the stall is measured instead of streamed. - Flap immunity: advanced_color_enabled returns Option (None on a failed query or the target briefly missing from the active paths during a re-probe) and the poller keeps last-known-good per field — a transient no longer reads as "HDR off", which used to cost an HDR session TWO spurious ring recreates (false, then true again a poll later). On top, a two-strikes debounce: only two consecutive samples agreeing on the same new descriptor trigger the recreate; a real "Use HDR" flip or game mode-set still lands in ~1/2 s. - RecoveryCadence: served recovery IDRs within 1.5 s coalesce into one event (a double-jolt's paired IDRs count once); four consecutive events evenly spaced (±20%) warn "client keyframe recoveries are METRONOMIC — a periodic host/display disturbance, not random network loss" with the measured period (re-warn ≤ every 30 s) — so an affected host self-diagnoses in its own log / web console instead of reporting "nothing in the logs". Unit tests include the field report's exact double-jolt-pair schedule. Verified: host tests + clippy clean on Linux and on the RTX box (211/211, --features nvenc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c19c87c435 |
fix(host): filter IddCx ghost adapters out of the Windows GPU inventory
pf-vdisplay's IddCx adapter enumerates as a perfect DXGI twin of the GPU it renders on — same Description, VendorId/DeviceId/SubSysId, even DedicatedVideoMemory; only the LUID differs (verified on the RTX box: the twins were byte-identical in DXGI). Every host with the driver installed therefore listed each render GPU twice: the console picker offered a dead twin (display-only — D3D11 device creation on it fails), and auto-select's max-VRAM tie between the twins was settled by enumeration order — the real GPU won by luck, not construction. The kernel adapter type is the only field that separates the twins, so enumerate() now queries KMTQAITYPE_ADAPTERTYPE per adapter (raw gdi32 FFI — the windows crate's Wdk bindings aren't enabled, and one 3-call query doesn't justify them) and drops indirect-display, display-only, and software adapters. Fail-open: a failed query keeps the adapter — better a listed twin than a hidden real GPU. Incidentally also filters other vendors' virtual displays (Parsec-VDD class) from user machines' pickers. Existing manual preferences heal via find_preferred's same-model fallback when occurrence indices shift — a ghost accidentally pinned as -0 now resolves to the real GPU. The regression test pins the adapter-type words captured from real hardware; a Windows-gated smoke test runs enumerate() against the live machine (on the RTX box it actively exercises ghost exclusion: the inventory went from twin 4090s to exactly one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5d9f1ccb6d |
feat(host): --no-mdns/PUNKTFUNK_MDNS gate + punktfunk1-host --pairing-pin — docker-testing enablers
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
apple / swift (push) Successful in 1m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
decky / build-publish (push) Successful in 24s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 46s
ci / bench (push) Successful in 6m13s
apple / screenshots (push) Successful in 5m33s
windows-host / package (push) Successful in 9m42s
docker / deploy-docs (push) Successful in 5m59s
android / android (push) Successful in 13m48s
arch / build-publish (push) Successful in 13m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m52s
deb / build-publish (push) Successful in 15m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m7s
ci / rust (push) Successful in 18m35s
mDNS advertisement (native _punktfunk._udp AND GameStream _nvstream) can now be disabled with --no-mdns on serve/punktfunk1-host or PUNKTFUNK_MDNS=0 (the PUNKTFUNK_ZEROCOPY off-grammar) — for multicast-dead environments (bridged Docker, CI netns) where the advert reaches nobody or fails outright and aborts the GameStream plane; clients dial a manually-added host instead (mDNS-blind host-add works since the 0.8.4 dial-first fix). Unit tests no longer advertise on the LAN (mdns: false). punktfunk1-host gains --pairing-pin <PIN> (the already-plumbed fixed-PIN test seam, now CLI-exposed; empty refused like --mgmt-token) so a CI harness runs a deterministic SPAKE2 ceremony instead of scraping the logged PIN. Verified live: --no-mdns + PUNKTFUNK_MDNS=0 skip the advert, --pairing-pin arms with the fixed PIN, and a punktfunk1-host --source synthetic + punktfunk-probe loopback run passes verification (60/60 frames, 0 mismatch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c8fdef8bd1 |
fix(host): per-client display scaling on GNOME — host-persisted scale for Mutter virtual monitors
ci / web (push) Successful in 53s
decky / build-publish (push) Successful in 12s
apple / swift (push) Successful in 1m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
ci / docs-site (push) Successful in 1m24s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
ci / bench (push) Successful in 6m15s
apple / screenshots (push) Successful in 5m29s
windows-host / package (push) Successful in 8m43s
arch / build-publish (push) Successful in 11m29s
android / android (push) Successful in 13m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m47s
deb / build-publish (push) Successful in 16m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m12s
ci / rust (push) Successful in 22m39s
docker / deploy-docs (push) Successful in 17s
The identity policy promised per-client scaling, but on GNOME it could never work through the DE: Mutter mints a fresh EDID serial (0x%.6x, a per-shell counter) for every RecordVirtual monitor and offers no way to pass a stable identity, so the monitors.xml entry GNOME writes when the user sets a scale never rematches on reconnect — and our own topology ApplyMonitorsConfig then clobbered whatever was left back to a hardcoded scale 1.0. The host now remembers the scale itself (<config>/display-scale.json, keyed by the identity key — fp / fp@WxH / "shared" per the policy): reapplied at connect via the RecordVirtual mode's preferred-scale (Mutter pre-validates it; older Mutter ignores the key) and preserved — not forced — by the topology apply, with an integral-logical-size snap + retry-at-derived fallback (GetCurrentState reports no supported-scales for virtual monitors to snap to). The user's mid-session changes are polled from DisplayConfig every 5 s and written back, so a host crash loses at most a few seconds; an unconfirmed "Keep Changes" revert is tracked faithfully too. Mutter also now resolves the identity slot (set_client_identity/last_identity_slot) like KWin, keying the registry's group arrangement and /display/state. Verified live on the GNOME 50 box: set 1.5 mid-stream → persisted under the client fp; full teardown (fresh serial 0x000018); reconnect → monitor up at 1.5, "reapplying the client's saved display scale" logged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0f68cbbcd |
feat(core,host): per-family MTU shard sizing — the IPv6 gating item
apple / swift (push) Successful in 1m10s
windows-host / package (push) Successful in 8m41s
release / apple (push) Successful in 8m47s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m52s
arch / build-publish (push) Successful in 10m51s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m46s
android / android (push) Successful in 12m52s
audit / cargo-audit (push) Successful in 2m3s
audit / bun-audit (push) Successful in 15s
ci / rust (push) Failing after 25s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m11s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m12s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 1m24s
apple / screenshots (push) Successful in 5m37s
ci / bench (push) Successful in 7m12s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 48s
deb / build-publish (push) Successful in 11m48s
flatpak / build-publish (push) Failing after 4m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m48s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m6s
docker / deploy-docs (push) Successful in 29s
Phase 1 of the dual-stack plan (design/client-parity-and-network-resilience.md, plan 5):
the host sizes each session's shard_payload from the QUIC remote's address family instead
of assuming IPv4 — 1408 over v4 (unchanged), 1388 over v6 (40-byte header). Rides the
existing Welcome::shard_payload negotiation, so there is zero wire change and old clients
simply follow.
This has to land before any v6 data path exists: the v4-maximal 1408 makes every sealed
video datagram overshoot a 1500-MTU IPv6 hop, and v6 routers never fragment — that's a
blackhole (every datagram dropped), not the graceful-ish degradation of the
|
||
|
|
8e6e8bb25c |
feat(core,host): adaptive bitrate — mid-stream encoder re-targeting when set to Automatic
New SetBitrate (0x05) / BitrateChanged (0x06) control messages: the host clamps like the Hello request, acks the resolved rate, and rebuilds the ENCODER ONLY in place (same mode, first new-rate frame is an IDR — the proven mode-switch resync, minus the pipeline churn). The client side is an AIMD controller (core abr.rs) in the data-plane pump, armed only when the user's bitrate is Automatic (Hello bitrate_kbps == 0): ×0.7 after two bad 750 ms windows (FEC-unrecoverable frames, ≥2% loss, one-way-delay rise above its rolling baseline — the pre-loss bufferbloat signal off the clock-skew handshake — or a jump-to-live flush), ~+6% after ~10 s clean, ceiling = the session's starting rate, 3 s cooldown, self-disables against a host that never acks (older build). Division of labour: adaptive FEC keeps answering fast random loss; bitrate now answers persistent congestion, closing the FEC death-spiral gap. The web-console sample reports the live rate. Also: join_host_port() brackets bare IPv6 literals before SocketAddr parsing (parse-side IPv6 groundwork, pairs with the next commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2003d8a75c |
style(host): rustfmt the bit_depth ternary in serve_session
apple / swift (push) Successful in 1m13s
apple / screenshots (push) Successful in 3m26s
android / android (push) Successful in 4m58s
arch / build-publish (push) Successful in 5m53s
ci / web (push) Successful in 1m4s
windows-host / package (push) Successful in 7m46s
ci / docs-site (push) Successful in 1m22s
ci / rust (push) Successful in 4m59s
ci / bench (push) Successful in 5m7s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
deb / build-publish (push) Successful in 4m41s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m57s
docker / deploy-docs (push) Successful in 20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m24s
Collapse the manually-wrapped if/else to rustfmt's canonical form; fixes the cargo fmt --all --check CI failure at punktfunk1.rs:895. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
838a1239cf |
feat(codec): make client codec selection real — GPU-aware native advertisement
apple / swift (push) Successful in 1m7s
release / apple (push) Successful in 8m27s
windows-host / package (push) Successful in 7m59s
apple / screenshots (push) Failing after 2m38s
android / android (push) Successful in 4m44s
ci / rust (push) Failing after 46s
arch / build-publish (push) Successful in 5m32s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 1m2s
deb / build-publish (push) Successful in 4m44s
ci / bench (push) Successful in 5m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
decky / build-publish (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 11m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m14s
docker / deploy-docs (push) Successful in 13s
The clients' codec pickers sent a preference the host threw away: host_wire_caps() hardcoded HEVC for every GPU backend, so the native path never emitted H.264/AV1 regardless of what the user chose. Host: advertise what the backend actually encodes, mirroring the GameStream serverinfo logic — software openh264 emits H.264; probed backends (Linux VAAPI, Windows AMF/QSV) advertise their per-GPU CodecSupport via the new wire_mask(); NVENC keeps the Moonlight-validated H.264|HEVC|AV1 static superset; an empty probe falls back to the superset so auto clients still resolve HEVC. Gate 10-bit to HEVC (like the 4:4:4 gate) now that a client can steer the codec — Main10 is the only 10-bit encode path. Fix the web-console stats label hardcoded to "hevc" (new Codec::label(), shared with the GameStream register_session mapping). Android: replace the hardcoded H264|HEVC Hello advertisement with a videoCodecs param fed by VideoDecoders.decodableCodecBits() (AV1 bit only when a real hardware video/av01 decoder exists — the decode loop was already mime-driven); offer AV1 in the codec picker on capable devices. Decky: add the missing "Video codec" dropdown (auto/hevc/h264/av1) to the plugin settings, round-tripping the same codec key the flatpak client reads. Apple: unchanged by design (AnnexB.swift is NAL-only, AV1 is never advertised); refresh the stale "hosts don't emit AV1 on the native path yet" comments here and host-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0b7e4a00ee |
fix(host): crash visibility + Windows deliberate-quit teardown; pin outranks quit
Two strands from the "Windows host went Offline after reconnect, zero errors in the logs" investigation. Crash visibility — the field reports kept arriving with nothing in the Logs tab because every way the host can die silently was unlogged: - A panic hook tees each panic (thread, location, backtrace) through `tracing` into the in-memory ring + host.log before the default hook runs — a panicking thread otherwise only hit stderr, absent from the web console and gone when stderr is detached. - A last-resort Windows SEH filter (windows/crash.rs) logs an unhandled native exception with its code, faulting address, and faulting MODULE — the smoking gun that separates our bug from a GPU runtime/driver DLL (amfrt64, the UMD, d3d11) crashing under us. - A 10 s watchdog around the vdisplay monitor teardown logs an ERROR when the driver REMOVE/CCD-restore looks wedged (it runs under the manager state lock, so a hang there silently blocks every future acquire). Deliberate-quit teardown (Windows pf-vdisplay) — the Windows manager never wired the linger skip the Linux registry has: on ⌘D (the QUIT close code) the monitor lingered 10 s, so a quick reconnect hit the Lingering-preempt's back-to-back REMOVE→ADD churn. A per-lease quit flag (VirtualDisplay:: set_quit_flag) now tears the monitor down immediately on a deliberate quit, so the reconnect finds the manager Idle and does a clean fresh ADD. Pin outranks quit (both platforms) — keep_alive=forever (the gaming-rig preset) promises "the screen stays alive", so a deliberate quit must skip only the linger WINDOW, never the pin. Windows release() checks keep_alive_forever() before the quit; the Linux registry had the inverse bug (force_immediate tore down even a pinned display) — fixed via effective_linger() with a unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dc834ea478 |
fix(rumble): accept the v2 vibration valid-flag; bound stale buzz to 1.5 s
Two hardenings against runaway vibration (one Deck session buzzed on a ~2 s cadence): the DualSense output-report parse now also treats valid_flag2 COMPATIBLE_VIBRATION2 (data[39] BIT2) as rumble-carrying — a writer hardcoding the ≥2.24-firmware convention previously had its rumble INCLUDING stops silently ignored, and a missed stop re-sends stale nonzero state forever via the host's 500 ms refresh. And the client's SDL rumble duration drops 5 s → 1.5 s: long enough that a couple of lost 500 ms refreshes don't gap genuine rumble, short enough that stale state dies on its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a41f679516 |
perf(host): one input channel — motion applies on arrival, not after the 4 ms tick
Rich input (gyro at the pad's report rate, trackpad contacts) rode a second mpsc channel that the input thread only drained after its main channel's 4 ms recv timeout — so during pure-gyro aiming (no button/pointer traffic, the common case) every motion sample ate up to 4 ms of added latency and quantization before reaching the virtual pad. At the Deck's 250 Hz sensor cadence that is a full sample period of jitter. Both planes now share one ClientInput channel: the thread wakes the moment either arrives and applies rich input immediately. The 4 ms timeout stays as the feedback-pump tick. Also adds a debug-level motion-cadence metric (inter-arrival p50/p95/max per 5 s window) so gyro delivery is measurable end-to-end instead of 'feels floaty'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e6d9454251 |
feat(host): fold Steam Deck to DualSense on Windows; split-pad touch + pad clicks
A Deck client asking for 'steamdeck' on a Windows host resolved to Xbox360, whose apply_rich is a no-op — gyro and both trackpads silently discarded. Windows already ships a full DualSense backend (UMDF driver, touchpad + motion, wire units 1:1), so pick_gamepad now folds SteamDeck -> DualSense there. The three near-identical DualSense-family appliers (Linux UHID, Windows DualSense, Windows DS4) are hoisted into one shared dualsense_proto::DsState::apply_rich, with two mapping upgrades for Steam dual-pad clients everywhere: * the Deck's two pads SPLIT the single DualSense touchpad — left pad -> contact 0 on the left half, right pad -> contact 1 on the right half — mirroring the physical thumb layout and the split-pad zones games and Steam Input already use (the left pad was previously dropped outright) * TouchpadEx pad clicks now press the touchpad-click button (persisted in DsState::touch_click, OR-ed in by both serializers; previously dropped by every DualSense-family backend, Linux included) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9ca672d434 |
fix(host): trackpad Y rides the wire in screen convention, flips at the Deck boundary
Both Deck trackpads were inverted on the virtual pad (first live Deck-to-Deck session): the client sends SDL's screen-convention coordinates (+y down) onto the wire, but the Deck's raw report fields are stick convention (+y up) — and Steam Input parses our report as real Deck hardware — while the host applier passed the value straight through. Pin the wire meaning down (quic.rs: TouchpadEx/Touchpad are screen convention, +x right +y down — what every client capture API produces) and translate to device-raw in the host applier, the layer that knows the quirk: steam_proto's apply_rich now negates y (saturating: -32768 has no i16 negation) for both TouchpadEx surfaces and the DualSense-standin Touchpad arm, which carried the same latent inversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
062a54e3a5 |
style: cargo fmt --all (rustfmt 1.96.0 drift across the re-arch branch)
`cargo fmt --all --check` (ci.yml) was red on main: the client re-architecture
commits and origin's windows-shortcut commit landed with rustfmt violations
(e.g. a 104-char .with_context line in hyprland.rs, an unsorted mod block in
vdisplay.rs, the input.rs `{`-placement CI flagged). Reformat the tree so the
fmt gate passes; no functional changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4543a3f529 |
feat(video): HDR10/P010 end to end (phase 6)
The client now advertises VIDEO_CAP_10BIT|HDR and carries the result all the way to glass: - csc_rows is bit-depth exact (10-bit limited code points differ from 8-bit by ~half a code) and folds in the P010/X6 MSB-packing factor; new 10-bit white/black tests. - The CSC shader grows a params block: mode 0 passes the transfer through (SDR as-is, or PQ onto an HDR10 swapchain); mode 1 tonemaps PQ→SDR in-shader (ST.2084 EOTF, 203-nit reference white exposure, BT.2020→709, soft maxRGB rolloff, sRGB encode) for desktops without an HDR surface. PUNKTFUNK_TONEMAP_PEAK tunes the rolloff. - The presenter probes VK_EXT_swapchain_colorspace + an HDR10/ST.2084 10-bit surface format and flips modes in-band with the stream's PQ signaling: fence-quiesce, then CSC pass + video image (10-bit A2B10G10R10 intermediate — PQ in 8 bits bands) + overlay pipe + swapchain rebuild through the deferred-destroy rules. - P010 decodes through all three paths: Vulkan Video (X6 multiplanar pool, R10X6 plane views), VAAPI dmabuf (R16/RG1616 plane imports), software (swscale as before). - session pump advertises the caps; the host still gates Main10 behind its PUNKTFUNK_10BIT policy. Probed on glass hardware: the KDE/NVIDIA surface exposes A2B10G10R10+HDR10_ST2084, so true PQ passthrough is available there. Known v1 gaps: software-decode PQ shows untonemapped (8-bit RGBA carries the transfer baked); the SDR overlay composites unscaled onto an HDR10 surface (dim OSD); no vkSetHdrMetadataEXT yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
349d16382e |
fix(stats): decode stage measures GPU completion; Alt+Enter fullscreen alias
The Vulkan path's receive_frame returns at SUBMISSION (~0.1 ms) — the hardware decodes asynchronously, so the decode stat was truthful but measured the wrong boundary. The pump now ships the frame to the presenter FIRST, then waits the frame's timeline fence (vkWaitSemaphores resolved through the shared device's proc chain) and stamps received→decode-COMPLETE — true NVDEC time at zero pipeline cost, since the presenter's own GPU wait is what actually gates sampling. Software/ VAAPI keep their synchronous stamps. Also: Alt+Enter joins F11 as the fullscreen toggle (some keyboards' Fn layer sends a media key for plain F11 — observed on glass as 'F11 only works with shift'); the shell's shortcuts panel lists both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a5bb5ec4d5 |
fix(input): pointer lock + the missing per-iteration motion flush
Two capture bugs compounding into 'sluggish and not captured': 1. flush_motion was never called from the run loop — the GTK client's frame-clock tick flushed pending motion every frame, and the port lost it. Pure mouse movement only reached the host when a click/key/ scroll happened to flush it; the host cursor simply didn't follow. Now one coalesced MouseMove per loop iteration. 2. Capture forwarded ABSOLUTE letterboxed coordinates with no pointer confinement (GTK parity, the plan's deferred 'stage-2' item): the visible local cursor outran the host cursor by the full e2e and escaped the window. Capture is now real pointer lock — SDL relative mouse mode (hidden, confined, raw deltas) with relative MouseMove on the wire, so the host cursor is the only cursor. Also: focus-gain re-engages after an auto-release (Alt-Tab undoes itself; the startup focus race can no longer strand the session uncaptured) while a chord release stays released until the user opts back in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6cb97959a2 |
docs(video): three-backend reality + log software-by-preference
The silent settings-driven software path cost a debugging round on the first Vulkan Video glass test (stale decoder=software from the VAAPI- broken-on-NVIDIA era) — now it says so. Module docs updated: auto is vulkan → vaapi → software. Validated on glass (RTX 5070 Ti, HEVC 4K@144): decode 11 ms → 0.1 ms, e2e p50 ~115 ms → 8.6 ms (the old number was software-decode queueing, not clock skew), 144 fps locked, 2686 frames, zero errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a1b890ef42 |
fix(host): coalesce keyframe-request storms into one IDR per cooldown
Clients request a keyframe on every FEC-unrecoverable frame and keep asking until the IDR actually arrives and decodes — a full round-trip on a link that is already behind. The host answered every request with a full IDR and only rate-limited under intra-refresh (opt-in, off by default), so on the default path a Wi-Fi loss burst produced a 20-40x bitrate spike storm that deepened the very loss it was recovering from — the second half of the periodic double-jolt. Coalesce a request storm into at most one forced IDR per cooldown ALWAYS: serve the first immediately (a genuinely wedged decoder still recovers at once), then suppress for the window (2 s under intra-refresh's healing wave, 750 ms for a full-IDR recovery — long enough to swallow one recovery event's round-trip echo, short enough to re-issue a lost IDR promptly). Seed the cooldown at session open and stamp it on both rebuild paths so the cold-open / post-rebuild storm coalesces into the IDR the fresh encoder already emits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1dc8dc7f0d |
fix(packaging): open mgmt/library port 47990 on the LAN firewall profiles
apple / swift (push) Successful in 1m11s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
android / android (push) Successful in 3m49s
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
The mgmt REST API has bound 0.0.0.0:47990 by default since
|
||
|
|
efed8d5a20 |
fix(host): AMF encoder reconnect reliability on Windows AMD
A second connection to a Windows AMD host came up black with nothing in the logs. The native AMF encoder's teardown never Flushed before Terminate, so a reconnect whose teardown overlapped the new session (a client may not signal an explicit exit, so session 1 tears down late — on the reconnect preempt grace or the QUIC idle timeout) left AMD's limited VCN encode-session slot occupied. The new session's Init then opened onto a wedged session that returns AMF_OK but never emits an AU. NVENC has no equivalent per-session cap, so NVIDIA never showed it. Recovery couldn't help either: the stall watchdog re-Init'd the SAME context, which can't clear a context/VCN-level fault, so it looped a dead context until MAX_ENCODER_RESETS ended the session. Reliability: - Component::drop now Flushes before Terminate (mirrors reset() and the design doc), releasing the VCN session cleanly so the next session's Init gets a free slot. - reset() escalates to a FULL context teardown once an in-place re-Init has run without producing an AU (resets_without_output >= 2), so a wedged reconnect self-heals via a fresh CreateContext+InitDX11 within the reset budget instead of re-initing a dead context in a loop. Logging (the failure was silent): - Per-context bring-up sequence number (context #N) — distinguishes a first connection from a reconnect's fresh context. - A one-shot "AMF produced its first AU on this context" line; its absence after a context #N bring-up is the smoking gun for a silent VCN wedge. - Terminate result logged on drop for both the component and the context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
636b9c1d1f | Merge remote-tracking branch 'origin/main' | ||
|
|
92f078adaf |
fix(host): survive Bazzite's SDDM session supervisor in the gamescope takeover
Diagnosed live on the .181 Bazzite F44 box (couldn't connect at all; field reports of streams dying after 30 s-5 min): Bazzite autologs into game mode via SDDM with Relogin=true, so the moment the managed takeover stops gamescope-session-plus@<client>, SDDM logs back in and restarts it within the same second. The resurrected autologin session then fights our transient session-plus over the Steam single instance and the GPU for the whole stream: its wrapper relaunches gamescope every ~7 s (each one missing the wrapper's hard 5 s readiness window on a slow NVIDIA init), the churn SIGSEGVs gamescopes, and eventually the streaming gamescope dies with it. Meanwhile a client that gave up left the pipeline-rebuild retry loop SIGKILLing and relaunching the box's Steam session for up to ~6 more minutes. - stop_autologin_sessions: runtime-mask each autologin unit before the SIGKILL stop, so no supervisor can restart it underneath the stream; match every loaded instance (the unit flaps through activating/failed mid-churn). Every restore path unmasks unconditionally (including the desktop-active early return), and --runtime keeps the mask in tmpfs so a reboot clears it. - launch_session: supervise the transient unit while polling for the node — the session-plus wrapper kill -9s a gamescope that missed its 5 s readiness handshake and exits 1, so relaunch it (after a short driver-settle cooldown) instead of waiting the rest of the 45 s on a corpse. - build_pipeline_with_retry: abort between attempts once the session's QUIC connection is closed — no more minutes of Steam churn for a departed client. Validated live on .181: cold-boot connect streams 2059 frames/45 s (p50 5.1 ms), zero SDDM resurrections while masked, TV session restored+unmasked on disconnect, warm same-mode reconnect reuses the session (866 frames/15 s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
eea23c5647 |
fix(core,host): make the native data plane survive real Wi-Fi links
Root-caused live on a phone at 100 Mbps (stream stuck seconds behind, then oscillating): a stack of transport defects, each amplifying the next. - MTU-safe shards: shard_payload 1452 overshot the IPv4/1500 budget (the old math forgot the 40 B header + 24 B crypto ride inside the UDP payload and counted IP+UDP as 8 B) — the kernel silently split EVERY video datagram into two IP fragments, doubling per-datagram loss on Wi-Fi. New config::mtu1500_shard_payload() = 1408 (1472 sealed = the exact ceiling), negotiated in the Welcome, pinned by a unit test. - Android batched I/O: recv/send batching was cfg(linux); Android is target_os="android" and silently fell back to a syscall per datagram. The libc crate binds neither recvmmsg/sendmmsg nor mmsghdr for Android, so a local bionic extern binding provides them (API 21+, floor is 28); cbindgen excludes them from the C header. The pump/runtime threads also get the Apple-QoS analogue on Android: nice −8 (below the decode thread's −10). - Latency-bounded receive: packets are consumed strictly in order at exactly the arrival rate, so a standing queue (Wi-Fi stall, power-save clumping) NEVER drains — observed as a stream permanently 6-7 s behind with both 32 MB socket buffers full. The pump now flushes the entire backlog (Session::flush_backlog: discard ring + kernel queue at memcpy speed, reset the reassembler) and requests a keyframe when frames keep completing > 400 ms behind the skew-corrected capture clock (30 consecutive, 2 s cooldown, logged). - Time-based loss window: the reassembler declared an incomplete frame lost a fixed 4 INDICES behind the newest — 33 ms at 120 fps, inside normal Wi-Fi retry/reorder timescales, so merely-late frames were pruned every few seconds, each costing a recovery-IDR burst + an inflated loss report. Now 120 ms of capture time (LOSS_WINDOW_NS), same fuse at every refresh rate, with a 64-index hard cap bounding memory against hostile pts. - Adaptive-FEC hysteresis: the controller was memoryless — one clean 750 ms report dropped FEC from 8 % straight back to the 1 % floor, so periodic burst loss (Wi-Fi scan / BT coexistence beats) always hit an unprotected stream and ping-ponged 1↔8 % with a frozen frame per cycle (observed in the host log as alternating loss_ppm=0/50000). Attack stays instant; decay is now one point per clean report. Verified: full core suite (incl. new flush + time-window tests) on macOS + Linux, host release build, arm64 cargo-ndk build, and a 30 s wired probe run at 2800x1260@120 — 3559/3559 frames, zero loss, capture→received p50 5.3 ms (host 5.1 + network 0.3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b88aa63c2 |
style: rustfmt drift from the native AMF SDK encoder
The native AMF encoder (
|
||
|
|
3ed0ceef8f | Merge remote-tracking branch 'origin/main' | ||
|
|
c966246e0c |
fix(host): stop force-disabling zero-copy on Mutter+NVIDIA by default
apply_session_env unconditionally forced PUNKTFUNK_FORCE_SHM=1 for every GNOME/Mutter session, added 2026-06-14 after a same-day stale-frame bug hunt found Mutter has no implicit dmabuf fence on NVIDIA. That override silently contradicted the documented "zero-copy is on by default for all Linux GPU backends" behavior and left Mutter+NVIDIA hosts on the slower CPU/SHM path unconditionally, with no way to opt back in. Live retesting (192.168.1.21, RTX 5070 Ti, real client with cursor movement/window drag/typing — the historical trigger) shows no visible staleness with the override removed. Drop the automatic force; PUNKTFUNK_FORCE_SHM stays as a manual escape hatch for anyone who does hit flashing/stale frames on a Mutter+NVIDIA host. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
6f47abab8c |
feat(host): native AMF SDK encoder for Windows AMD — drop libavcodec
Direct-SDK AMF encoder (encode/windows/amf.rs), the AMD analogue of the direct-NVENC path, replacing the libavcodec *_amf dispatch. C-vtable FFI pinned to AMF headers v1.4.36, runtime-loaded from the driver's amfrt64.dll (no build feature, no new dependency) exactly as NVENC loads its DLL. - AVC/HEVC (SDR NV12 + 10-bit HDR P010) and AV1 (RDNA3+, probed); a bounded poll retires the libavcodec ~2-frame output hold; native in-place reset(). - Intra-refresh wave (PUNKTFUNK_INTRA_REFRESH), in-band HDR mastering/CLL metadata (*InHDRMetadata -> HEVC SEI / AV1 OBU), and a native codec probe feeding the GameStream advertisement (windows_backend_is_ffmpeg -> windows_backend_is_probed). - AMD dispatch / advertisement / 4:4:4 are native-only; the libavcodec AMF fallback and the PUNKTFUNK_AMF_FFMPEG hatch are removed. FFmpeg serves QSV only (its AMF path retained solely as the latency A/B comparator). - Overload back-pressure: submit bounds in-flight surfaces below the input ring, draining finished AUs (buffered for poll, FIFO-preserved) to free a slot and retry on AMF_INPUT_FULL instead of tearing the encoder down and forcing an IDR; this also closes a latent ring-overwrite corruption seen under load on-glass. Validated on the lab Ryzen iGPU (AMF runtime 1.4.37): HEVC/AVC across a native reset, HEVC Main10 mastering+CLL SEIs byte-verified, intra-refresh accepted, a backpressure burst FIFO-clean, and end-to-end via the macOS client. Measured §5.2 latency A/B: native encode_us p50 ~5 ms (0.31 frame periods) vs libavcodec ~17 ms (1.01). 4:4:4 stays unsupported (VCN hardware limit). Live-gated tests skip cleanly on non-AMD boxes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eaaf176adf |
style: rustfmt drift from the AMF/watchdog commits
`cargo fmt --all --check` (a CI gate) failed on main: config.rs (the new
`zerocopy: val(...).map { !matches!(...) }` from
|
||
|
|
76bc7fee97 |
feat(host): default the Windows AMF encode input to zero-copy D3D11
On-glass A/B on the Ryzen 7000 iGPU (1080p120 HDR P010, hevc_amf,
PUNKTFUNK_PERF stage split): the system-memory readback costs the encode
thread 2.7-2.9 ms p50 (6.6 ms p99) per frame in submit; the zero-copy D3D11
pool path does the same work in 0.26 ms p50 (0.5 ms p99) — and on an iGPU the
readback also burns the shared memory bandwidth the game needs. The docs-site
already promised "on by default ... D3D11 on Windows" since the Linux flip
(
|
||
|
|
167d590349 |
fix(host): encode-stall watchdog — heal the silent AMF/QSV freeze in place
Field reports: Windows AMD/Intel streams freeze after ~3-5 min regardless of desktop activity. Root cause: the libavcodec AMF/QSV poll is non-blocking (EAGAIN -> Ok(None)), and the encode loop's drain treated None as benign without popping `inflight` — a wedged driver (QueryOutput stops producing) meant frames kept being submitted, inflight grew unboundedly, no AU ever reached the send thread, and nothing logged: a silent permanent freeze. The input-side twin: once libavcodec's one-frame buffer fills, avcodec_send_frame EAGAINs and the submit `?` killed the whole session. Add `Encoder::reset()` (in-place encoder rebuild; implemented for AMF/QSV by dropping the wedged libavcodec encoder so the next submit re-opens it on the current device, forced IDR) and an encode-stall watchdog in the stream loop: trip on a poll error, on no AU within max(2 s, 8 frame intervals) while frames are owed, or on an owed backlog worth more than the window's frames (the slow-leak latency-runaway form). Recovery is a bounded (5 consecutive, cleared by any delivered AU) in-place rebuild + forced IDR — a logged ~one-second hiccup instead of a dead stream; exhaustion or a reset-less backend still fails the session with a clear error. Submit failures route through the same bounded recovery. The three existing pipeline-rebuild paths (session switch, mode switch, capture loss) now also clear the stale in-flight records that pointed at the dropped encoder. Backends whose poll blocks (direct NVENC sync, software) can't false-trip: they never return Ok(None) mid-stream and drain inflight below depth each tick. Validated: clippy -D warnings (nvenc,amf-qsv), 191 host tests, synthetic E2E 300/300 frames, and an on-glass AMD iGPU session (1080p120 HDR hevc_amf). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
622c8bf701 |
fix(host): silence write-only cur_node_id on non-Linux encoder builds
cur_node_id (the capture 5-tuple's node id, added for the Linux dedicated- game-exit check) is read only under #[cfg(target_os = "linux")], so on the Windows nvenc/amf-qsv build it was assigned but never read — failing `clippy -D warnings`. Read it on non-Linux platforms (the `let _ = &launch` idiom already used in this file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9814368c8c |
feat(host): enable GPU zero-copy by default on all backends
Now that the per-capture worker subprocess makes an NVENC EGL/CUDA driver fault survivable (design/zerocopy-worker-isolation.md), the reason the NVENC zero-copy path stayed opt-in is gone. zerocopy::enabled() now defaults ON for both GPU backends (was ON VAAPI / OFF NVENC). Fallbacks are intact: VAAPI's one-shot CPU auto-downgrade (VAAPI-gated, never trips for NVENC) and NVENC's per-capture fallback + worker-death latch. Reframe the shipped host.env examples and setup guides to rely on the default rather than force PUNKTFUNK_ZEROCOPY=1 (an explicit =1 skips the VAAPI auto-downgrade). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7257bcb6a6 |
fix(host): preserve a physical monitor's refresh when adding a virtual display
Connecting reset an existing physical monitor's refresh (e.g. 120->60 Hz) because the topology code read the physical's mode AFTER the virtual output perturbed the compositor layout — by which point it had already been downgraded. Read/preserve each physical's mode from a pre-connect snapshot. - Mutter: build_primary_keeping_physicals takes the pre-virtual snapshot and preserves each physical's real mode (pick_keep_mode, unit-tested) - KWin: capture each output's mode when disabling for exclusive, re-assert it on re-enable (a bare enable defaulted to ~60 Hz) - Windows: skip the refresh-resetting SDC_TOPOLOGY_EXTEND when a physical is already active Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
79c4f4cbc1 |
feat(host): user-defined custom display presets
Save named bundles of the display-management policy (the six behavior axes
plus the game-session axis) as custom presets, alongside the built-ins. A
custom preset is data — stored in <config>/display-presets.json — not a Preset
enum variant, so DisplayPolicy::effective() stays pure and the built-in set is
untouched; applying one writes a Custom policy via the existing PUT
/display/settings.
- policy.rs: CustomPreset/CustomPresetInput + load/add/update/delete store
- mgmt.rs: GET/POST /display/presets + PUT/DELETE /display/presets/{id},
surfaced on GET /display/settings
- web console: custom-preset cards with save-as / edit / delete + i18n
- regenerated api/openapi.json; docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
1d17524151 |
feat(host): isolate the zero-copy GPU import in a worker process
The tiled EGL/GL→CUDA import crashed the whole host (SIGSEGV inside libnvidia-eglcore via cuGraphicsMapResources) when the compositor invalidated an imported dmabuf mid-map — reproduced on the Bazzite F44 Game→Desktop switch (design/zerocopy-hardening-handoff.md). A driver SIGSEGV is uncatchable in-process, so the whole EglImporter (tiled EGL/GL→CUDA and LINEAR Vulkan→CUDA) now runs in a per-capture `zerocopy-worker` subprocess: dmabuf fds go over a SEQPACKET socketpair (SCM_RIGHTS, sent once per buffer keyed by dmabuf st_ino; NeedFd resend self-heals cache desync), frames come back as CUDA-IPC pooled device buffers (still zero-copy, +one socket RTT/frame). Worker death poisons the capturer so the existing capture-loss rebuild runs — the host survives; 3 consecutive deaths latch the GPU import off (CPU/SHM path). PUNKTFUNK_ZEROCOPY_INPROC=1 keeps the old in-process import for debugging/A-B. Also fixed along the way: a failed *tiled* import no longer falls through to the CPU mmap de-pad (which scrambled tiled bytes; LINEAR keeps the fallback); Nv12Blit dropped its GL textures while still CUDA-registered (unregister now runs first); GlBlit had no Drop at all (GL objects leaked per size change); VkBridge's per-fd src cache is now invalidated on renegotiation/eviction instead of never. Design: design/zerocopy-worker-isolation.md. Unit tests: 14 new (protocol fd-passing, worker dispatch, client handshake/death/NeedFd, death latch). On-glass validated on the RTX 5070 Ti/GNOME box (.21): the worker path streams at p50 1.30 ms (NV12, 1800 frames 0-mismatched, parity with the in-process path), and a kill -9 of the worker mid-stream is survived by the host and recovered — poison -> capture lost, rebuilding pipeline in place -> a fresh worker in ~185 ms -> streaming resumes (2385 frames, 0 mismatched). A real KWin compositor-crash repro is still pending (a worker kill -9 is strictly harsher, so it corroborates). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1992eb1c52 |
feat(host): game-mode integration + dedicated game sessions
Implements design/gamemode-and-dedicated-sessions.md (Parts A1-A5 + B0-B2):
reconciles the merged display-management registry with session-mobile
Bazzite/SteamOS hosts and adds a per-launch dedicated gamescope mode.
- A1 DisplayOwnership {Owned,External,SessionManaged} + poolable_now(): the
registry pools only what it owns, so gamescope managed/attach outputs are no
longer double-owned by the registry AND the gamescope restore worker (fixes
the game-mode-reconnect stale-node wedge).
- A2 validated reuse: (backend,mode,launch,epoch) reuse key + kept_display_alive
liveness probe + reused_gen/mark_failed on a reused-display first-frame failure.
- A3 policy-driven managed restore (keep_alive replaces the hardcoded 5s debounce;
forever = held = gaming-rig truthful) + crash-restore persist + SIGKILL teardown
(kill_unit, applied to our transient unit AND the autologin stop -- validated
live on .181 to avoid the F44 GPU-context leak).
- A4 session epoch: observe_session_instance bumps the epoch + invalidate_backend
on a desktop-compositor instance change; gamescope spawns are exempt.
- A5 per-spawn log + PID-scoped gamescope node discovery.
- B0 game_session {auto,dedicated} policy (top-level, preset-orthogonal) +
pick_gamescope_mode dedicated_launch + steam -silent command shaping.
- B1 free the autologin Steam before a dedicated Steam spawn (single-instance).
- B2 game-exit -> APP_EXITED_CLOSE_CODE (0x52) clean session end.
Adversarially reviewed (11 findings fixed). Validated on glass (.181 Bazzite F44,
RTX 4090): dedicated spawn streams a real game smoothly; keep-alive reuse; the
SIGKILL fix avoids the F44 vkCreateDevice leak. Workspace green
(build / test --workspace / clippy -D warnings / fmt), OpenAPI + C header
regenerated, web console tsc + vite build green. clients/probe: bump the
no-video timeout 8s->45s for gamescope cold starts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d1c90ca24f |
docs(readme): Windows host capture is IDD-push, not DXGI/WGC
The DXGI Desktop Duplication + WGC relay paths were removed; sealed IDD-push (finished frames pushed straight into the host's own IddCx driver, no screen-scraping) is now the sole Windows capture path. Fix the stale "DXGI/WGC capture" claims in the root and punktfunk-host READMEs, which also contradicted the push-based IDD description already present in the root README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
07c8b868e6 |
style(host): cargo fmt --all (rustfmt 1.9.0 drift)
The CI image's rustfmt reformats these files (multi-line assert!/tracing! macros, match-arm and struct-variant wrapping) — pre-existing drift that the Format job caught. Reformat to match. Pure formatting; no logic change. main.rs also gets a blank line before a standalone comment so rustfmt stops mis-indenting it as a trailing-comment continuation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
51c8dda585 |
feat(vdisplay/mutter): pin the client's refresh by default; drop PUNKTFUNK_MUTTER_VIRTUAL_REFRESH
The >60 Hz virtual-monitor path (RecordVirtual "modes" with the client's exact WxH@Hz) was gated behind PUNKTFUNK_MUTTER_VIRTUAL_REFRESH, default OFF, after a high-refresh virtual CRTC SIGSEGV'd gnome-shell on session teardown. That crash was since fixed by stopping the screencast before any monitor reconfig, so the gate is dead weight — and a silent footgun: every non-headless GNOME client was capped at Mutter's PipeWire-derived 60 Hz unless they knew the hidden flag. Make it the default: the custom-mode path now runs whenever mode.refresh_hz > 60 (≤60 Hz stays byte-identical to before — Mutter's 60 Hz default is already correct), and the virtual_refresh_enabled() env read is removed. Docs updated (configuration.md env table, vrr-plan.md reference). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |