The client asks the kernel for a 32 MB SO_RCVBUF, but the kernel silently clamps
it to net.core.rmem_max — whose default is far too small. A too-small recv buffer
is the dominant client-side wall above ~1 Gbps. Measured live (Fedora host -> two
clients, real 2.5G LAN, GSO off): a client capped at 4 MB rmem_max dropped 31.6%
of a 2 Gbps stream at the receiver, while a 32 MB client delivered the same
2 Gbps at 0.0% loss. The host already shipped this tuning; the client packages
didn't (the RPM's %post even referenced the host-only file), so a client-only
install streamed lossy at high bitrate.
Add scripts/99-punktfunk-client-net.conf (rmem/wmem = 32 MB, distinct filename so
host+client can coexist) and ship+apply it from both the .deb (build-client-deb.sh)
and the RPM client subpackage (install, %files client, %post client).
For reference the full ladder (punktfunk speed-test): 0% loss to 1.5 Gbps on a
4 MB client; 31.6% at 2 Gbps on 4 MB vs 0% at 2 Gbps on 32 MB. iperf3 put the raw
link at ~2.35 Gbps TCP / ~2.4 Gbps UDP, so the stack now tracks the wire given a
big enough recv buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audio: a headless host has no speakers, and on a LAN with AirPlay devices PipeWire picks a random
HomePod as default — so desktop audio (which the host captures from the default sink's monitor)
went to a HomePod over AirPlay instead of to the client, and there was no "Punktfunk" output to
select. Ship a `punktfunk-sink.conf` (a `support.null-audio-sink` adapter — NOT the non-existent
module-null-sink, which makes pipewire refuse to start) with high priority.session so it's the
default; run-headless-kde.sh installs it and restarts pipewire once on first install. The host then
captures its monitor and streams it. (Disable AirPlay sinks out of band: `dnf remove
pipewire-config-raop`.)
Input: the host's libei portal D-Bus connection goes stale when the compositor session restarts the
portal under it, and the in-process reopen loop can't recover it (EIS setup keeps timing out) — only
a full restart does. Add PartOf=punktfunk-kde-session.service so the host restarts with the session.
Both verified live on the Fedora 44 KDE box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a headless KDE appliance, libei input injection silently failed: the EIS socket comes from the
xdg RemoteDesktop portal, which never came up, and even up it would pop an unanswerable "Allow
remote control?" dialog. Three fixes in run-headless-kde.sh, all idempotent + safe on the dev box:
- Reach graphical-session.target: xdg-desktop-portal is ordered behind it and its start job fails
without it, but a headless linger session never gets there and Fedora's target has
RefuseManualStart=yes — drop that in once, then start the target.
- Start the portal with `start` (the old `try-restart` is a no-op when inactive — the first-boot
case), so it actually comes up.
- Pre-seed the RemoteDesktop grant: vendor the `kde-authorized` permission-store GVariant DB and
copy it to ~/.local/share/flatpak/db/ (never clobbering an existing one), so the portal grants
RemoteDesktop without a dialog. Shipped by the RPM + .deb.
Diagnosed + fixed live on the Fedora 44 KDE box: libei devices RESUME and emit (MouseMove/keys).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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
a601022.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>