• v0.10.1
    ci / web (push) Successful in 52s
    ci / docs-site (push) Successful in 1m5s
    apple / swift (push) Successful in 4m33s
    ci / bench (push) Successful in 5m22s
    android-screenshots / screenshots (push) Successful in 3m30s
    ci / rust (push) Successful in 23m9s
    decky / build-publish (push) Successful in 1m23s
    release / apple (push) Successful in 24m48s
    android / android (push) Successful in 12m54s
    arch / build-publish (push) Successful in 13m16s
    windows / build (aarch64-pc-windows-msvc) (push) Successful in 55s
    web-screenshots / screenshots (push) Successful in 2m51s
    docker / deploy-docs (push) Successful in 9s
    docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 28s
    docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 23s
    docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
    docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 30s
    docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
    flatpak / build-publish (push) Successful in 7m4s
    windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m19s
    linux-client-screenshots / screenshots (push) Successful in 7m45s
    rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m47s
    windows-host / package (push) Failing after 7m43s
    rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m47s
    windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m51s
    deb / build-publish (push) Successful in 12m5s
    windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m2s
    apple / screenshots (push) Successful in 19m24s
    Stable

    enricobuehler released this 2026-07-13 22:05:33 +00:00 | 0 commits to main since this release

    Wire-compatible with 0.10.x — no protocol or ABI change (WIRE_VERSION stays 2, the native C ABI stays v6). This is an input release: touchscreen input modes come to the Linux and Windows clients, and the cross-client gamepad audit lands as correctness and robustness fixes across every client and both hosts. Every change is client-local or routes an existing wire bit — nothing touches the handshake, so any 0.10.x host/client pair keeps working with no re-pairing.

    It lands the cross-client + host gamepad audit (G1–G25) as ~20 targeted fixes spanning every client (Apple, Android, Linux, Windows) and both host input backends (Linux uinput/uhid, Windows XUSB/UMDF), plus one Steam Deck boot fix that gets the Deck pad back into Game Mode on image-based hosts — and brings the SDL presenter's touchscreen handling up to Android/Apple parity.

    Touchscreen input — three modes on Linux & Windows

    The SDL presenter (Linux / Steam Deck + Windows) had no finger handling, so SDL synthesized mouse events from touch — and under the stream's relative-mouse lock that walked the host cursor into the corner (the reported Steam Deck bug). Touch is now first-class, matching what the Android and Apple clients already do: a persisted Touch input setting (new picker in both the GTK and WinUI settings screens) selects how a touchscreen drives the host, with SDL's touch→mouse synthesis turned off.

    • Trackpad (default) — the screen acts as a relative trackpad with pointer ballistics and the shared gesture vocabulary: tap = left click, two-finger tap = right click, two-finger drag = scroll, tap-then-drag = held left-drag, three-finger tap = cycle the stats overlay.
    • Direct pointer — the cursor jumps to and follows your finger (absolute positioning).
    • Touch passthrough — every finger is delivered as a real host touchscreen contact.

    Direct touchscreens route through a new incremental gesture engine (a port of the Android TouchInput / Apple TouchMouse logic), while indirect trackpads keep driving the mouse; fingers map through the aspect-fit letterbox onto the content rect. Passthrough is opt-in (default stays Trackpad), matching the other clients. The gesture engine, letterbox mapping, and settings back-compat are unit-tested (28 tests).

    Host input — a transient failure no longer kills every controller

    All seven virtual-pad managers (Linux uinput/uhid + Windows XUSB/UMDF) carried an identical broken latch that was set on the first pad-creation error and never cleared — so a single transient hiccup (a startup race on /dev/uinput, a momentary EBUSY, the Windows companion driver not being ready yet) permanently disabled every controller for the rest of the session. That latch is now one shared, unit-tested PadGate with capped exponential backoff (1 s doubling to 30 s): after a failure, creation is blocked only until the backoff elapses, so a genuinely broken setup self-heals within one window (udev reload / driver install / next connect) with no host restart — and the manager stops re-attempting and re-logging on every one of the 60–240 input frames/sec.

    Host input — held state stays held

    • Linux — the uinput backend emitted only XOR-changed button edges while advancing its "previous" state unconditionally. Because the kernel write is best-effort, a single dropped EV_KEY edge left a button stuck pressed (or stuck released) until it next toggled. Buttons now re-assert their absolute state every frame, exactly like the axes — restating an unchanged key is free (the input core discards a no-op EV_KEY).
    • Steam Deck (host) — a held trackpad click was wiped on the very next button/stick frame and only flickered back on the next rich-plane event; it's now persisted across the frame rebuild like the touch/motion bits, and the two owners of the click bit (rich plane vs. the DualSense touchpad-click wire button) release independently.
    • Windows XUSB — the pad is now eager-created on device Arrival (with last_active refreshed), the change-detect fields are ordered with proper Release/Acquire so a concurrent update can't be missed, and the per-section security descriptor is freed instead of leaked.

    Host input — buttons that were being dropped

    • DualSense mute / capture — clients have been sending BTN_MISC1 (mic-mute / Steam quick-access), but the PlayStation-family virtual pads never mapped it, so the mute button was inert on every DualSense/DualShock 4 session. It now drives the DualSense mute bit.
    • Steam back grips on Windows — the Windows DualSense and DualShock 4 backends passed raw wire buttons straight through, silently dropping paddle buttons (BTN_PADDLE1..4) and ignoring PUNKTFUNK_STEAM_REMAP. They now fold the grips through the same remap policy the Linux backends already used (default still Drop; set the env to map them onto stick-clicks or shoulders).

    Apple clients

    • Hold guide and keep playing. While the PS/guide button was physically held, the first stick, trigger, or face-button move emitted a spurious guide-release — so you couldn't hold guide to keep the host's Steam overlay engaged while touching anything else. Guide is now preserved through the button diff and toggled solely by the Home handler.
    • Share / Create is its own button. The dedicated Share/Create/Capture element was mapped onto the same bit as View, so on an Xbox-Series pad Share was indistinguishable from View and never delivered the capture bit the host decodes. It now routes to BTN_MISC1, matching the Rust client. (On-glass verify on a real Xbox-Series pad still owed.)

    Android client

    Four fixes bringing the Android pad to SDL/Apple parity:

    • Rapid d-pad taps register. Android batches joystick motion, so a press-and-release inside one batch lived only in the event's historical samples and was missed; every historical HAT sample is now fed through the transition logic.
    • A malformed rumble lease can't kill the rumble thread. A ttl_ms == 0 lease with nonzero amplitude threw on the one-shot duration and took down the whole rumble poll thread; the duration is now floored at 1 ms.
    • No feedback-binding leak on disconnect. A controller unplugged mid-session leaked its open lights/rumble session; feedback binds are now evicted when a slot closes.
    • Plus the held exit chord fix (G24).

    Every client

    • Menu diagonal tie-break now resolves horizontally across all clients, so a perfectly diagonal stick push in the console/gamepad UI moves predictably instead of arbitrarily.
    • Stick/trigger axes truncate uniformly across clients, removing a one-LSB skew between platforms.
    • Legacy Steam Deck rumble is bounded when a stop-frame is lost, so a dropped "stop" can't leave a Deck motor buzzing on the older path.

    Steam Deck — the controller comes back to Game Mode

    On image-based (systemd-sysext) hosts such as Bazzite, the virtual Steam Deck pad was silently degrading to a non-promotable UHID device and vanishing from the host's Game Mode. A sysext image merges after systemd-modules-load and early udev have already run, so the shipped modules-load.d + udev rule were read too late and vhci_hcd never loaded — and the unprivileged host can't modprobe it itself. The sysext post-merge step now mirrors both files into real /etc (read at the normal early-boot time), reloads udev, loads vhci_hcd, and re-triggers the device for the live session, so the pad arrives as a real USB device that Steam Input will promote. (deb/arch/rpm were never affected — real /usr is present at early boot.)

    Windows — the dead gamepad fork is gone

    The Windows shell carried a 629-line near-verbatim fork of pf-client-core's SDL gamepad service, frozen at an old single-pad design and left mostly unreachable — it had also drifted from core (opening every device for metadata, force-enabling the Valve HIDAPI drivers, missing the steam_virtual skip so it could pin Steam Input's own virtual pad and kill gyro, and deriving the pin key from an opened handle). The shell now points straight at pf_client_core::gamepad::GamepadService, and the spawned session already runs the full service for all real forwarding.

    Internals

    • Gamepad BTN_* wire constants are now sourced from punktfunk_core with the bits pinned, and the Apple side asserts every wire button/axis constant against the generated C ABI header in CI, so any client-side drift fails the build.
    • The shared SwCreateCtx is hoisted into gamepad_raii, the DualSense HID-output feedback plane is de-duplicated across the Windows backends, and the two-motor vibratorIds ordering assumption on Android is documented.
    Downloads