The pacman-repo setup step used a bash heredoc (`<<'EOF'`), which fish — the
default shell on CachyOS — cannot parse ("expected a string, but found a
redirection"). Replace it with a cross-shell `printf | sudo tee -a` form in both
the Arch guide and packaging/arch/README.md; `$repo`/`$arch` stay literal for
pacman and the output is byte-identical to the old heredoc.
Firewall: stock Arch ships none (ports already open), but CachyOS enables
firewalld by default and an Arch package must never touch the running firewall.
Ship firewalld service definitions the host package installs to
/usr/lib/firewalld/services/ (punktfunk-gamestream, punktfunk-native), not
auto-enabled; the install scriptlet prints the enable command only when
firewall-cmd is present. Document it in the Arch guide (new section) and README.
The mgmt API (loopback) and web console ports are deliberately not opened.
Also fix the "GTK4 couch/Deck client" mislabel — it's the native
GTK4/libadwaita Linux client (desktop/laptop/Deck are targets; the
controller-optimized launcher is one view, not its identity) — across the Arch
PKGBUILD/README, Arch guide, and the Debian README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every other distro has a full Host Setup page; Arch only had table rows. Add
docs/arch.md (signed pacman binary repo: key import + repo + install, GPU
prereqs, service/linger, web console, client, PKGBUILD appendix), slot it into
the nav after fedora-kde, and point the install/client tables at it. Update the
client-install rows from 'from the PKGBUILD' to the binary repo now that it exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layering is a last resort per the Bazzite docs (slows every OS update, can
block upgrades until removed); a sysext never enters an rpm-ostree
transaction, survives OS updates, and installs/updates with no reboot —
the mechanism Fedora Atomic ships via fedora-sysexts.
- build-sysext.sh wraps the built host+web RPMs into punktfunk-<V-R>-x86-64.raw:
/etc payload relocated to /usr/share/punktfunk/etc (a sysext carries only
/usr), the punktfunk-sysext helper embedded, ID=fedora + VERSION_ID pinned
(merges on Bazzite via ID_LIKE; REFUSED after a major rebase instead of
running soname-broken binaries — both behaviors validated live on Bazzite 43).
SELinux labels are baked in as squashfs pseudo-xattrs from matchpathcon:
unlabeled files run fine for user units but system daemons are DENIED
(udev couldn't read the gamepad rule under enforcing) — validated on-glass.
Refuses duplicate input package names (a stale noarch punktfunk-web next to
the x86_64 one built a chimera image with the dead node launcher once).
- punktfunk-sysext.sh: install/update/status/remove against per-Fedora-major
feeds (…/generic/punktfunk-sysext/f43[-canary]), SHA-256-verified, applies
the udev/sysctl scriptlet work + /etc copies, prints the layering-migration
hint. Live-validated on the .41 Bazzite box incl. service restart + web console.
- publish-sysext-feed.sh + rpm.yml: build + publish the image per matrix leg
(fedver 43/44), canary feeds pruned to 6, stable release assets attached.
- update-punktfunk.sh warns when the sysext shadows a layered install.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tested on .173: a WDA_EXCLUDEFROMCAPTURE window (affinity readback 0x11,
confirmed active) is pixel-identically visible in the punktfunk/1 stream
across no-flag / flag-set / flag-cleared phases — the flag makes no
difference to a present-tap capture. Replace the "untested, treat as
expected" note in the IDD-push residual list with the measured result,
and correct the framing: WDA visibility matches what a person at the
screen sees (it exceeds an ordinary capture tool, not the physical
viewer).
Add the matching public-facing paragraph to the security page covering
both asymmetries — WDA windows appear (same as a physical viewer), DRM
video is blanked (less than a physical viewer) — tied back to the page's
"a client sees what someone at the machine sees" model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every release workflow hardcoded a canary base version (0.5.0 in
Apple/Android/rpm/flatpak/deb, 0.3 in windows-msix/windows-host/decky) that
had to be hand-bumped on each stable release and wasn't. With stable at
v0.6.0, every canary was a version *behind* stable — e.g. the Apple canary
showed up on TestFlight as 0.5.0 while 0.6.0 was already published.
Add scripts/ci/pf-version.{sh,ps1} (bash + pwsh twin) as the single source of
truth: stable = the vX.Y.Z tag; canary = latest stable tag with minor+1,
patch 0 (v0.6.0 -> 0.7.0), so canary is always exactly one minor ahead of the
newest release with zero maintenance. Falls back to the workspace Cargo.toml
version when no tag is fetchable. All workflows now eval/call it and format
their own channel suffix off $PF_BASE; only the canary branch changed, stable
branches and per-channel suffixes are untouched. channels.md drops the old
manual "bump the canary base" release step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consumes the 0xCF host-timing plane (99cb8dd) on all four GUI clients: each
keeps a bounded pending ring of receipt samples keyed by pts, matches the
host's per-AU capture→sent reports against it, and the HUD equation becomes
= host 3.1 + network 6.7 + decode 2.1 + display 2.3
falling back to the combined `= host+network …` term whenever no timing
matched the window (old host / datagram loss) — same total, one split
fewer, never a misleading zero. Apple additionally gains the split as the
only equation line under the stage-1 fallback presenter (receipt is
presenter-independent), a `nextHostTiming` wrapper with its own plane lock,
and a unit-tested `HostNetworkSplitter`; Android extends the JNI stats
array 16→18 doubles (0–15 unchanged); Windows/Linux thread the split
through `Stats` into the HUD and the headless/debug logs.
Docs updated: design/stats-unification.md Phase 2 → implemented (wire
format, fallback semantics), and the docs-site matrix's Sunshine "Host
processing latency" row is now a direct match (ours includes the paced
send; avg vs p50).
Verified here: linux client clippy -D warnings green on the live tree,
windows stub check + hand-verified diff, android cargo-ndk arm64 check
green, apple loopback test extended (needs the rebuilt xcframework + swift
test on the mac). On-glass: pending on all platforms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One stat model everywhere (design/stats-unification.md): four measurement
points (capture/received/decoded/displayed), three stages that tile the
interval exactly, and a HUD that shows the addition explicitly —
end-to-end 14.2 ms p50 · 19.8 p95 · capture→on-glass
= host+network 9.8 + decode 2.1 + display 2.3
replacing each client's ad-hoc mix of overlapping absolutes (the Apple HUD's
three arrow lines that looked sequential but weren't), mean-vs-median decode
times (Windows/Linux), missing same-host-clock flags (Windows/Linux), and
three different names for the same capture→received measurement (probe's
"reassembled", Apple/Android's "client", Windows/Linux's post-decode "lat").
Per client: Apple threads receivedNs through the VT decode via the frame
refcon bit pattern so the decode stage exists at all (stage-1 fallback
honestly degrades to a capture→received headline); Windows carries
FrameTimes through the existing frame channel to the render thread and adds
e2e p50/p95 post-Present; Linux stamps received at AU pop and rides
decoded_ns on DecodedFrame to the paintable-set site; Android pairs receipt
stamps with MediaCodec output buffers via the codec's pts round-trip (JNI
stats array 14→16 doubles, indexes 0-13 unchanged). fps now uniformly counts
received AUs; lost/(received+lost) per window, hidden at zero.
docs-site gains "Understanding the Stats Overlay": what each line means, why
the equation only approximately sums (percentiles), and a line-by-line
Moonlight/Sunshine matrix — including that Moonlight has no end-to-end
number and its "network latency" is an ENet control RTT, so punktfunk's
headline must not be compared against any single Moonlight line.
Verified here: linux client + probe + core check/clippy/fmt green, android
native cargo-ndk arm64 check green. Pending: Windows CI + on-glass, swift
test on the mac, on-device Android.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The "Punktfunk" shortcut is no longer hidden: it now ships committed
artwork (grid/wide/hero/logo/icon, generated by scripts/gen-steam-art.py
— a pure-stdlib SDF renderer drawing the lens mark + a monoline
"punktfunk" wordmark) applied via SetCustomArtworkForApp /
SetShortcutIcon. Existing installs are unhidden and re-arted once per
ART_VERSION; relaunching the library entry streams to the last host.
- Updates cover the flatpak CLIENT too: check_update compares the
user-scope installed commit against its remote, applyUpdate runs
`flatpak update --user` first (awaited) and the plugin reinstall —
which reloads the panel — last; docs spell out the sudo-less --user
update ("sudo flatpak update" silently skips per-user installs).
- Fullscreen page: DialogButton stretches to 100% width in the gamepad
UI, so the Stream/Pair/Refresh/… actions filled whole rows — sized to
content + right-aligned now; the header drops its Update button (About
tab + QAM banner keep the flow) and the back button gets a real 40px
hit target.
- Settings: the disable-Steam-Input note also shows for Automatic — on a
Deck that now forwards the built-in controller as a Steam Deck pad
(paddles/trackpads/gyro), which needs Steam Input off for the shortcut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five verified findings from the IDD-push/pf-vdisplay deep audit:
- Keyed-mutex acquire (BOTH endpoints): AcquireSync returns WAIT_TIMEOUT
(0x102) / WAIT_ABANDONED (0x80) as SUCCESS-severity HRESULTs, which the
windows-rs Result wrapper erases — a busy slot read as "acquired", so
driver and host could race the same ring texture (torn frames) and the
designed busy-skip backpressure was dead code. Both sides now classify
the raw vtable HRESULT; WAIT_ABANDONED counts as acquired (ownership
transfers — refusing it would wedge the slot forever).
- Host SDR hot path leaked one ID3D11VideoProcessorInputView per converted
frame: the D3D11_VIDEO_PROCESSOR_STREAM ManuallyDrop field suppressed the
release after VideoProcessorBlt. Released by hand now, success or not.
- Driver leaked IddCx's per-acquire surface reference (from_raw_borrowed on
a TRANSFERRED reference — the MS sample Attach/Reset's it): the swap-chain
surface set survived swap-chain destruction, the likely true root cause of
the ~50 MB-per-reconnect VRAM loss that device pooling only mitigated.
Now adopted via from_raw (publisher or not) and dropped pre-Finished.
- PUNKTFUNK_IDD_PUSH removed: capture is unconditionally IDD-push, but the
vdisplay manager still gated the lingering-monitor preempt (and render
pin) on the knob, whose default was OFF — dev/CLI runs reused a lingering
monitor whose IddCx swap-chain is dead (black reconnect). The preempt and
the render-GPU pin are now unconditional; host.env comments no longer
promise the removed DDA/WGC fallback.
- Driver D3D device: dropped D3D11_CREATE_DEVICE_SINGLETHREADED (unsound
since DEVICE_POOL shares one device across processors) and the pooled
immediate context is now SetMultithreadProtected — two concurrent
monitors' workers otherwise race an unlocked context (UB in the UMD).
No wire-contract change (pf-driver-proto untouched); the driver fixes take
effect on the next pf-vdisplay redeploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Firewall (the service.rs core landed in 796d381): scope the web-console rule
(TCP 47992) to Domain+Private by default with a `--allow-public-network` opt-in
that deletes-then-re-adds the rule, and add the installer "Allow connections on
Public networks" task (unchecked) forwarding the flag to `service install` and
`web setup`. Default is now trusted-networks-only; Public is explicit.
Vulnerability disclosure: SECURITY.md (report to security@punktfunk.com, scope,
SLAs, safe harbor), a Gitea issue-template contact link, a README security line,
and a Reporting section on the docs Security page.
Docs: the Security page now documents the Private/Domain firewall default (and
how to fix a misclassified-Public network / opt in); removed internal design-doc
and CLAUDE.md links from the user-facing docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two disk-write fixes:
- pf-xusb/pf-dualsense no longer write C:\Users\Public\pf*-driver.log
unconditionally — the file log is now opt-in (debug builds, or the
PFXUSB_DEBUG_LOG / PFDS_DEBUG_LOG system env var), mirroring the audit-§4.4
fix pf-vdisplay already got: a release driver never writes the world-writable
Public file (info-leak/DoS surface), and the per-report OUTPUT/SET_STATE hex
dumps stop being a sustained per-rumble disk-write path during gameplay.
OutputDebugStringA stays unconditional; the host's driver-silence WARN and
the gamepad-driver-health failure-mode table now say the log is opt-in.
- service.log/host.log get one-generation rotation: at each (re)open a file
over 10 MB is renamed to .old, so a crash-restart loop or a RUST_LOG=debug
left in host.env can't grow the append-forever logs without bound. Rotation
runs only before an open (never under a live appender — host.log's handle
lacks FILE_SHARE_DELETE, so a racing rename harmlessly fails).
Windows CI compile/clippy pending (drivers workspace + host are not
Linux-cross-checkable); rides along with the next pad-driver redeploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OS floor is now enforced at install time (MinVersion=10.0.22621 with an
explanatory [Messages] override): pf-vdisplay is built against IddCx 1.10, and
on Windows 10 (incl. LTSC) / Win11 21H2 the device fails start with Code 10
STATUS_DEVICE_POWER_FAILURE (field-reported). Docs (site requirements/install/
windows-host pages + README) state the floor; new docs-site Security page.
Installer also gains the trayicon task (punktfunk-tray.exe file + HKLM Run key,
post-install launch as the signed-in user, upgrade taskkill + uninstall
--quit/taskkill choreography before file deletion), and the wizard/cleanup
text/port sweeps move off the stale :3000 web-console references to :47992
(cleanups sweep both for upgrades from old installs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port 3000 collides with half the dev-server ecosystem; 47992 sits next
to the mgmt API (47990) in the punktfunk port family. Updates the run
scripts, systemd/scheduled-task units, Dockerfile, Windows firewall
rule + installer, packaging, and every doc that referenced :3000.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gen12+/Arc iHD exposes ONLY EncSliceLP, so the default open fails with
'no usable encoding entrypoint'. Try full-feature first (AMD unchanged,
validated on the 780M), retry low_power=1, cache the mode per codec;
PUNKTFUNK_VAAPI_LOW_POWER pins it. Probes inherit the ladder. Docs note
the Intel HuC firmware requirement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)
**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
`video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
(drift-guarded against the wire values). Header regenerated.
**Host:** passes `hello.preferred_codec` into `resolve_codec`.
**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.
**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).
Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild configuration.md into a complete PUNKTFUNK_* reference (verified
against config.rs, the host.env templates, and the env read sites): core,
gamescope/session-following, compositor, video quality, gamepads, audio,
Windows host, auth/paths, perf tuning, diagnostics, and client-side knobs.
Rework bazzite.md: it now documents both Steam Gaming Mode (gamescope) and
the KDE Plasma desktop with auto-detect/session-following, attach vs managed,
and the Desktop screencast + kde-desktop-setup.sh input grant — previously it
only described the managed gamescope model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web console
- Pairing/Library/Stats refactored into self-contained subsections that each own
their own queries + mutations; a shared slot-based layout (view.tsx) is filled by
the live page (containers) and Storybook (pure cards + fixtures) so the layout can't
drift.
- All paired devices in one list on Pairing with a protocol column (punktfunk/1 +
Moonlight), routing each unpair to the right endpoint; the redundant Clients page is
removed.
- Library: overview grid split from the add/edit form into separate files.
- Login screen links out to the docs.
Docs
- "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows)
plus a new "Forgot your Password?" troubleshooting page, linked from the login screen.
- Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the
Bun entry, launchers, systemd units, and packaging.
Tooling
- Biome now respects .gitignore (stops linting generated code), config migrated to
2.5.1; all lint issues fixed cleanly.
Also includes this branch's in-progress host, Apple client, packaging, and CI changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the outdated Windows Host page:
- Capture is IDD direct-push only — drop the stale Windows.Graphics.Capture +
Desktop Duplication claim and the (removed) monitor-capture fallback; the
pf-vdisplay driver is now required.
- Install link points at the Gitea release (where the signed installer is
attached) instead of the package registry.
- Brand prose as "Punktfunk" (executables/paths/protocol/URLs/service names
stay as-is).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plugin self-update (no Decky store): CI publishes a per-channel manifest.json
({version, immutable per-version artifact, sha256}) beside the zip and bakes
update.json {channel, manifest} into the plugin. main.py `check_update` reads the
installed version from package.json (the value Decky reports — not plugin.json),
fetches the channel manifest, and the frontend shows an "Update to vX" button that
drives Decky Loader's own install RPC (root downloads + SHA-256-verifies + hot-reloads).
CI now stamps a plain-numeric semver (0.3.<run> canary / X.Y.Z stable) into
package.json — a -ciN suffix would mis-order under compare-versions.
Linux client: `--fullscreen` (plus SteamDeck/gamescope env fallback) enters GTK
fullscreen on stream start so Gaming-Mode chrome is hidden; native-mode resolution
falls back to the display's first monitor when the window isn't mapped yet (was
dropping to the 1080p floor — wrong on the Deck's 1280×800); add a confirmed
"Remove saved host" action (KnownHosts::remove_by_fp).
Docs: new docs/steam-deck.md (Decky install/pair/stream/self-update/troubleshooting),
wired into meta.json nav, and cross-linked from clients/install-client/channels. This
is the page docs.punktfunk.unom.io/docs/steam-deck — the website's download link
pointed at it before it existed; committing it makes that link resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the Windows host docs page for first-time setup, on par with the
other host guides: remove the standout "Status:" banner, restructure into
Requirements / Install (web console + pairing + configure) / How it works /
Notes & limits.
Bring the content up to date with the shipping host:
- encode is all-vendor (NVENC/AMF/QSV + software fallback), not NVIDIA-only
- virtual display is punktfunk's own pf-vdisplay IDD (SudoVDA removed)
- gamepads need no prerequisite — UMDF drivers bundled; ViGEmBus is gone
- add HDR10 + Vulkan-game HDR layer coverage
Fix the same stale claims where other pages cross-reference the Windows host
(requirements, running-as-a-service, install, roadmap, status).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Android app is in Google Play Internal Testing, so the public Play Store URL
doesn't resolve for non-testers. Lead the Android install instructions with a
"request a tester invite on Discord" CTA (the Play listing unlocks once a Google
account is added to the test track), and surface the Discord + r/Punktfunk
community links in the README, the docs intro, and the docs-site nav.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the "Linux-first" framing across the README and docs site in favor of
first-class Linux AND Windows hosts, and surface the Windows IDD-push
virtual-display path as a distinct differentiator (punktfunk's own indirect
display driver the host pushes frames into — a real virtual display, no physical
monitor or dummy plug, even on the secure desktop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discovery: replace the flaky per-OEM NsdManager with the same mdns-sd browse
the Linux/Windows clients use, in the Rust core over JNI and polled by Kotlin
(discovery.rs + nativeDiscovery{Start,Poll,Stop}); Kotlin keeps only the Wi-Fi
MulticastLock + permission UX. IPv4-only (the core can't dial a bare/scoped v6
literal); daemon + fold-thread cleanup on every failure path; field
sanitization so a rogue advert can't corrupt the picker snapshot. Discovery
now starts regardless of NEARBY_WIFI_DEVICES (raw multicast only needs the
MulticastLock) — a denial no longer kills it forever. ParseTxtTest replaced by
ParseRecordTest.
Hosts: hide already-saved hosts from the "Discovered" section (match by
fingerprint, else address:port — mirrors the Apple client); add an optional
Name field to the Add-host sheet and a Rename action on saved cards.
Input: touch -> absolute mouse "direct pointing" like the Apple client — the
host cursor follows the finger (new nativeSendPointerAbs -> MouseMoveAbs). Tap
= left click, two-finger tap = right click, two-finger drag = scroll,
tap-then-drag = left-drag, three-finger tap = HUD toggle.
Settings: revert the dropdowns to the stock ExposedDropdownMenuBox look (a
controller-focus UI will come separately); even out the Add-host field gaps.
Docs updated (CLAUDE.md, client READMEs, docs-site status).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows installer ballooned to 154 MB and installed forever because the node-server
bundle externalized the WHOLE @unom/ui dependency tree (payload, lexical, date-fns,
prismjs…) to .output/server/node_modules — 47,567 files / 730 MB copied into Program
Files. Set Nitro `noExternals: true` so every dependency is bundled + tree-shaken into the
server output: .output drops to ~75 files / 10 MB, and the bare external imports
(srvx, seroval…) bun couldn't resolve at runtime are gone — so the console runs on bun
(no node, no node_modules), which is the issue we previously worked around with node.
Windows installer now ships bun.exe + the ~75-file .output (was node.exe + a node_modules
forest) and runs `bun .output\server\index.mjs`:
- windows-host.yml: fetch a pinned portable bun (build tool AND shipped runtime); drop the
node fetch + the .output/server install; smoke-boot under the bundled bun.
- pack-host-installer.ps1 / punktfunk-host.iss: -NodeExe -> -BunExe; stage {app}\bun\bun.exe.
- web-run.cmd / build-web.ps1: run/restart on bun; docs updated.
Net win everywhere: the Linux .deb shrinks (node still runs the self-contained output), and
the docker web image — which already ran `bun run .output/server/index.mjs` with only
.output copied — is fixed (the externals had no node_modules to resolve at runtime).
Validated locally: noExternals build = 75 files / 10 MB; node AND bun both serve /login
(200) + static assets (200) + gate /api (401).
(A true single binary via `bun build --compile` is blocked for now: Nitro serves public
assets from an import.meta-relative path `--compile` doesn't embed (/$bunfs/public); the
75-file payload is the clean result.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows host installer shipped only the host exe + SudoVDA driver + FFmpeg, so a
fresh install had no web management console — required for basically every user (status,
paired devices, the PIN pairing flow). The console was only ever set up by hand on the
dev box (build-web.ps1 + a hand-made PunktfunkWeb task whose web-run.cmd wasn't even
committed). Bundle it into the same installer, mirroring the proven Linux punktfunk-web
deploy.
- windows-host.yml builds the Nitro node-server console (bun, deb.yml's shape) + fetches
a pinned portable Node, smoke-boots it under node (/login == 200) to gate the build, and
hands web/.output + node.exe to the pack script.
- pack-host-installer.ps1 gains -WebDir/-NodeExe and stages the .output tree, node, and
the two new scripts into the non-WOW64-redirected build area.
- punktfunk-host.iss lays the payload into {app}\web\.output + {app}\node\node.exe, adds
a wizard page for the console login password pre-filled with a crypto-random default
(shown on the finish page; kept on upgrade), and runs web-setup.ps1.
- web-setup.ps1 writes the ACL'd %ProgramData%\punktfunk\web-password (Administrators +
SYSTEM), registers the PunktfunkWeb scheduled task (boot, SYSTEM, restart-on-failure ->
web-run.cmd -> node on :3000), opens inbound TCP 3000, and starts it. web-run.cmd
sources the host's mgmt-token + the password and runs the bundled node.
- The console proxies the host's loopback mgmt API with the host's own
%ProgramData%\punktfunk\mgmt-token (no host-code change). Uninstall removes the task +
firewall rule.
Validated locally: bun build -> node-server bundle, node boot serves /login (200) and
gates /api (401). The Windows-only bits (ISCC compile, scheduled task, password page,
firewall) validate on the Windows runner CI + on-glass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Near-term 7.1 channel bed; moonshot object-based spatial audio via
Wine/Proton (where dynamic objects are currently discarded) with
client-side head-tracked spatialization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A push to main publishes canary builds to canary channels (fast iteration,
unchanged); a single vX.Y.Z tag releases every platform at one version to the
stable channels and attaches all artifacts (.deb/.rpm/.msix/.apk/.aab/.dmg +
flatpak/decky/host-installer) to one Gitea Release. Collapses the
host-v*/win-v*/host-win-v* tag namespaces into v* — the channel split makes the
version-shadow bug structurally impossible (canary and stable are separate repos,
never a shared version line).
- scripts/ci/gitea-release.{sh,ps1}: one idempotent release helper
(create-or-fetch + delete-before-upload), replacing 3 copy-pasted inline blocks
and fixing their latent 409-on-reupload bug; prerelease flag auto-derived from
the tag (an -rc tag won't shadow "Latest")
- channels: apt canary/stable distributions; rpm *-canary/base groups; flatpak
canary/stable OSTree branches + a 2nd .Canary.flatpakref; generic-registry
canary/ vs latest/ aliases; Play internal/alpha; Apple TestFlight vs notarized DMG
- android versionName threaded through gradle (versionCode stays run_number);
Apple canary = TestFlight-only (no DMG/tvOS); canary base bumped to 0.3.0
- docs: new docs-site channels.md (subscribe table + cut-a-release runbook +
box migration), refreshed ci.md workflow table + packaging READMEs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the management console (web/) off shadcn/ui to the shared @unom/ui
design system the marketing site + docs are built on, on the punktfunk
violet brand over dark chrome:
- Add @unom/ui/@unom/style/motion/radix-ui/zod + Geist; web/.npmrc maps the
@unom scope (packages are public-read, so CI needs no npm auth).
- styles.css: one dark-violet palette (#141019/#1c1530, brand #6c5bf3 ->
#a79ff8) exposed under BOTH the shadcn token names the routes use and
@unom/ui's contract, so routes + components both resolve; pulls in
@unom/ui's material gloss + easings.
- components/ui/* now back onto @unom/ui (AnimatedButton/InputText/Label/
AnimatedCard); brand-mark/wordmark/logo replace the generic Radio icon in
the shell + login.
- MaterialProvider (specular gloss) at the root. No UI sounds, like the site.
docs-site: new /api route renders the host management REST API as an
interactive Scalar reference (reads public/openapi.json, a snapshot of
docs/api/openapi.json), branded violet and linked from the top nav, the
docs sidebar, the landing page, and host-cli.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rename steam-deck-host.md → steamos-host.md (nav + install table updated).
- Lead with the rationale: SteamOS host support targets the upcoming Steam
Machine; the Steam Deck is the SteamOS device validated against today.
- Soften the WiFi note: ~250 Mbps was our testing on one device/network,
not a universal ceiling — other SteamOS hardware/drivers/bands may do more.
- Generalize Deck-specific language to SteamOS devices throughout.
- Document --no-gamestream (secure native-only) + GameStream-compat caveat.
- decky README: drop stale `serve --native` (now just `serve`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follows the security audit (#5/#9): the GameStream-compat plane carries inherent on-path weaknesses
that can't be fixed on the wire without breaking stock Moonlight — its pairing runs over plain HTTP
(#9, MITM-able during the pairing window) and its legacy control encryption can reuse GCM nonces (#5,
a passive eavesdropper can recover/forge input). The native punktfunk/1 plane (SPAKE2 PIN pairing +
per-direction AEAD nonces) has neither. So flip the default to secure-by-default:
- `serve` → native punktfunk/1 plane + management API ONLY (no GameStream surface).
- `serve --gamestream` → ALSO the GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet
control, _nvstream mDNS). Opt-in, logged with a trusted-LAN caveat. `--moonlight` is an alias.
- The native plane is now ALWAYS on in `serve` (`--native` is a kept-for-compat no-op); the unified
GameStream+native host is `serve --gamestream`.
`gamestream::serve` gates the GameStream spawns (nvhttp/rtsp/control/mdns) on the flag; the native
plane + mgmt + native-pairing handle always run.
To avoid silently regressing validated Moonlight deployments, the explicit deployment configs PRESERVE
Moonlight via `--gamestream` (each documents dropping it for a secure native-only host): the Linux
systemd unit, the Steam Deck installer, and the Windows service default (DEFAULT_HOST_CMD). The bare
`serve` default (new/manual use) is secure.
Docs swept to match (host-cli, moonlight, quickstart, install, packaging READMEs, CLAUDE.md, README,
…): Moonlight setup now instructs `--gamestream`; native/console refs use bare `serve`. OpenAPI
regenerated (a stale "run `serve --native`" string). fmt + clippy clean; 94 host tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SteamOS is immutable read-only Arch, and the Deck is AMD (VAAPI) — so none of the
checked-in packaging (arch/sysext is NVENC-first + client-oriented, deb/rpm are
soname-mismatched) actually installs a working host on a Steam Deck. The proven path
(distrobox-built native binary + systemd-run units) was 100% manual. Make it one command.
- scripts/steamdeck/install.sh — idempotent installer: ensure the pf2 Debian-trixie
distrobox + toolchain → build host (+web console) → write config (generated web login
password) → raise UDP buffers to 32 MB + udev + input group (sudo, skipped gracefully
if unavailable) → install + start punktfunk-host / punktfunk-web systemd USER services
with linger. Flags: --open (accept unpaired clients), --no-web, --src=DIR. Builds
on-device so a rebuild always matches the running SteamOS (no prebuilt-binary fragility
across OS updates); VAAPI on the Deck's AMD GPU.
- scripts/steamdeck/update.sh — rebuild from current source + restart (config/pairings persist).
- scripts/steamdeck/README.md — deep reference (why on-device, what's installed, gotchas).
- docs-site: new "Steam Deck (Host)" guide + sidebar entry; install.md splits Arch from the
Steam Deck host path; packaging/arch/README points Deck-host users here and corrects the
stale "NVENC-only" note (VAAPI host encode landed).
Live-validated on the Deck: installer runs clean, both services come up, host listens
(QUIC :9777 + mgmt :47990), web serves (302→login); on a client connect it takes over the
Game-Mode gamescope session at the client's mode, captures via PipeWire, and VAAPI-encodes
(hevc_vaapi) — full pipeline confirmed in the host journal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HDR (10-bit BT.2020 PQ) works end-to-end with the Windows host — it captures
an HDR desktop (WGC FP16 / Desktop-Duplication FP16 for the secure desktop)
and encodes HEVC Main10 to HDR-capable clients (Windows, Android). Only the
Linux host is blocked upstream (no 10-bit compositor capture). Corrected the
roadmap (grid + shipped/blocked), Windows Host page, status, and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- install (host): add a Windows (NVIDIA) section with signed-installer and
certificate-trust steps; note the .cer is the same across releases.
- install-client: clarify the Windows MSIX certificate is the same every
release (trust once, updates need nothing).
- Move "Project & Internals" out of the public docs site: relocate
implementation-plan, apple-stage2-presenter, gamescope-multiuser,
dualsense-haptics, ci, and gamestream-host-plan to docs/; drop them from
the nav. Move windows-host into Host Setup.
- Rewrite roadmap as a lean public page with an at-a-glance grid and
current statuses (Windows host shipped/beta, Apple incl. tvOS shipped,
Android shipped, concurrent sessions + delegated pairing done).
- Fix status.md link to the now-internal implementation plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh the README and documentation for public visitors:
- README: public-facing rewrite with accurate status for all four native
clients (macOS, Linux, Windows, Android) and the Windows host.
- docs site: fix stale client status (Android is a full client, not a
scaffold; Windows client is stage-1 complete + signed MSIX), add the
missing Android client section, correct "which client" guidance.
- Windows host: corrected from "deferred/scoped" to implemented & shipping
(NVIDIA-only, x64-only) across windows-host, roadmap, status,
requirements, running-as-a-service, and the README.
- Remove internal infrastructure from public docs (box names, private IPs,
SSH/token commands, deploy topology); rewrite status.md as a public
project-status page; sanitize ci.md and implementation-plan.md.
- Update clients/android and clients/apple READMEs to current state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSIX (the client's format) can't install the host's LocalSystem secure-desktop
service or the SudoVDA kernel driver, so the host ships as a signed Inno Setup
setup.exe that runs elevated and delegates to the existing idempotent
`punktfunk-host service install`.
- packaging/windows/punktfunk-host.iss: lay exe into Program Files, optional
SudoVDA driver task, run service install/start; [Code] stops+waits the service
before file copy on upgrade; uninstall runs service uninstall.
- pack-host-installer.ps1: cert (reuses MSIX_CERT_PFX_B64 / self-signed CN=unom),
sign inner exe + setup.exe, fetch/stage SudoVDA, run ISCC, export public .cer.
- fetch-sudovda.ps1 / install-sudovda.ps1: pinned SudoVDA + nefcon download, cert
import, gated device-node create (no phantom dup), pnputil install (warn-not-abort).
- nvenc/: synthesize nvencodeapi.lib via llvm-dlltool from a 2-export .def so
--features nvenc links with no GPU/SDK at build time.
- .gitea/workflows/windows-host.yml: build (nvenc) -> clippy -> ISCC -> sign ->
publish setup.exe + .cer to the generic registry pkg punktfunk-host-windows.
Tag host-win-v* -> X.Y.Z (+ latest/ alias); main push -> rolling 0.2.<run>.
- setup-windows-runner.ps1: provision Inno Setup; docs: installer instructions.
SudoVDA/nefcon release URLs+SHA-256s in fetch-sudovda.ps1 are placeholders
(baseline v0.2.1) — fetch warns + prints the computed hash until pinned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apple is TestFlight-only (no App Store) — link the join URL; drop the App Store
placeholder. Add the live Google Play listing for io.unom.punktfunk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-device install steps in one place: Linux (Flatpak via flatpak.unom.io +
native apt/rpm/Arch), Steam Deck, Windows (signed MSIX from the registry),
macOS (notarized DMG from releases), and iOS/Android (store/beta links). Adds
it to the Connecting nav and cross-links clients.md, whose Linux/Flatpak bullet
now points at the hosted flatpak.unom.io repo instead of the bundle README.
Mobile store/TestFlight URLs are placeholders pending the public listings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bodies of work in one commit (the rename moved files the fixes also touched).
Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
clients/probe (crate punktfunk-probe), client-linux->clients/linux,
client-windows->clients/windows, punktfunk-android->clients/android/native
(crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
kept only in docs/implementation-plan.md. docs/m2-plan.md->
docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.
Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
tracked it for the client keyframe-recovery loop; it was never reachable from
the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
frames_dropped and request a keyframe when it climbs -- the same loss-driven
recovery Linux/Windows already had. Under infinite GOP the decoder silently
conceals reference-missing frames, so the decode-error trigger rarely fires.
Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
interruption / server reset) and drop the permanent `broken` latch on a
transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.
Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The winit-commit docs claimed "Reactor rejected, no SwapChainPanel hatch" — that was wrong.
windows-rs PR #4499 added the SwapChainPanel widget; the client now uses WinUI 3 via
windows-reactor. Update CLAUDE.md M4, the bootstrap-doc status banner (reactor integration:
pinned git dep, CARGO_WORKSPACE_DIR, App-SDK build.rs, LL-hook stream input), and the
docs-site clients page (WinUI 3, launch-and-pick-a-host).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the SDL3 gamepad service (near-verbatim port of the GTK client's — SDL3 is
cross-platform) and wires it into the winit app: per-session capture (buttons/axes,
DualSense touchpad + motion 0xCC), feedback (rumble, lightbar, raw DualSense effects),
single-pad-forwarded model with auto pad-type from the physical controller. Built from
source on Windows (no system SDL3).
- gamepad.rs: GamepadService (app-lifetime SDL thread) attach/detach on session
connect/end; auto_pref resolves "Automatic" to the attached pad's type.
- app.rs: hold the service, attach on Connected, detach on Ended/Failed/close. Also
simplify the keydown path (drop the identical if/else arms).
- main.rs: start the service for the windowed path, resolve GamepadPref from settings +
the physical pad.
Build gotcha documented + fixed in the dev loop: SDL3's build-from-source MSVC
precompiled-header chokes on the `ü` in the dev box's username embedded in the cargo
registry path (MSB8084/C4828) — CARGO_HOME must be an ASCII path
(C:\Users\Public\.cargo). Unrelated to our code.
Docs: CLAUDE.md M4 + docs/windows-client-bootstrap.md status banner (winit-not-Reactor
rationale, CARGO_HOME gotcha, what's pending) + docs-site clients.md "Windows desktop
client (in development)". Crate is build + clippy + fmt + test green on
x86_64-pc-windows-msvc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The signing rollout is confirmed end to end: the latest published RPM (0.2.0-0.ci1089) carries
a header GPG signature (added by `rpm --addsign`) and passed the in-CI `rpmkeys --checksig`
self-verify before publishing (a bad/unsigned build fails that gate and never reaches the
registry). So flip every .repo snippet from gpgcheck=0 to gpgcheck=1 and add the package-signing
public key (served from the generic registry, committed at packaging/rpm/RPM-GPG-KEY-punktfunk) to
gpgkey= alongside the Gitea metadata key — dnf/rpm-ostree imports both. Covers rpm/README,
packaging/README, the bootc Containerfile, and the docs-site bazzite/fedora-kde install pages;
rpm/README's signing section reframed from "dormant/enabling" to active (+ key-rotation notes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TOFU let anyone who could reach the host click "Trust" and stream, which defeats the point
on a LAN. Make SPAKE2 PIN pairing the default and only way to trust a NEW host; TOFU survives
as an explicit HOST opt-in (for fully trusted networks), advertised over mDNS so clients render
their trust UI from the host's policy rather than offering trust on faith.
Contract:
- Host advertises pair=required (default) or pair=optional. pair=required rejects unpaired
clients at the handshake; pair=optional accepts them (TOFU).
- Clients: a pinned host whose fingerprint matches connects silently; a pinned host whose
fingerprint CHANGED forces re-pairing via PIN (no re-trust shortcut); a NEW host is offered
TOFU only if it advertised pair=optional, otherwise PIN pairing is mandatory; a manually-typed
or unknown-policy host is always PIN.
Host (crates/punktfunk-host/src/main.rs):
- m3-host now REQUIRES pairing by default (was open by default). New --allow-tofu opts into
accepting unpaired clients + advertising pair=optional; pairing is always armed (PIN logged at
startup). serve --native was already secure-by-default (serve --open). The mDNS advert and the
accept loop already mapped require_pairing -> pair=required + reject; only the m3-host CLI
default + help text changed.
Clients honor the advertised policy:
- Android (MainActivity.kt): TOFU only for a discovered pair=optional host; manual/unknown -> PIN;
fp-change -> re-pair only (dropped the "Forget & re-TOFU" shortcut).
- Apple (HostDiscovery/SessionModel/ContentView/HostCards/HostStore): new allowsTofu
(pair==optional, distinct from unknown); connect() gates .awaitingTrust on it; unpinned
non-optional hosts route to the PIN sheet; "Forget Identity" re-pairs rather than re-TOFUs.
- Linux (app.rs/ui_hosts.rs/session.rs): ConnectRequest.pair_required -> pair_optional;
initiate_connect routes pinned/fp-changed/optional/else; manual + --connect unknown -> PIN; a
pinned connect rejected on trust grounds re-pairs.
Docs (CLAUDE.md, README.md, docs-site/content/docs/pairing.md): describe the gated model — PIN is
the default, TOFU an explicit opt-in with an impostor warning.
Verified: host cargo check/clippy/fmt clean; Android built + live (emulator -> home-worker-2):
a manual connect now opens the PIN dialog (no Trust button) and the PIN ceremony streams; Apple
swift build clean; Linux clippy -D warnings + fmt clean on the Linux box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the host docs match the real distribution path and the actual CLI. Reviewed by a
multi-agent pass (6 editors against one verified fact sheet + an accuracy reviewer); its
findings (a wrong client-Recommends claim, a native-concurrency overstatement) folded in.
- Install front door: new README "Install (host)" method-picker + docs-site/install.md
(+ nav), routing each distro to its package registry; source build demoted to a fallback.
- Registry-first install: ubuntu-gnome/ubuntu-kde now lead with the apt registry (not a
cargo build); bazzite leads with the Gitea RPM registry (was COPR/source). Source builds
moved to an appendix.
- CLI accuracy: serve --native arms pairing from the web console (NOT --allow-pairing, which
with --require-pairing/--max-concurrent is m3-host-only); --open disables mandatory pairing.
host-cli/configuration/pairing/quickstart/troubleshooting corrected; mgmt API documented as
always HTTPS+token. Native host serves one session at a time (extras queue) — not multi.
- Firewall: real ports documented (native UDP 9777 + the ephemeral data port caveat +
GameStream ports) for Debian + Arch (ufw + nftables), not just Bazzite.
- Sync/accuracy: punktfunk-client (GTK4) presented as a shipping client (not "roadmap"),
punktfunk-client-rs as the headless tool; host Recommends punktfunk-web only (not the
client); COPR chroots f43/44; bootc header says Gitea registry not COPR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Mac App Store requires App Sandbox, which the macOS app didn't declare.
App Sandbox is macOS-only (invalid on iOS/tvOS, fails upload validation), so
the macOS target now uses a dedicated Config/Punktfunk-macOS.entitlements while
iOS/tvOS keep the shared Config/Punktfunk.entitlements (unchanged). The single
macOS app is sandboxed for BOTH channels — the Developer ID DMG is codesigned
with the same file — so the local build equals what App Store users get.
Entitlement set (verified against the code + Apple docs):
- app-sandbox, network.client.
- network.server: NOT optional despite the client being outbound-only — the
sandbox gates the bind() syscall as network-bind, and quinn (quic.rs) + the
raw-UDP plane (transport/udp.rs) both bind explicitly, so host->client
datagrams never arrive without it (the classic QUIC-under-sandbox trap).
- device.audio-input (mic uplink), device.bluetooth + device.usb (Xbox/DualSense
controllers over BT/USB via GameController), keychain-access-groups (existing).
Omitted: device.hid (undocumented), files.user-selected.* (no pickers),
networking.multicast (Bonjour browse is exempt; requesting it breaks signing).
CI (release.yml): add a macOS App Store archive+upload-to-TestFlight step
mirroring the iOS lane (manual Apple Distribution signing + the 'Punktfunk macOS
App Store Distribution' profile, app-store-connect/upload, installer-signed pkg),
continue-on-error until the portal prereqs exist; point the Developer ID DMG
codesign at the sandboxed entitlements. Docs (ci.md) + clients/apple README
updated; the runner additionally needs the macOS platform on the App Store
Connect record + the '3rd Party Mac Developer Installer' cert.
Verified: signed Debug build embeds exactly the intended entitlements
(codesign -d --entitlements), swift build green against the rebuilt xcframework.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docker.yml: build the punktfunk-fedora44-rpm builder image (parameterized Dockerfile,
FEDORA_VERSION=44) alongside the F43/Bazzite one.
- rpm.yml: matrix the build/publish over both channels — fedora-fedora-rpm→bazzite (F43,
libavcodec.so.61) and fedora44-rpm→fedora-44 (F44, libavcodec.so.62). fail-fast:false so one
channel's break doesn't sink the other. (Bootstrap: the F44 builder image must be pushed by
docker.yml once before rpm.yml's fedora-44 job can pull it — same dance as the other images.)
- fedora-kde.md: rewrite as the reproducible RPM-install guide validated live on a Fedora 44
KDE box (RTX 4090): RPM Fusion + akmod-nvidia + the ffmpeg-free→RPM-Fusion swap for NVENC +
Secure Boot MOK enroll; the fedora-44 dnf repo + `dnf install punktfunk`; and the headless
punktfunk-kde-session.service (kwin --virtual with NO_PERMISSION_CHECKS — an interactive
Plasma session won't hand its privileged zkde_screencast protocol to an external client).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>