55dfb4800f638f8261aad1f059d7b92e270efec2
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92c6da9546 |
fix(capture/mutter): restore zero-copy + sync via dmabuf implicit fence
ci / web (push) Failing after 42s
apple / swift (push) Failing after 1m5s
ci / rust (push) Failing after 1m10s
ci / docs-site (push) Failing after 44s
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 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
deb / build-publish (push) Successful in 2m54s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 5m13s
The previous attempt ( |
||
|
|
8531135bb7 |
fix(capture/mutter): stale-frame flashes + stuck input after disconnect on GNOME
ci / web (push) Failing after 49s
apple / swift (push) Failing after 1m4s
ci / rust (push) Failing after 1m9s
ci / docs-site (push) Failing after 42s
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 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m58s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 4m17s
Deep dive into the two GNOME-only host bugs (KWin/gamescope clean):
1. Stale-frame flashes (windows at old positions, typed text reverting):
Mutter renders its virtual monitors DIRECTLY into the PipeWire buffer
pool, and NVIDIA has no implicit dmabuf fencing — our zero-copy
import raced the render and encoded each pool buffer's PREVIOUS
contents. Fix, in order of preference:
- Consumer-side PipeWire explicit sync (SPA_META_SyncTimeline): new
drm_sync module (DRM timeline-syncobj wait/signal via raw ioctls,
unit-tested incl. a live signal->wait round trip); announced
post-format via update_params (the OBS pattern — at connect time
the meta makes producers fail allocation, observed on KWin), with
a blocks=3 Buffers filter so the producer's sync pod wins; acquire
point awaited before any read (GPU import or CPU mmap), release
point signaled on every path.
- Where the producer can't do explicit sync (Mutter on NVIDIA today:
no cogl sync_fd, "error alloc buffers"), a sticky fallback flips
the capture to the synchronous CPU/shm path — Mutter's glReadPixels
download orders against its render, so frames are correct by
construction. First session pays one ~10 s probe+retry; later
sessions go straight there. Validated live on home-worker-3
(GNOME 50 + RTX 4090): clean fallback, 30 MB HEVC streamed.
- Sync is only announced on Mutter sessions (new VirtualOutput.mutter
tag): KWin+NVIDIA fails allocation when merely asked, and doesn't
need it (verified unchanged: zero-copy CUDA import + 1.1 MB/10 s).
PUNKTFUNK_EXPLICIT_SYNC=0 disables the probe outright.
2. Clicks wedged in the focused app after disconnect+reconnect: a client
vanishing mid-press left keys/buttons latched in the compositor —
Mutter keeps the destroyed EIS device's implicit grab and the focused
app stops taking clicks until restarted. EiState now tracks held
keys/buttons/touches (wire codes) and synthesizes releases through
the normal inject path before the EIS connection goes away.
GNOME hosts on NVIDIA temporarily lose zero-copy (correctness over
throughput); the moment Mutter+driver gain working explicit sync, the
sync path engages automatically and zero-copy returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
86f463cf71 |
fix(housekeeping): unaligned read UB + recv-drop parity; dedup mmsghdr; doc fixes
ci / rust (push) Has been cancelled
From a bug-hunt + unsafe-audit pass (4 reviewers + adversarial verify). It confirmed ZERO real bugs in the recent batched/paced data-plane work — these are the surfaced cleanups + one genuine soundness fix: - SOUNDNESS (reduce unsafe): inject/gamepad.rs::pump_ff did `ptr::read` of an InputEventRaw (align 8, holds a timeval) out of a 1-aligned [u8; N] buffer — UB per the reference (x86_64 tolerates it, but it can miscompile under LTO). Use ptr::read_unaligned + a SAFETY note. Zero behavior change. - recv parity: recv_batch (recvmmsg) didn't drop an oversized/truncated datagram the way scalar recv does — poll_frame now skips a message whose len fills the buffer (> MAX_DATAGRAM_BYTES), matching recv's `n >= RECV_BUF` drop. (AEAD already rejected these on encrypted sessions; this restores the documented invariant on the batched path.) - dedup unsafe FFI: factor the identical mmsghdr-from-iovec construction out of send_batch + recv_batch into one `mmsghdrs()` helper — the raw-pointer scaffolding + its lifetime SAFETY note now live in one place. - docs: TARGET_SOCKBUF no longer calls paced sending future work (it landed, m3.rs::paced_submit); gamescope.rs input is no longer "(TODO)" (wired + live-validated); the PUNKTFUNK_PERF `wire_mbps` field is renamed `tx_mbps` and noted as attempted/sealed bytes (send_dropped shows what didn't reach the wire). Full suite (35 + loopback round-trip + 6) + clippy + fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c894c6f897 |
feat(host): host-managed gamescope session at the client's mode (dynamic res + refresh)
ci / rust (push) Has been cancelled
Nested games on the Bazzite host saw the wrong display: refresh capped at 60 Hz, the box's connected TV's EDID modes leaking in (DOOM landed on 2560×1440@60), and the resolution fixed at whatever the always-on session was launched at — the client's requested mode never reached the game. Root causes: the session-plus gamescope command has no --nested-refresh (Xwayland advertises 59.96 Hz for every mode), --prefer-output HDMI-A-1 makes gamescope read the TV EDID, and the ATTACH model launches one fixed-resolution session. New vdisplay path: PUNKTFUNK_GAMESCOPE_SESSION=<client> — the host LAUNCHES gamescope-session-plus headless AT THE CLIENT'S mode and relaunches it when the mode changes. Injected via a host-written GAMESCOPE_BIN wrapper (--nested-refresh $PF_HZ, the flag session-plus doesn't expose) + DRM_MODE=cvt (gamescope generates clean CVT modes at that refresh instead of the TV's EDID). The session runs as a transient `systemd-run --user` unit (clean cgroup teardown of the Steam tree); state lives in a host-lifetime static (MANAGED_SESSION), NOT in GamescopeDisplay (which is per-client-session) — so a same-mode reconnect REUSES the running session instantly (no Steam restart) while a different mode RELAUNCHES it (games can't change output mode live; a game/Steam restart on a mode change is unavoidable and acceptable). Reuses the existing node + EIS auto-discovery (find_gamescope_node / find_gamescope_eis_socket, factored into point_injector_at_eis) and the existing mid-stream Reconfigure → vd.create(mode) machinery — no protocol or m3 control-flow change. Validated live on bazzite (RTX 4090): games' Xwayland now advertises 5120×1440 @ 239.90 Hz as the preferred mode (was 59.96), the TV's 3840×2160/4096×2160@60 modes are gone, frames stream; reconnect at 1920×1080@120 relaunches and games see that; same-mode reconnect reuses with no restart and frames flow instantly. scripts: host.env.example documents PUNKTFUNK_GAMESCOPE_SESSION (mutually exclusive with the legacy NODE=auto attach); punktfunk-steam-session.service marked deprecated (superseded — must not run alongside the host-managed path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9128bc3836 |
feat(host): attach to a running gamescope session (Bazzite headless Steam)
Bazzite (and SteamOS-like hosts) run Steam Big Picture inside their OWN gamescope-session-plus session. Nesting a second gamescope+Steam can't work — the second Steam sees the first and exits, taking the nested gamescope down with it (crash in its exit handlers), killing both video and input. The robust model is to let punktfunk OWN that session: run gamescope-session-plus headless at the client's resolution (full Steam Deck UI polish: MangoApp, VRR, controller config) and have the host ATTACH to it rather than spawn its own. The video half already existed (PUNKTFUNK_GAMESCOPE_NODE=<id> attaches to a PipeWire node). This finishes it: - PUNKTFUNK_GAMESCOPE_NODE=auto discovers the gamescope Video/Source node, so the (dynamic) node id needn't be hand-wired. - The attach path now also points the libei injector at the running session's EIS socket: find_gamescope_eis_socket() scans XDG_RUNTIME_DIR for gamescope-<N>-ei, connect()-probes each (stale dead-session sockets refuse), and writes the newest live one to the relay file the injector reads. So input reaches the attached session with zero manual config. scripts/punktfunk-steam-session.service: a systemd --user unit that runs gamescope-session-plus headless at a configured resolution, with the one-time headless-appliance setup (linger + multi-user.target) documented inline. Validated live on bazzite (RTX 4090): the full Steam Big Picture session streams (1499 frames, p50 ~1ms) with mouse/keyboard injected into it (device resumed, all caps, emitted=true), node + EIS socket both auto-detected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
609136cd2d |
fix(inject): make the gamescope EIS injector reconnect robustly across sessions
ci / rust (push) Has been cancelled
Root cause of "input doesn't work" on the unified host: a single fresh session injects fine (EIS connects, "Gamescope Virtual Input" device added), but the host-lifetime injector reused a STALE per-session EIS socket across sessions → "connect EIS socket …: Connection refused". (Headless gamescope is EIS-only — it ignores uinput — so libei/EIS is the one input path for both gamescope and KWin; no second path needed.) - connect_socket_file: re-READ the relay file and RETRY the connect on refused/missing (the live gamescope's EIS appears shortly), bounded at 15s, instead of connecting once and bubbling ECONNREFUSED. - GamescopeProc::drop: clear the relayed EIS socket name on teardown so a dead session can't hand a stale path to the next reconnect. Validated: two sessions back-to-back each reconnect (EIS connected + device added). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6fdf7d1511 |
feat: client-selectable compositor (protocol → host → client → C ABI → mgmt → web)
A client can now request which compositor backend the host drives its virtual
output on (gamescope/KWin/Mutter/wlroots). The host honors the request if that
backend is available, else falls back to auto-detect and reports the resolved
choice back — wire-compatible both directions (no ABI bump).
Protocol (punktfunk-core):
- New CompositorPref (config.rs): Auto|Kwin|Wlroots|Mutter|Gamescope with
u8/name mappings. Appended as one optional byte to Hello (client preference)
and Welcome (host's resolved choice). Both decoders already tolerate trailing
bytes, so old↔new interop is preserved — ABI_VERSION stays 2. Round-trip +
back-compat (truncated-message) tests.
- C ABI: punktfunk_connect_ex(compositor) + PUNKTFUNK_COMPOSITOR_* constants;
punktfunk_connect delegates with AUTO, so the existing symbol is unchanged.
NativeClient::connect / worker_main thread the preference through.
Host:
- vdisplay::available() enumerates usable backends via cheap, side-effect-free
probes (KWin zkde global, gamescope binary+version, GNOME/Sway env), plus
Compositor id/label/as_pref/from_pref/all helpers.
- m3 handshake resolves the preference to a concrete backend during the
handshake (pick_compositor pure + resolved logging), reports it in Welcome,
and threads it into virtual_stream (replacing the unconditional detect()).
- mgmt GET /v1/compositors lists every backend with availability + the
auto-detected default (OpenAPI regenerated).
Client:
- punktfunk-client-rs --compositor NAME; logs the host's resolved choice from
the Welcome ("session offer … compositor=…").
Web console:
- Host page gains a Compositors card (availability + default badges) via the
codegen'd useListCompositors hook; en/de strings added.
Also fixes a pre-existing, env-dependent test-isolation bug:
mgmt::tests::paired_clients_list_and_unpair seeded the real
~/.config/punktfunk/paired.json (AppState::new loads it), so a real
GameStream-paired client leaked into body[0] on a dev box — now cleared first.
Live-validated against headless KWin: --compositor kwin honored, --compositor
mutter falls back to kwin (available=[kwin, gamescope]), resolved choice
round-trips to the client. Tests: +6 (wire/back-compat, resolution precedence,
endpoint); workspace green, clippy/fmt clean, C ABI harness PASS at abi_version=2,
web typecheck + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
75eb8fa0d6 |
feat(host): KDE-reliability phase 2 — pipeline retry, graceful capture teardown, refresh reconcile
Hardens the virtual-display → capture → encode bring-up against the transient failures that surfaced as black screens / wrong refresh on cold KDE sessions. - m3: build_pipeline_with_retry wraps the initial vd.create() + first-frame with bounded exponential backoff (4 attempts, 500ms→2s). is_permanent_build_error classifies config/version/missing-tool failures so they fail fast instead of burning the retry budget. Encoder + frame clock now pace to the *achieved* refresh reported in VirtualOutput::preferred_mode, not the requested rate. - capture/linux: PortalCapturer::Drop sends a pipewire channel quit and joins the thread, so a dropped/failed/retried capturer releases its PipeWire thread + EGL/ CUDA context promptly instead of leaking it to process exit. First-frame timeout now reports the node id and distinguishes "format never negotiated" from "negotiated but no buffers arrived" via a negotiated flag set in param_changed. - vdisplay/kwin: set_custom_refresh reads back the active mode from kscreen-doctor and returns the refresh KWin actually gave us (a rejected custom mode silently leaves the output at 60Hz); create() carries it into preferred_mode. - vdisplay/gamescope: find_gamescope_node requires the Video/Source object (the node.name=gamescope tag is on two objects; the other wedges the link); a version check warns on <3.16.22 (the PipeWire-1.6 capture-deadlock signature). Live-validated against headless KWin: 720p120 build with requested=120 achieved=120, zero-copy CUDA frames, and no per-session thread accumulation across back-to-back sessions. Tests: +3 unit (retry classifier, gamescope version parse); 49 host tests green, clippy/fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bfd64ce871 |
rename: lumen → punktfunk, everywhere
ci / rust (push) Has been cancelled
Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |