340cbcfe220b68d5e0883eaa6ce0a81bf4b7efc5
215 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
340cbcfe22 |
fix(packaging): point the packaged systemd unit at /usr/bin/punktfunk-host
ci / web (push) Failing after 46s
apple / swift (push) Successful in 1m17s
ci / rust (push) Successful in 1m19s
ci / docs-site (push) Failing after 42s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m53s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 5m17s
scripts/punktfunk-host.service is dev-oriented — its ExecStart references the source tree (%h/punktfunk/target/release/punktfunk-host). When the deb/rpm ship it to /usr/lib/systemd/user, a fresh install with no hand-rolled unit would try to run a binary that isn't there. Rewrite the ExecStart to the installed /usr/bin/punktfunk-host during packaging (sed in build-deb.sh + the spec); the source unit stays as-is for from-source dev. Hosts with a custom ~/.config unit (which shadows the packaged one) are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4098b252bc |
fix(abi): exclude internal Apple recvmsg_x FFI from the C header
ci / web (push) Failing after 46s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Failing after 32s
ci / rust (push) Successful in 1m20s
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 4s
deb / build-publish (push) Successful in 3m16s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 4m43s
cbindgen swept transport/udp.rs's `recvmsg_x` foreign import and its `MsghdrX` #[repr(C)] struct into the generated C header — they're internal Apple-only FFI, not part of the public C ABI, and reference socklen_t/ssize_t/iovec which the C ABI harness doesn't include, so c_abi_harness_round_trips failed to compile. Add them to cbindgen.toml export.exclude and regenerate the header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f9b857aac2 |
feat(capture): true SHM path (PUNKTFUNK_FORCE_SHM) for race-free Mutter+NVIDIA
ci / web (push) Failing after 37s
apple / swift (push) Failing after 1m3s
ci / rust (push) Failing after 1m11s
ci / docs-site (push) Failing after 43s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 5m17s
Empirically, Mutter+NVIDIA dmabuf capture has NO working GPU sync — confirmed on worker-3: explicit sync fails buffer alloc (EINVAL, no cogl sync_fd), and the dmabuf carries no implicit fence (EXPORT_SYNC_FILE waited=false). So any dmabuf read — zero-copy import OR mmap — races Mutter's render and flashes the buffer's previous frame. The prior "CPU fallback" still listed DmaBuf in its buffer types, so Mutter kept handing dmabufs and it never fixed anything (got worse). PUNKTFUNK_FORCE_SHM=1 offers MemPtr+MemFd ONLY (no DmaBuf), forcing Mutter to glReadPixels-download into mappable memory — which orders against its render, so the frame is complete + current by construction (race-free). Costs the download (~3 ms) + zero-copy; correct at 1080p/4K60. KWin/gamescope are unaffected (they blit into the buffer, no read-before-render race) and keep zero-copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
2ebffe3457 |
perf(core): recvmsg_x batched receive on Apple (macOS client)
apple / swift (push) Failing after 1m2s
ci / rust (push) Failing after 1m11s
ci / web (push) Failing after 39s
ci / docs-site (push) Failing after 41s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 3m5s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 4m30s
macOS/iOS have no recvmmsg(2), so the Mac client did one recv() syscall per packet (non-allocating after the earlier fix, but still a syscall each — a single-core wall at line rate that Moonlight avoids). Add the Darwin recvmsg_x(2) batched-receive path (the recv counterpart of Linux recvmmsg): one syscall drains up to RECV_BATCH datagrams into the reused ring. struct msghdr_x + the extern aren't in the libc crate, so declared here (cfg target_vendor=apple). Opt-in via PUNKTFUNK_RECVMSG_X (it's FFI we can't exercise off-Apple) with auto-fallback to the tested scalar recv-loop on any unexpected error. Linux recvmmsg + the non-Apple scalar loop are unchanged; apple.yml compiles the path. Re GRO: Linux recv already batches via recvmmsg (32/syscall), so UDP GRO is only a marginal add there and needs a recv-path redesign to split coalesced buffers — deferred as low-ROI vs the Mac, which had no batching at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9c86f667ca |
perf(core): in-place AES-GCM seal + reused wire-buffer pool (host send)
ci / web (push) Failing after 39s
ci / docs-site (push) Failing after 33s
apple / swift (push) Successful in 1m16s
ci / rust (push) Successful in 1m20s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m3s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 4m35s
The host sealed every packet with ~3 heap allocations: aes-gcm's convenience encrypt() allocates the ciphertext Vec, seal_for_wire allocates the seq||ct||tag wire Vec, and seal_frame allocated a fresh Vec<Vec<u8>> per frame. At line rate (~250k–500k pkt/s for 2.5–5 Gbps) that's the single-core allocator wall. - SessionCrypto::seal_in_place uses AeadInPlace::encrypt_in_place_detached to encrypt into the caller's buffer and write the detached tag at the end — byte-identical to seal's ciphertext||tag, no allocation (unit-tested for byte equality + decrypt). - Session keeps a wire_pool the caller returns via reclaim_wires; seal_frame seals each packet in place into the reused buffers (clear() keeps capacity), so after warmup there's no per-packet ciphertext/wire allocation. paced_submit and submit_frame reclaim the pool after sending. End-to-end encrypted/lossless multi-frame tests stay green (validates the pool reuse doesn't corrupt across frames). Next: write packetize directly into a contiguous send buffer (kills the remaining shard allocs + GSO's coalescing copy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
448986f41c |
perf(core): UDP GSO send path (the multi-Gbps lever)
apple / swift (push) Successful in 1m16s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
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
ci / rust (push) Successful in 1m31s
deb / build-publish (push) Successful in 2m36s
ci / web (push) Failing after 36s
ci / docs-site (push) Failing after 32s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m42s
rpm / build-publish (push) Successful in 4m38s
docker / deploy-docs (push) Successful in 17s
sendmmsg already batches syscalls but still builds one sk_buff per datagram — the kernel-side wall above ~1 Gbps. UDP Generic Segmentation Offload hands the kernel one big buffer it splits into gso_size datagrams, building ~1 GSO skb per ≤64 segments. Research (LWN/Cloudflare/Tailscale) measures ~2.4x throughput at equal CPU and 17-44x fewer syscalls, and that sendmmsg batching alone is insufficient — you need true segmentation offload. Adds Transport::send_gso (default = send_batch) + a UdpTransport Linux override: coalesces a frame's equal-size wire packets (shards are zero-padded to a constant size, so a whole frame is one gso_size) into ≤64-segment sendmsg(UDP_SEGMENT) calls. seal/send routes through it. Opt-in via PUNKTFUNK_GSO (new unsafe hot-path code) with automatic fallback to sendmmsg on any GSO error (unsupported kernel/ path), latched per process. Loopback unit test validates the cmsg segmentation; full session over loopback streams clean (0% loss). Linux-only; loopback/non-Linux keep sendmmsg/scalar. Next levers: in-place AES-GCM seal (kill per-packet allocs), UDP GRO on recv, drop the sleep-pacing in favor of the kernel qdisc, jumbo MTU. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b1bbfdf0e |
feat(client-linux): VAAPI hardware decode — zero-copy dmabuf into GraphicsOffload
ci / docs-site (push) Failing after 45s
ci / web (push) Failing after 32s
apple / swift (push) Successful in 1m16s
ci / rust (push) Failing after 1m18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 7s
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 5s
docker / deploy-docs (push) Has been skipped
deb / build-publish (push) Failing after 1m38s
rpm / build-publish (push) Successful in 4m10s
Stage 1.5: on Intel/AMD clients libavcodec's VAAPI hwaccel decodes on the GPU; frames map to DRM-PRIME dmabufs (av_hwframe_map, zero copy) and reach GTK as GdkDmabufTexture (BT.709 limited CICP color state — GDK's dmabuf default is BT.601). Inside GtkGraphicsOffload that is the decoder-to-subsurface path, direct-scanout eligible when fullscreen. Fallback ladder, live-verified on the NVIDIA dev box: no VAAPI device -> software decode at session start (logged reason); a mid-session VAAPI error (e.g. broken nvidia-vaapi-driver) demotes to software and the host's IDR/RFI recovery resynchronizes; a rejected dmabuf import logs and the stream continues. PUNKTFUNK_DECODER=software|vaapi overrides; the first-frame log now names the active path. The hwaccel path is raw ffmpeg-sys FFI (ffmpeg-next wraps none of it): hw device ctx + get_format pinned to AV_PIX_FMT_VAAPI (NONE on mismatch so cpu-fallback never silently engages inside libavcodec), thread_count=1, LOW_DELAY. Surface lifetime rides DrmFrameGuard into the texture's release func — GDK runs it on both success and failure. Needs an Intel/AMD client box (Steam Deck/Bazzite) to live-verify the hardware path; the software path is unchanged and revalidated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b5c30dff4f |
perf(host): lift bitrate cap to 8G, raise MTU to 1452, FEC env knob
Groundwork for multi-Gbps (2.5G link here, 5G to the Mac Studio). The encoder is pixel-rate bound, not bitrate bound, so these unblock the transport: - MAX_BITRATE_KBPS 2G -> 8G, MAX_PROBE_KBPS 3G -> 10G (the cap was policy, not a hardware limit — NVENC emits multi-Gbps trivially with the 2-way split). - Welcome shard_payload 1200 -> 1452: fills a 1500 MTU, ~17% fewer packets for free (even size, FEC-safe; negotiated so the client follows). - PUNKTFUNK_FEC_PCT env overrides the 20% FEC default — a clean wired LAN can drop it (every recovery shard is wire bytes+packets); 0 disables FEC. Next: UDP GSO (the dominant lever — research shows ~2.4x throughput / ~40x fewer syscalls; sendmmsg batching alone is insufficient) + in-place AES-GCM seal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aac48408fd |
Merge remote-tracking branch 'origin/main'
ci / web (push) Failing after 44s
apple / swift (push) Successful in 1m16s
ci / rust (push) Failing after 1m17s
ci / docs-site (push) Failing after 44s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 1s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 29s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
deb / build-publish (push) Failing after 47s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 1m5s
|
||
|
|
4ff6f447a8 |
ci(packaging): punktfunk-client .deb + RPM subpackage
Hook the Linux client into the existing packaging CI:
- deb.yml builds both binaries and publishes punktfunk-host AND
punktfunk-client to the Gitea apt registry; new
packaging/debian/build-client-deb.sh mirrors the host script
(shlibdeps auto-Depends — GTK4/libadwaita/SDL3/FFmpeg/PipeWire
sonames; no NVIDIA filter, the client links no CUDA). Built and
inspected locally on Ubuntu 26.04.
- punktfunk.spec gains a "client" subpackage (binary + desktop entry +
udev rule); rpm.yml's publish loop picks it up unchanged.
- New shared assets: packaging/linux/io.unom.Punktfunk.desktop and
scripts/70-punktfunk-client.rules — DualSense hidraw uaccess (USB +
Bluetooth, steam-devices style) so SDL's HIDAPI driver gets
touchpad/motion/lightbar/triggers instead of degrading to evdev.
- Builder images learn the client link deps (rust-ci already had
them; fedora-rpm adds gtk4/libadwaita/SDL3-devel) with idempotent
install steps in deb.yml/rpm.yml since jobs run against the
previous push's image.
Workspace check CI (build/clippy/test) already covers the crate since
|
||
|
|
11fc3be726 |
fix(core): libc is a unix-wide dep — unbreak iOS/tvOS xcframework slices
ci / web (push) Failing after 37s
ci / docs-site (push) Failing after 36s
apple / swift (push) Successful in 1m17s
deb / build-publish (push) Failing after 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 1s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 1s
ci / rust (push) Failing after 1m22s
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 4s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 56s
|
||
|
|
67a32711b3 |
chore(apple): Xcode 27 project upgrade + hardened runtime
apple / swift (push) Failing after 27s
ci / web (push) Failing after 9s
ci / docs-site (push) Failing after 44s
ci / rust (push) Failing after 1m15s
deb / build-publish (push) Failing after 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 36s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 57s
Applied via Xcode's recommended-settings upgrade and distribution prep: - LastUpgradeCheck / scheme LastUpgradeVersion 2650 -> 2700. - DEVELOPMENT_TEAM (F4H37KF6WC) hoisted to the project-level build configs; the now-redundant per-target copies are dropped (all targets inherit it). - ENABLE_HARDENED_RUNTIME = YES on the macOS app target (required for Developer ID notarization). Signing stays Apple Development + Config/Punktfunk.entitlements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4be993df87 |
fix(apple/stage2): disable layer vsync wait to kill fullscreen stutter
apple / swift (push) Failing after 28s
ci / web (push) Failing after 47s
ci / rust (push) Failing after 1m19s
ci / docs-site (push) Failing after 33s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 12s
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
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 13s
deb / build-publish (push) Failing after 44s
The experimental stage-2 presenter (CAMetalLayer + display link) stuttered badly in fullscreen but ran fine windowed. render() runs on the display-link / MAIN thread and calls layer.nextDrawable(), which blocks that thread until a drawable frees. With the layer's own displaySyncEnabled left on (default), present also waits for the hardware vsync, so the block serializes the main thread to the display — windowed, the WindowServer's looser compositing hides it; fullscreen's tighter, more-direct path exposes it as judder. (Apple dev-forum guidance: displaySync off measurably reduces nextDrawable() blocking.) - displaySyncEnabled = false (macOS-only): the display link is already the per- vsync pacing source, so the layer's redundant vsync wait only adds the stall. - maximumDrawableCount = 3 (explicit): more in-flight headroom before nextDrawable() has to block on the main thread. Swift-only (no core/ABI change → no xcframework rebuild). Validated: swift build; swift test (39 passed, 0 failures). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b5ee9f47b |
perf(core): batched non-allocating recv on Apple targets (macOS client wall)
apple / swift (push) Failing after 28s
ci / rust (push) Failing after 1m18s
ci / web (push) Failing after 47s
ci / docs-site (push) Failing after 35s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 4s
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
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 16s
deb / build-publish (push) Failing after 43s
The batched `recvmmsg` recv path was Linux-only; macOS fell back to the trait default, which calls the scalar `recv` — a fresh `vec![0u8; 2049]` allocation (plus zeroing and a copy) PER PACKET on the single receive thread. At line rate that alloc/free churn, not the syscall, was the single-core wall: measured the real Mac client topping out ~315 Mbps and dropping the session at 800, while a Linux client (recvmmsg) held a clean 1 Gbps against the same host, and Moonlight (batched recv) does 900 on the same Mac. Add a `cfg(all(unix, not(linux)))` `recv_batch` that drains up to RECV_BATCH datagrams per call with `libc::recv(MSG_DONTWAIT)` straight into the caller's reused ring buffers — no per-packet allocation or copy. Still one syscall per datagram (a future `recvmsg_x` batch would cut that too), but it removes the dominant cost. Linux recvmmsg path and the Windows/loopback default unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c56b1b455a |
feat(punktfunk/1): request-IDR recovery for a wedged client decode
apple / swift (push) Successful in 1m17s
ci / rust (push) Failing after 31s
ci / web (push) Failing after 42s
ci / docs-site (push) Failing after 40s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 10s
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 6s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (push) Failing after 15s
deb / build-publish (push) Failing after 43s
Fixes the intermittent first-connect freeze. The host streams infinite GOP — one opening IDR, then P-frames only (recovery keyframes just on loss) — so when the client's decoder wedges on the cold first session (a lost/corrupt opening IDR, a bad early P-frame) the picture stays frozen until the far-off next keyframe. The client had no way to ask for one; now it does. Add a RequestKeyframe control message (client -> host, reliable control stream), mirroring Reconfigure: - core: quic.rs RequestKeyframe (type 0x03) + roundtrip test; client.rs CtrlRequest::Keyframe + NativeClient::request_keyframe; abi.rs punktfunk_connection_request_keyframe (header regenerated). - host: m3.rs decodes it in the control loop and signals the encode loop, which coalesces a burst and calls enc.request_keyframe() — wiring the existing NvencEncoder hook (force_kf -> next frame pict_type=I), the same recovery the GameStream path already had via force_idr. - apple: PunktfunkConnection.requestKeyframe(); StreamPump (stage-1) requests on layer.status==.failed; Stage2Pipeline (stage-2) on a sync submit failure and on the async decode-error callback via a thread-safe KeyframeRecovery. All throttled to <=1/250ms (the decode stays wedged for several frames until the IDR lands, so per-frame requests would flood the control stream). Self-healing: a lost recovery IDR is re-requested after the throttle; the host coalesces bursts into a single IDR. Validated: cargo fmt + clippy clean; core + host test suites green (incl. new request_keyframe_roundtrip); swift build + test (39 passed); xcframework rebuilt (all 5 slices), header regenerated with no unrelated drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71d6b64f81 |
fix(ci): POSIX shell in deb/rpm Version step (dash "Bad substitution")
ci / docs-site (push) Failing after 43s
ci / rust (push) Failing after 2m13s
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
ci / web (push) Failing after 47s
apple / swift (push) Successful in 1m17s
deb / build-publish (push) Successful in 2m48s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (push) Failing after 30s
docker / deploy-docs (push) Successful in 17s
deb.yml runs in the Ubuntu rust-ci image whose /bin/sh is dash, where the bash
substring `${GITHUB_SHA::8}` is a "Bad substitution" — the deb build failed at the
Version step every run. Compute the short SHA with `cut` instead. (rpm.yml ran fine
because the Fedora image's /bin/sh is bash, but fix it the same way for robustness.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0b1322d1c6 |
fix(packaging): ship the UDP socket-buffer sysctl in the .deb and .rpm
ci / web (push) Failing after 46s
apple / swift (push) Successful in 1m16s
ci / docs-site (push) Failing after 38s
ci / rust (push) Failing after 1m52s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
deb / build-publish (push) Failing after 2m6s
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 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m47s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Failing after 3m4s
The host requests a 32 MB SO_SNDBUF, but the kernel clamps it to net.core.wmem_max (~416 KB on a stock box) — so high-bitrate frames overflow the socket buffer and the host drops a large fraction of packets on send (measured 28.5% loss / 54k dropped at 1 Gbps to a clean LAN client on a fresh Bazzite box). scripts/99-punktfunk-net.conf fixes it (32 MB caps) but the packages never installed it. Ship it to /usr/lib/sysctl.d/ (auto-applied at boot by systemd-sysctl) and apply it in the deb/rpm postinst. This is the dominant cause of the sub-Gbps ceiling on an untuned host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
06346e5037 |
docs(rpm): use repo_gpgcheck for the unsigned Gitea RPMs
ci / web (push) Failing after 40s
ci / rust (push) Successful in 1m8s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Failing after 48s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
deb / build-publish (push) Failing after 2m21s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m25s
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 2m24s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 3m45s
Gitea GPG-signs the repo metadata but not the individual packages, while its auto-served bazzite.repo sets gpgcheck=1 — so `rpm-ostree install` fails with "could not be verified" on our unsigned RPMs. Document writing the repo explicitly with gpgcheck=0 + repo_gpgcheck=1 (verify the signed metadata, which carries each package checksum) instead of curling the served .repo. Note the TLS-only fallback and that per-package signing is future hardening. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
58cb416abb |
ci(rpm): publish punktfunk-host RPM to the Gitea registry (Bazzite)
ci / web (push) Failing after 44s
ci / rust (push) Successful in 1m7s
apple / swift (push) Successful in 1m16s
ci / docs-site (push) Failing after 38s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
deb / build-publish (push) Failing after 2m20s
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 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m21s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 3m57s
Mirrors the apt pipeline for Fedora Atomic / Bazzite. New `rpm` workflow builds the host RPM in a Fedora 43 builder image (ci/fedora-rpm.Dockerfile — matches Bazzite's libavcodec.so.61, with a self-contained 16-symbol libcuda link stub so no NVIDIA packages are needed in CI) and uploads to Gitea's public RPM registry (group "bazzite") on every main push (rolling 0.0.1-0.ciN.<sha>) and v* tag (clean X.Y.Z-1). Bazzite hosts then track it with `rpm-ostree upgrade`. - packaging/rpm/build-rpm.sh: git-archive tarball + rpmbuild (--nodeps, since the toolchain is rustup + dnf, not RPMs); copies to dist/, asserts no cuda/nvidia leak. - punktfunk.spec: overridable pf_version/pf_release for CI snapshots; exclude libcuda.so from auto-Requires (NVENC/EGL come from the driver, out of band) — same NVIDIA filter as the .deb; fix a bogus changelog weekday. - docker.yml builds+pushes the new fedora-rpm image; packaging README + rpm/README document the rpm-ostree install/update path (recommended option). Builder image seeded to the registry so rpm.yml's first run finds it. RPM build + clean-Requires verified locally in the image (libavcodec.so.61 / libavutil.so.59, no cuda). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e2257a6158 |
fix(apple): persist Keychain trust — sign macOS + data-protection keychain
ci / web (push) Failing after 34s
ci / docs-site (push) Failing after 40s
apple / swift (push) Successful in 1m17s
ci / rust (push) Successful in 1m8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
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
docker / deploy-docs (push) Successful in 19s
deb / build-publish (push) Failing after 2m19s
The client identity prompted for Keychain access on every launch/rebuild. Root cause: the macOS app target was ad-hoc signed (CODE_SIGN_IDENTITY = "-"), and the identity lived in the file keychain whose "Always Allow" ACL is bound to the app's exact code signature (cdhash for ad-hoc). Every rebuild changed the binary -> changed the cdhash -> the ACL no longer matched -> re-prompt. - Sign the macOS target with Apple Development (team already set) instead of ad-hoc, so the designated requirement is identity-based and stable across rebuilds. - Move the identity to the data-protection keychain (kSecUseDataProtectionKeychain) gated by a team-scoped keychain-access-group entitlement — access is granted by the app's entitlement, not a per-binary ACL, so it's prompt-free and survives rebuilds. Add Config/Punktfunk.entitlements and wire CODE_SIGN_ENTITLEMENTS into all six app configs (macOS/iOS/tvOS). - Unsigned / ad-hoc builds (e.g. `swift run`) lack the entitlement (errSecMissingEntitlement) — fall back to the legacy file keychain so they still work (with the old prompt), no hard failure. macOS re-mints the identity on first run (the old file-keychain copy isn't in the data-protection keychain) -> one re-pair, which is acceptable. iOS keeps its identity (the explicit access group equals the prior default). Validated: swift build; swift test (39 passed, 0 failures); xcodebuild -showBuildSettings confirms Apple Development + Config/Punktfunk.entitlements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfed90bff2 |
ci(deb): publish punktfunk-host .deb to the Gitea apt registry
ci / web (push) Failing after 49s
ci / rust (push) Successful in 1m6s
apple / swift (push) Successful in 1m18s
ci / docs-site (push) Failing after 40s
docker / build-push (., web/Dockerfile, punktfunk-web) (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 6s
docker / deploy-docs (push) Successful in 20s
deb / build-publish (push) Failing after 2m17s
Wires up the half-built Debian packaging: build-deb.sh existed but nothing invoked or published it. Adds a `deb` workflow that builds the release host in the Ubuntu 26.04 rust-ci image, packages it (dpkg-shlibdeps-resolved Depends, NVIDIA driver filtered out), and uploads to Gitea's public Debian registry on every main push (rolling 0.0.1~ciN.<sha>) and v* tag (clean X.Y.Z). Ubuntu hosts then track it with `apt update && apt upgrade`. Also: box-setup docs (packaging/debian/README.md), a pointer from the packaging README, ignore dist/, and drop backticks from the package Description (the unquoted control heredoc ran them as a command substitution). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
184f94e867 |
Merge remote-tracking branch 'origin/main'
ci / web (push) Failing after 36s
ci / rust (push) Successful in 1m8s
ci / docs-site (push) Failing after 34s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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 5s
apple / swift (push) Successful in 1m17s
docker / deploy-docs (push) Successful in 16s
|
||
|
|
a95984bb4f |
feat(client-linux): feature parity with the Swift client
Everything the macOS app does that stage 1 lacked, before any new feature work (user directive): - Input capture is now a deliberate, reversible STATE (Moonlight- style): engaged on stream start and click-into-video (the engaging click is suppressed), released by Ctrl+Alt+Shift+Q (toggles) or focus loss; held keys/buttons are flushed host-side on release; cursor hiding + shortcut inhibition follow the state; HUD hint when released. Per-session window handlers disconnect with the page. - Gamepads: app-lifetime SDL service (GamepadManager parity) — pad list + "Forwarded controller" pin in Settings (auto = most recent), "Automatic" pad TYPE resolves from the physical pad at connect; DualSense touchpad contacts + ~250 Hz motion samples on the 0xCC plane (Swift GamepadWire scale constants); feedback grows adaptive- trigger replay and player LEDs via raw DS5 effects packets (the wire's 11-byte blocks drop into SDL_SendGamepadEffect verbatim); held pad state zeroed on pad switch/detach. sdl3 "hidapi" feature. - Microphone uplink: PipeWire capture -> Opus 20 ms -> 0xCB datagrams (validated live: host received 711 mic packets), Settings toggle. - Speed test per saved host (Swift's "Test Network Speed…"): 2 s probe burst, goodput/loss + recommended ~70 % bitrate, one-tap apply. - Settings: host compositor preference (sent in the Hello), native- display resolution/refresh resolved from the window's monitor at connect (new default), bitrate ceiling to 3 Gbit/s. - Hosts page: saved/trusted hosts section for direct pinned reconnect (mDNS not required), rebuilt on every page return. Deliberately not ported: audio device pickers (PipeWire routing owns this on Linux), resize-to-request_mode (not wired in Swift either), pointer-lock relative mouse (stage-2 presenter, needs raw Wayland). DualSense fidelity needs a physical pad to live-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dea749186d |
fix(quic/apple): QUIC keep-alive + reconnect input re-engage
ci / rust (push) Failing after 36s
ci / web (push) Failing after 51s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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
ci / docs-site (push) Failing after 40s
apple / swift (push) Successful in 1m16s
docker / deploy-docs (push) Successful in 17s
Three native-client bugs isolated against a stock Moonlight client (which stays connected / keeps input working under the same actions): - Connection drops mid-stream: the quinn endpoints (host + client) ran with default transport config, so keep_alive_interval was OFF. Any quiet stretch (no input, audio muted/stalled, a capture hiccup, a mode change) let the idle timer expire and quinn closed the session -> next_au=Closed -> "Session ended". Moonlight's ENet sends keepalive pings; we sent nothing. Add a shared TransportConfig (keep-alive 4s under an explicit 20s idle timeout) to both endpoint::server_from_der and endpoint::client_pinned_with_identity. - Reconnect input dead (macOS): the session-start auto-capture one-shot was consumed even when engageCapture(fromClick:false) was refused (window not key yet at the instant of reconnect), with no retry -> capture stayed off and input never forwarded. Clear the one-shot only on a successful engage, and retry on NSWindow.didBecomeKey. Stays scoped to session start, so it does not resurrect the rejected auto-grab-on-activation behavior. - Reconnect input dead (iOS): wasCapturedOnResign leaked stale state across sessions and the foreground-restore could fire before this session's InputCapture was wired (setForwarding no-ops on nil). Reset it per session in start() and guard the didBecomeActive restore on inputCapture != nil. Validated: cargo build -p punktfunk-core --features quic; swift build; swift test (39 passed, 0 failures); xcframework rebuilt (all 5 slices), no ABI/header drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8a6224fd8 |
fix(encode): bound per-frame size with a tight VBV buffer
ci / rust (push) Failing after 36s
ci / web (push) Failing after 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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
docker / deploy-docs (push) Successful in 17s
ci / docs-site (push) Failing after 39s
apple / swift (push) Successful in 1m16s
NVENC ran CBR (bit_rate == max_bit_rate, rc=cbr) but never set rc_buffer_size, so it used a loose default VBV. A high-motion P-frame was then allowed to spike to many times the average frame size; the extra packets overflow the depth-2 send queue (newest frame dropped) and the kernel UDP buffer (WouldBlock drops), which the client sees as framedrops/jitter — and on the infinite-GOP GameStream path as old/stale frames flashing until the next RFI. Set a tight ~1-frame VBV (rc_buffer_size = bitrate/fps) so the encoder holds frame size roughly constant and absorbs motion as a momentary QP/quality dip instead — the Sunshine/Moonlight low-latency model. Tunable via PUNKTFUNK_VBV_FRAMES (default 1.0); larger trades burst tolerance for motion quality. Fixes both the punktfunk/1 and GameStream paths (shared encoder). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f088c6f56 |
fix(client-linux): absolute mouse was dropped — pack the surface size in flags
ci / web (push) Failing after 45s
ci / rust (push) Successful in 1m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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
apple / swift (push) Successful in 1m18s
ci / docs-site (push) Failing after 42s
docker / deploy-docs (push) Successful in 17s
The MouseMoveAbs wire contract packs the client coordinate-space size as (width << 16) | height in `flags` (same as touch); injectors normalize against it and drop the event when it is zero. The GTK client sent flags=0, so KWin's libei path refused every motion (`emitted=false`) — found via the first real user test from home-worker-3. - ui_stream: send_abs() packs the negotiated mode into flags for motion + click-position events. - core input.rs: document the contract on MouseMoveAbs itself (it was only implied by TouchDown's doc). - client-rs --input-test: add a MouseMoveAbs sweep so the absolute path stays covered — Moonlight and the Mac client only send relative motion, which is why this gap survived every prior live test. Validated live against serve --native: kind=MouseMoveAbs emitted=true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f09def4138 |
ci: GTK4/libadwaita/SDL3 dev packages for punktfunk-client-linux
ci / web (push) Failing after 38s
apple / swift (push) Successful in 1m14s
ci / docs-site (push) Failing after 42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / deploy-docs (push) Successful in 17s
ci / rust (push) Successful in 5m38s
Baked into the rust-ci image, plus an idempotent apt step in the rust job itself — ci.yml runs against the previous push's image (docker.yml bootstrap note), so the image change alone would leave this push and the next one red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
96a35ca84c |
feat(client-linux): native GTK4 client — stage 1, first light at 1080p60
ci / rust (push) Failing after 29s
ci / web (push) Failing after 35s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
ci / docs-site (push) Failing after 38s
apple / swift (push) Successful in 1m15s
docker / deploy-docs (push) Successful in 17s
New crate crates/punktfunk-client-linux (binary punktfunk-client), the native Linux client on the Option A architecture (2026-06-12 research): - GTK4/libadwaita shell linking punktfunk-core directly (no C ABI): mDNS host list, TOFU fingerprint prompt, SPAKE2 PIN pairing dialog, preferences (mode/bitrate/gamepad/shortcut capture), stats overlay, --connect host[:port] for scripting. - Video: FFmpeg software HEVC decode (LOW_DELAY, slice threads) -> RGBA -> GdkMemoryTexture inside GtkGraphicsOffload (the dmabuf subsurface path lights up when VAAPI lands; black-background keeps fullscreen scanout-eligible). - Audio: Opus -> PipeWire playback stream, the host virtual-mic's adaptive jitter ring inverted. - Input: keyboard as the exact inverse of the host VK table (evdev keycodes, layout-independent; unit-tested), absolute mouse through the Contain-fit transform, WHEEL_DELTA(120) scroll, compositor shortcut inhibition while streaming, Ctrl+Alt+Shift+Q release chord, F11 fullscreen. SDL3 gamepad capture (single pad-0 model) + rumble and DualSense lightbar feedback on the same thread. - Session pump owns video+audio pulls; the gamepad thread owns rumble+hidout — possible because NativeClient's plane receivers are now mutexed, making it Sync (Arc-shared, compiler-verified per-plane contract instead of the ABI's manual assertion). - Linux-gated deps + a stub main keep cargo build --workspace green on macOS. Validated live against serve --native on this box: 1920x1080@60, locked 60 fps, capture->decoded p50 ~6.4 ms (software decode, debug build). Teardown keys off AdwNavigationPage::hidden — NavigationView push fires a transient unmap/map cycle that must not end the session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
99b4de32ee |
feat(pairing): delegated approval (§8b-1) — approve an unpaired device from the console
ci / web (push) Failing after 40s
ci / rust (push) Successful in 1m6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
apple / swift (push) Successful in 1m20s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / docs-site (push) Failing after 46s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
docker / deploy-docs (push) Successful in 16s
An identified-but-unpaired device that knocks on a pairing-required host is now
held as a pending request the operator approves from the web console — pairing it
with no PIN fetched out of band — instead of a flat reject.
- core: Hello gains an optional trailing device name (len u8 || UTF-8, ≤64,
same trailing-back-compat pattern as compositor/gamepad/bitrate). client-rs
--name sends it; the connector sends None (fingerprint-derived label).
- native_pairing: in-memory pending queue (note_pending dedups by fingerprint,
evicts the least-recently-active past a 32 cap, 10-min TTL); approve_pending
pins the fingerprint, deny drops it. Names are sanitized (strip control/ANSI/
bidi — untrusted wire input); add()/remove() roll back in-memory on a persist
failure; pairing clears any stale pending knock.
- m3: the require_pairing gate records the knock (sanitized label) before
rejecting; anonymous (certless) clients record nothing.
- mgmt: GET /native/pending, POST /native/pending/{id}/approve (optional {name})
and /deny; OpenAPI + tests; docs/api/openapi.json regenerated.
- web: a "Waiting for approval" section on the Pairing page (live-poll, Approve/
Deny, error-surfaced via QueryState); en+de strings.
- Also completes an in-progress NativeClient Sync refactor (receivers behind
per-plane mutexes) that was left half-applied in the tree.
Adversarially reviewed (4 lenses + 3-vote verify); the confirmed findings are
fixed here. Validated live on the GNOME box: knock (with a wire name, and a
malicious ANSI/bidi name that got neutralized) → pending → approve → the same
identity streams real video. Full workspace tests + clippy + fmt green; web tsc
clean. Roadmap §8b-1 marked done; §8b-2 (peer-push approval) is the client
follow-up. See docs-site pairing page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9758751a4d |
ci(release): make the throwaway keychain the default keychain
ci / web (push) Failing after 44s
ci / rust (push) Successful in 54s
apple / swift (push) Successful in 1m19s
ci / docs-site (push) Failing after 42s
docker / deploy-docs (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
release / apple (push) Failing after 2m34s
exportArchive's signing lookup consults the default keychain; search list membership alone leaves the (valid) identity invisible to it. Restored to login.keychain in cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v0.1.0 |
||
|
|
343cb544d9 |
ci(release): manual Developer ID export — cloud signing has no fallback
ci / web (push) Failing after 34s
ci / rust (push) Successful in 55s
ci / docs-site (push) Failing after 34s
apple / swift (push) Successful in 1m18s
docker / deploy-docs (push) Failing after 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
release / apple (push) Failing after 2m35s
With -allowProvisioningUpdates, exportArchive prefers cloud-managed
Developer ID signing; the App-Manager API key can't ("Cloud signing
permission error") and the valid local identity is never tried.
signingStyle=manual + explicit signingCertificate, cloud flags off
this step (archive keeps them for profile fetch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6b49279c32 |
ci(release): stage Apple intermediate CAs in the signing keychain
ci / web (push) Failing after 34s
ci / rust (push) Successful in 55s
ci / docs-site (push) Failing after 32s
apple / swift (push) Successful in 1m19s
docker / deploy-docs (push) Successful in 12s
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 3s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 3s
release / apple (push) Failing after 2m43s
Fresh boxes lack the Developer ID / WWDR intermediates; without the issuing chain the imported identity is invalid and xcodebuild says "No signing certificate Developer ID Application found". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0f8896570 |
fix(web): mobile navigation — add a bottom tab bar + top bar
ci / web (push) Failing after 49s
ci / rust (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
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 4s
apple / swift (push) Successful in 1m19s
ci / docs-site (push) Failing after 37s
docker / deploy-docs (push) Successful in 16s
The app shell's only navigation was the desktop sidebar (`hidden … sm:flex`), so on phones (< sm) it was hidden with no replacement — you couldn't navigate at all. Add a responsive mobile layout shown only below `sm`: a top bar (brand + language switcher) and a fixed bottom tab bar with the five nav items (icon + label). The desktop sidebar is unchanged. Page content gets bottom padding so the fixed bar doesn't cover it, and the bar respects the iOS `safe-area-inset-bottom`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
02bcf41803 |
ci(release): TestFlight upload best-effort until the ASC app record exists
ci / web (push) Failing after 41s
ci / rust (push) Successful in 56s
ci / docs-site (push) Failing after 35s
apple / swift (push) Successful in 1m19s
docker / build-push (., web/Dockerfile, punktfunk-web) (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 3s
docker / deploy-docs (push) Successful in 16s
release / apple (push) Failing after 2m44s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0733eae361 |
Merge remote-tracking branch 'origin/main'
ci / web (push) Failing after 37s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / docs-site (push) Failing after 36s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m18s
|
||
|
|
57e7f9fe25 |
feat(release): production Apple builds — notarized macOS dmg + iOS TestFlight
release.yml (v* tags / dispatch, macos-arm64 runner): universal mac +
iOS xcframework -> xcodebuild archive -> Developer ID export ->
notarytool + staple -> dmg on the Gitea release; iOS archive uploads
to TestFlight (app-store-connect/upload). Per-run throwaway keychain;
ASC API key authenticates notarization, upload, and automatic-signing
profile fetch. macOS App Store lane deferred (needs App Sandbox);
tvOS deferred (tier-3 Rust targets).
All app targets now share bundle ID io.unom.punktfunk — ONE App Store
listing with universal purchase (decided pre-submission; effectively
unchangeable after). ITSAppUsesNonExemptEncryption=false declared
(standard-algorithm AES-GCM, exempt).
build-xcframework.sh resolves Apple toolchains itself: cargo's HOST
artifacts (proc-macros, build scripts) are loaded by the running OS,
and a newer-than-OS beta Xcode ld emits LINKEDIT layouts dyld rejects
("mis-aligned LINKEDIT string pool" -> misleading E0463) — so prefer
a non-beta Xcode for everything, fall back to CLT for mac-only slices
(env untouched: an explicit DEVELOPER_DIR=<CLT> trips xcrun's license
check), refuse iOS/tvOS without a real Xcode (CLT has no iOS SDK).
The runner plist no longer injects DEVELOPER_DIR for the same reason.
punktfunk_Logo.icon: dropped the Xcode-27-beta-only Icon Composer
features (refractivity, specular-location) — 26.5's actool crashes on
them, and store builds must use release Xcode. Visual delta is the
refraction/specular nuance only; re-author when 27 ships.
Validated on home-mac-mini-1 with Xcode 26.5: mac+iOS xcframework
slices, unified bundle IDs, signing-free app build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9291568ce0 |
refactor(apple): decompose ContentView (735 -> 272 lines)
ci / web (push) Failing after 35s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / docs-site (push) Failing after 40s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m20s
Split the monolithic ContentView into focused view files — a pure structural refactor with no behavior change (verified: builds macOS/iOS/tvOS, the test suite is green, and a fidelity review against the original found no discrepancies): - ContentView (272): the coordinator — owns the session model / host store / discovery, switches home<->session, holds the connect logic (it reads @AppStorage) + the dev hooks, and the stream builder (whose stable identity across awaiting-trust->streaming must NOT move — it stays here). - HomeView (251): the hosts grid + navigation + toolbar + sheets + "On this network" discovery section + empty state. - HostCards (158): HostCardView + DiscoveredCardView, sharing a CardMetrics struct (dedupes the platform-tuned sizing the two cards had copy-pasted). - TrustCardView (80): the TOFU prompt + fingerprint formatting. - StreamHUDView (67): the streaming overlay HUD. State flows idiomatically: @StateObject (ContentView) -> @ObservedObject in subviews, @State -> @Binding; the connect logic is passed as closures. Sheet placement is preserved — the pairing/speed-test sheets stay on the outer body so they survive the trust->home transition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e8135ccec |
refactor(apple): code-quality pass — audit fixes + centralized defaults keys
A 6-agent adversarial audit of the client (11 confirmed of 39 findings, the rest
filtered) drove these:
- fix: SessionAudio ring buffer — guard a write larger than the ring (would push
readIdx past writeIdx and corrupt the buffer; never happens, but guard not corrupt).
- fix: CADisplayLink retain cycle (stage-2 presenter) — a weak-target DisplayLinkProxy
so the view can deallocate (the link retains its target); stage-2 teardown added to
both StreamView/StreamViewController deinits as a safety net.
- fix: GamepadFeedback deinit { flag.stop() } — the drain thread holds the connection
strongly and self weakly, so an abrupt teardown without stop() would leak it.
- refactor: centralize the 12 UserDefaults/@AppStorage key literals (scattered across
8 files) into one DefaultsKey enum — a typo silently splits a setting's reader from
its writer.
- docs: RumbleRenderer @unchecked Sendable invariant; the HID digit-row table; the
stage-2 layer compositing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c8099c0125 |
fix(vdisplay/mutter): stop screencast before monitor reconfig — fixes >60Hz teardown crash
ci / web (push) Failing after 45s
ci / rust (push) Successful in 57s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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
ci / docs-site (push) Failing after 29s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m19s
The high-refresh teardown SIGSEGV was caused by ApplyMonitorsConfig disabling the still-actively-captured high-refresh virtual output. Reorder teardown: Stop the screencast FIRST (Mutter removes the virtual + auto-reverts the temporary config), then re-assert the physical layout once the virtual is gone. Never reconfigure a live virtual CRTC. With this, PUNKTFUNK_MUTTER_VIRTUAL_REFRESH=1 is stable: validated at 5120x1440@240 on Mutter 50 + NVIDIA — virtual output Meta-0@240, real 240fps, gnome-shell survives back-to-back sessions + teardowns, physical (HDMI-1) restored each time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
91d5874e94 |
docs: user-facing docs revamp — structured product docs + per-platform setup
ci / web (push) Failing after 47s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
ci / docs-site (push) Failing after 37s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m19s
Replace the dev/agent-log pages with a proper user-facing doc set: - Getting Started: Introduction (rewritten), How It Works, Quick Start. - Host Setup: Requirements, then clean per-platform guides — Ubuntu GNOME, Ubuntu KDE, Fedora KDE (new), Bazzite (rewritten) — plus Running as a Service (desktop / headless GNOME / headless KDE). - Connecting: Clients overview, Moonlight, Pairing & Trust. - Configuration: host.env reference, Host CLI, Troubleshooting. - The dev/design notes (architecture, roadmap, the deferred design specs, CI) move to a clearly-separated "Project & Internals" nav section. Removes the superseded box-specific pages (gnome-box, headless-box, linux-setup, overview). status.md (the internal progress tracker, with box IPs) is kept as a file but dropped from the public nav. Site builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
015f2ee47b |
fix(vdisplay/mutter): gate >60Hz virtual mode behind an env flag (teardown SIGSEGV)
ci / web (push) Failing after 34s
ci / rust (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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
ci / docs-site (push) Failing after 41s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m20s
Pinning the virtual output to a high client refresh via RecordVirtual "modes" works mid-stream, but a high-refresh virtual CRTC SIGSEGVs gnome-shell on session TEARDOWN (observed at 5120x1440@240) — taking down the whole GNOME session, so subsequent connects fail with RemoteDesktop ServiceUnknown. Gate it behind PUNKTFUNK_MUTTER_VIRTUAL_REFRESH, default OFF — Mutter then derives the virtual monitor's refresh from the PipeWire framerate (60Hz, stable). The >60Hz path stays in-tree for investigation; re-enable once the teardown crash is understood. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ecb4e6e1d5 |
Merge remote-tracking branch 'origin/main'
ci / rust (push) Successful in 55s
ci / web (push) Failing after 48s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
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 17s
ci / docs-site (push) Failing after 36s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m19s
|
||
|
|
f6a7f3c12d |
feat(vdisplay/mutter): pin the virtual output to the client's refresh (>60 Hz)
RecordVirtual without a "modes" property makes Mutter derive the virtual monitor's refresh from the PipeWire stream framerate and default to 60 Hz — so a 240 Hz client mode rendered at 60 (the encoder just padded to 240 with duplicate frames). Pass an explicit "modes" entry (size + refresh-rate + is-preferred) so Mutter creates the virtual monitor at the client's exact WxH@Hz. Mutter >= 47; older Mutter ignores the unknown key (60 Hz fallback, no regression). Confirmed first via raw D-Bus on the box, then validated end-to-end: the virtual output Meta-0 reports 1920x1080@240.00 and the host encodes 480 *immediate* (real, not paced) frames per 2 s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fa407700e0 |
docs(roadmap): gamescope multi-user research (deferred); render->capture parked
Document the gamescope multi-user (independent-desktops) research and defer it: the current shared host-lifetime input/audio/mic vs the per-session plumbing it would need — per-instance EIS sockets + a per-session injector + per-session null-sink audio routing + per-session mic — and why it's not worth it now (a large multi-file refactor for the niche multi-user-on-one-box case, while the common multi-device scenario is already covered by the shared-desktop multi-view concurrency that landed). New gamescope-multiuser.md + roadmap section 14 (concurrent sessions: multi-view done, multi-user deferred). Also park render->capture in section 12: pipewire-rs 0.9.2 exposes no buffer-meta / raw-pointer / stream-timing API, so reading SPA_META_Header.pts would need raw spa_sys FFI into the working capture hot path — disproportionate for the smallest glass-to-glass term; g2g is effectively complete as capture->present (the stage-2 presenter measures it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b10714b62 |
feat(apple): stage-2 presenter — explicit decode + Metal present + glass-to-glass
ci / web (push) Failing after 38s
ci / rust (push) Successful in 53s
docker / build-push (., web/Dockerfile, punktfunk-web) (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 16s
ci / docs-site (push) Failing after 39s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m17s
Opt-in (Settings -> Presenter; `punktfunk.presenter`, default stage-1). Stage-1's AVSampleBufferDisplayLayer decodes AND presents internally with no per-frame callback, so neither decode nor present can be stamped or hand-paced. Stage-2 takes explicit control: - VideoDecoder: VTDecompressionSession, async output callback stamps decode-completion, session rebuilt on every IDR / format change. Unit-tested (testVideoDecoderAsyncCallbackDeliversPixels). - MetalVideoPresenter: CAMetalLayer + CVMetalTextureCache + a runtime-compiled BT.709 limited-range NV12->RGB shader, present at the next vsync. The CVMetalTextures + pixel buffer are held until the GPU completes. - Stage2Pipeline: pump thread -> decoder -> newest-ready 1-slot ring; the hosting view's display link drains it once per vsync and stamps capture->present (the display-link target time projected into CLOCK_REALTIME). - LatencyMeter gains record(ptsNs:atNs:offsetNs:); the HUD shows a capture->present (glass-to-glass, modulo host render->capture) line, skew-corrected via clockOffsetNs. Measured live ~11 ms p50 vs ~2.2 ms capture->client. - StreamView / StreamViewIOS host the CAMetalLayer as a sublayer + a CADisplayLink (NSView.displayLink on macOS) when stage-2; input capture + HUD unchanged. The session-active gates switch from `pump != nil` to `connection != nil` so capture engages without a StreamPump. Validated: builds macOS/iOS/tvOS; the decode half is unit-tested; the Metal present is live-validated on glass (correct image + the capture->present number). Colorspace is BT.709 SDR for now; 10-bit/HDR + a pacing policy are later. Plan: docs-site/content/docs/apple-stage2-presenter.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
848738ed00 |
docs(site): status log — CI + automatic docs deployment landed
ci / web (push) Failing after 35s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / docs-site (push) Failing after 39s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 1m18s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2226031577 |
fix(ci): deploy target is unom-1, not home-main-2
ci / web (push) Failing after 36s
ci / rust (push) Successful in 54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
ci / docs-site (push) Failing after 37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m18s
website/cms deploy to the unom-1 DMZ VM (192.168.50.50) — the website README's home-main-2 mention is stale. Caddy upstream fixed in unom/reverse-proxy 6ae79b8, firewall port in unom/infra 9670aa8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2ed755f0c3 |
fix(vdisplay/mutter): make the virtual output the SOLE display, not primary + secondary
ci / web (push) Failing after 38s
ci / rust (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / docs-site (push) Failing after 43s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m13s
Keeping the physical monitor enabled as a secondary let the cursor, windows, and keyboard focus land on it — relative pointer motion wandered off the streamed surface, so on the client the cursor "disappeared" and clicks/keys went nowhere visible. Omit the physical outputs from ApplyMonitorsConfig so Mutter disables them for the session; everything is confined to the streamed virtual output. Restored on teardown. Validated on-box: mid-session DisplayConfig shows only the virtual output (Meta-0) as the sole primary; the physical (HDMI-1) is restored after the session ends. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |