cargo tree -p punktfunk-client-session finds no ffmpeg. The host still does,
which is the whole point: pf-encode keeps libavcodec unconditionally and no
host workflow, packaging script or licence file was touched.
Deleted: crates/pf-ffvk, video_vulkan.rs, video_vaapi.rs, video_libav.rs, the
libavcodec half of video_d3d11.rs, the av_log machinery, ffmpeg::codec::Id as
the decoder's vocabulary (the quic CODEC_* wire constants now serve, which is
why the evidence table was keyed on them), DecodedImage::VkFrame and ::Dmabuf,
the presenter's AVVkFrame lane, and the ffmpeg-fallback feature with
everything behind it. DrmFrameGuard collapses from an enum to a newtype, which
removes an unsafe impl Send. Roughly 25,000 lines.
Then the CI, packaging, licensing and docs work the plan's §6 lists: the
Windows workflows lose FFMPEG_DIR, PF_FFVK_VULKAN_INCLUDE and their PATH
prepend; the MSIX loses its DLL wildcard; the client .deb stops emitting libav
sonames on its own because depends come from dpkg-shlibdeps; arch, flatpak and
nix drop the dependency; and the README's "FFmpeg 7 or 8" contract narrows to
the host.
Three defects reached users' machines in the first cut, and none was in the
deletion itself.
All three desktop Settings UIs offer vulkan, vaapi and d3d11va as stored
decoder values, so those strings sit in shipped settings files today. Refusing
them by name — which is the correct rule for a stale pin — would have bricked
every upgraded client whose owner ever touched that dropdown. They now migrate
onto the native rung for the same hardware family, at decoder construction AND
at each dialog's lookup, because a legacy value that matches no preset
displays as "Automatic" and silently rewrites the user's preference on the
next save.
M9's evidence filter was deleted on the argument that with no libavcodec twin
below, barring an unproven rung removes hardware decode rather than moving
down one rung. That is true on Windows and false on Linux for Intel and every
unknown vendor id, where prefer_vulkan_first is false and the order is
native-vaapi → native-vk: a rung that has decoded nothing anywhere sitting
above one that is 250/250 on three drivers. Every Intel Linux desktop would
have moved from libavcodec VAAPI, shipping for years, onto pf-vaadec by
default — and a rung that constructs and then produces wrong pixels leaves
only by the error-streak demotion, which this codebase already documents as
not tripping on the B580's strobing. The filter is restored as a narrow, pure,
testable rule: an unproven rung yields to a proven one, and to nothing else.
Windows deliberately passes no rung below, because that vendor family is the
one with a measured wrong-pixel report against Vulkan decode, and trading no
evidence for evidence of corruption is the wrong direction.
And the notices still said FFmpeg was bundled. The root file is what both
desktop clients include_str! and what the MSIX ships, three lines under the
new card saying no FFmpeg is bundled; Apple's Acknowledgements said it too, on
iOS, tvOS and macOS. The generator now emits four per-client files scoped by
transitive closure — 0 FFmpeg mentions in each, verified — while the root file
keeps it for the host. That also ends the standing false attribution of
ffmpeg-next, GTK4, windows-rs and the NVENC SDK to an iPhone.
Windows has no reachable box, so it was compiled instead: a cross clippy at
-D warnings on x86_64 and aarch64-pc-windows-msvc with the C toolchain stubbed
so build scripts run without linking. That gate immediately caught an
include_str! path one directory too deep, which nothing else could have.
Gates: container clippy -D warnings, 160 tests, workspace check, both Windows
targets clean, client ffmpeg count 0 and host 2. The four decode crates are
untouched, so the hardware rungs' 250/250 stands.
⚠ Owed and unrun: no GPU has executed any of this milestone. M8's on-glass
software check, M7's D3D11 and VAAPI AV1 hardware legs, and M9's field bake
all still want hardware, and the bake window and criteria remain the user's.
The arch job was the one still paying full freight every run: ~1 GB of
pacman across its two install steps (never cached — container layers die
with the job) and cold cargo builds (arch was skipped in the sccache
rollout). punktfunk-arch-ci bakes base-devel + both makepkg legs' deps +
bun + node + sccache; the in-job installs become --needed no-op guards for
the one push where :latest lags. Rolling-release note in the Dockerfile:
packages now build against the image's snapshot, the same staleness the
gamescope cache already embraces, re-snapshotted on any ci/ edit.
sccache reaches makepkg by crossing the sudo boundary explicitly —
env_reset strips ambient env, so the wrapper env rides the existing
`sudo -u builder env ...` list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two first-run discoveries. actions/checkout (and every JS action) execs
`node` inside the job container — the android-ci image didn't ship it, so
the job died at checkout with exit 127 (flatpak.yml documents the same
lesson for fedora:43); a trailing layer keeps the fat SDK/NDK layers
cache-valid. And windows-msix has been red since bf981027 required
punktfunk.exe in the package without adding punktfunk-cli to the build —
the same gap 90c84ef4 already closed for deb, now closed here (rpm and
arch already build it; the CLI has no features, so the arm64 leg's
--no-default-features changes nothing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Android was the last job still downloading its world every run: ~3 GB of
SDK/NDK/CMake from Google, a from-source cargo-ndk build, and one monolithic
cache key where a gradle edit invalidated the cargo registry and the Rust
target/ with it. punktfunk-android-ci (content-keyed, LAN registry) bakes
JDK 21 + SDK + NDK + cargo-ndk + sccache; the workflow shrinks to checkout →
two caches → gradle. The cargo-home cache joins the fleet-wide namespace
(same lockfile, same layout — it was the same bytes under a private key),
gradle gets its own key shared with android-screenshots (which stored the
identical content under a second name), and target/ leaves the cache —
sccache covers the three ABI builds now.
Apple: sccache (self-healing ~/.local/bin install + already provisioned on
the mini) covers every cargo invocation build-xcframework.sh makes across
the swift job, the screenshots job and release.yml — three jobs that each
recompiled the same core. screenshots.sh learns PF_SHOT_DERIVED_DATA so CI
pins one stable DerivedData root instead of cold-building into two throwaway
mktemp trees per run (release.yml's disease, same cure), and the Simulators
get shut down after capture (the 846-leaked-sims lesson).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend = the existing RustFS (storage.unom.io, S3, region home-central;
LAN-pinned to home-central's address by ci-core's unbound so cache traffic
never hairpins the router). Repo secrets SCCACHE_ACCESS_KEY_ID/SECRET carry a
keypair scoped to the unom-ci-sccache bucket; keys embed compiler hash +
target + flags, so the Ubuntu, Fedora, cross-arm64 and MSVC universes share
one bucket without ever colliding.
Wired: ci (rust, rust-arm64), deb (all three), rpm, bench,
linux-client-screenshots, windows, windows-msix, windows-host.
CARGO_INCREMENTAL=0 alongside (sccache and incremental are mutually
exclusive, and incremental artifacts are what bloated the persistent Windows
target dirs anyway). The binary is baked into the builder images; a per-job
ensure-step (same pattern as the GTK4 packages step) keeps jobs green while
the running :latest predates the bake, and ensure-windows-toolchain.ps1
self-provisions sccache.exe on the Windows runner. windows-drivers stays
unwrapped (wdk-build owns its build env), arch/android/apple are follow-ups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five builder images now live on home-ci-core's LAN registry
(192.168.1.58:5010) under content keys — a hash of the ci/ tree (+
rust-toolchain.toml for the cross image). docker.yml builds one only when its
key has no manifest yet, so a push that doesn't touch ci/ costs a curl per
image instead of seven WAN pushes and a set of per-SHA tags that no plain
prune could ever reclaim. Releases pin builders by copying the key manifest
to a vX.Y.Z tag via the registry API — no rebuild, no bytes moved.
Around that: deb/rpm/arch/android/apple/decky get path filters so docs-only
pushes stop lighting up the whole fleet (branch pushes only — tag runs match
tags:, as flatpak/windows-msix releases have proven for months); the
report-only bench job moves to bench.yml (nightly + dispatch) and stops
occupying a fleet slot per push; flatpak caches its Flathub runtimes and
builder state instead of re-downloading multi-GB every run; rpm's cargo
registry cache gets its own key namespace instead of sharing the Ubuntu
jobs'; audit caches cargo bin+registry rather than the whole toolchain dir;
docker-prune.sh loses the local act-cache cap/burst-clear (the cache is
central now — deleting it under disk pressure was how runner-2 ended up
cold-building everything) and gains a leaked-network prune.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds punktfunk-rust-ci-arm64cross (the rust-ci toolchain + an Ubuntu ports
arm64 multiarch sysroot) and a deb.yml job that cross-builds the client
package on the ordinary amd64 runner — no arm64 runner in the fleet and
none needed. Client only: the Linux host encodes with NVENC/QSV/AMF, all
x86. The apt registry keys pool entries by the .deb's own arch, so an arm64
box picks the package up with no client-side configuration.
Three things this had to get right, each of which cost a failed build:
* rustup target add must run against the toolchain rust-toolchain.toml
PINS, not the image's default `stable` — otherwise the pinned toolchain
has no aarch64 std and the build dies ~230 crates in on "can't find
crate for core". Copying the pin file in also pre-downloads the
toolchain every CI job currently re-fetches on first use.
* ffmpeg-sys-next compiles a probe it intends to RUN, so it forces
.target(HOST) while still passing the TARGET's include paths; the arm64
dir then shadows the host's own libc headers and the x86 compiler dies
in bits/math-vector.h on NEON/SVE types. Prepending the host dir does
NOT help — GCC drops a -I that duplicates a system directory, keeping
its original later position — so ci/pf-host-cc strips target include
dirs from host compiles instead.
* the job hard-fails on a non-AArch64 session binary, rather than
shipping an amd64 payload under an arm64 package name.
skia needs no special handling: the aarch64-linux-gnu textlayout+vulkan
prebuilt resolves, so arm64 ships the FULL client, OSD included — unlike
Windows ARM64, which still builds --no-default-features.
The cross image builds in its own docker.yml job (needs: build-push) since
it is FROM punktfunk-rust-ci:latest and must not race the entry that
publishes that base. rpm/Arch/Flatpak stay x86_64 for now — their builders
have no arm64 sysroot, which is its own piece of work.
Plan: punktfunk-planning design/embedded-arm64-client.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The host .deb was built on the Ubuntu 26.04 rust-ci image, so dpkg-shlibdeps
baked in `Depends: libavcodec62` (FFmpeg 8) and a glibc-2.41 floor — making it
uninstallable on Ubuntu 24.04 LTS (FFmpeg 6.1 / libavcodec60, glibc 2.39; apt
reports the deps as "too recent"). The source floor (ffmpeg-next 8, libavcodec
>=61 APIs) means a straight 24.04 rebuild would fail too.
Build the host on Ubuntu 24.04 instead — lowering the glibc floor to 2.39 so one
binary runs on 24.04 -> 26.04 — and bundle a from-source LGPL FFmpeg 8 into the
package so it no longer depends on the distro libav*. Everything else the host
links is soname-compatible on 24.04 (opus is vendored via cmake; NVENC/libcuda
are dlopen-only, never link-time), and the only FFmpeg encoders used are
*_nvenc / *_vaapi (software H.264 fallback is the BSD-2 openh264 crate, not
FFmpeg libx264), so an LGPL build keeps the bundle license-clean.
- ci/rust-ci-noble.Dockerfile (new): ubuntu:24.04 builder; nv-codec-headers +
FFmpeg 8 (--enable-nvenc --enable-vaapi, shared) -> /opt/ffmpeg; PKG_CONFIG_PATH.
- packaging/debian/build-deb.sh: BUNDLE_FFMPEG=1 copies libav*/libsw*/libpostproc
into /usr/lib/punktfunk-host, patchelf-sets the rpath ($ORIGIN per-lib + binary
--force-rpath), feeds them to dpkg-shlibdeps (captures libva2/libdrm2), and drops
the libav* sonames from Depends. Normal (non-bundle) path unchanged.
- .gitea/workflows/deb.yml: split into build-publish (client/web/scripting on the
26.04 image) and build-publish-host (noble image, BUNDLE_FFMPEG=1); parallel,
identical version step, same apt distribution -> one universal host .deb.
- .gitea/workflows/docker.yml: build+push punktfunk-rust-ci-noble.
- packaging/debian/README.md: document the 24.04 LTS path + bundled local build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The re-architecture split the Linux client into two binaries (shell + Vulkan
session streamer) and added the pf-ffvk crate, whose build.rs runs bindgen over
FFmpeg's libavutil/hwcontext_vulkan.h (#include <vulkan/vulkan.h>). Two release
regressions fell out of that on the desktop-package legs:
1. Missing build dep. The Arch build (archlinux:base-devel + its own pacman list)
fatal-errored at `vulkan/vulkan.h file not found`. Add the Vulkan dev headers
wherever the client compiles from a self-managed dep set: arch (PKGBUILD
makedepends + arch.yml), rpm (spec BuildRequires + rpm.yml + fedora image),
deb.yml (belt-and-suspenders; the rust-ci image already bakes libvulkan-dev),
and the screenshots job. The flatpak builds offline against the GNOME SDK, so
install Vulkan-Headers into /app as a pinned module and point pf-ffvk's bindgen
at it via PF_FFVK_VULKAN_INCLUDE.
2. Only the shell shipped. arch/deb/rpm built and installed just punktfunk-client;
the shell execs its sibling punktfunk-session for a connect, so desktop
streaming would break exactly as Decky's did. Build and install BOTH binaries
in all three, and declare the runtime Vulkan loader the session binary dlopens
(vulkan-icd-loader / vulkan-loader / libvulkan1 — not a DT_NEEDED, so
shlibdeps/auto-requires can't see it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ffmpeg-sys-next binds a curated header list that omits every
hwcontext_*.h, so AVVulkanDeviceContext/AVVulkanFramesContext/AVVkFrame
— the surface that lets FFmpeg's Vulkan Video decoder run on the
presenter's own VkDevice and hand the decoded VkImages back — had no
Rust bindings at all. pf-ffvk generates them at build time from the
SYSTEM headers, which is the only ABI-safe option: the struct layout
depends on FF_API_* deprecation gates resolved in libavutil/version.h
(confirmed: FFmpeg 8.1 still carries the FIXED_QUEUES fields). Ships
ash<->vulkan.h handle conversions; opaque AVHW*/AVFrame types keep
ffmpeg-sys-next the owner of the core surface (pointer casts across).
Needs vulkan-headers (Arch) / libvulkan-dev (CI, added);
PF_FFVK_VULKAN_INCLUDE overrides for cross builds. Verified: bindgen
layout tests pass, av_vk_frame_alloc links and zero-initializes.
Co-Authored-By: Claude Fable 5 <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>
deb.yml builds the punktfunk-web .output in the rust-ci image, but that image had no bun
(only ci.yml's web/docs jobs use the oven/bun image) -> "bun: not found". Bake bun (+ unzip
for its installer) into ci/rust-ci.Dockerfile, and bootstrap it in the deb web step too so the
job is green against the previous image (docker.yml rebuild lag) — mirroring the rpm.yml fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The punktfunk-web management console (pairing + status) shipped only via apt. Extend it
to the other HOST packaging methods, mirroring the Debian punktfunk-web .deb (flatpak is
the client, correctly excluded):
- rpm/punktfunk.spec: new noarch `punktfunk-web` subpackage (the .output bundle + a
/usr/bin/punktfunk-web-server node launcher + both systemd --user units + web-init.sh +
web.env.example), gated behind `%bcond_with web`. OFF by default because building the
Nitro/Node SSR bundle needs `bun`, which a plain rpmbuild / COPR mock chroot lacks. Host
package weak-Recommends punktfunk-web.
- ci/fedora-rpm.Dockerfile: install bun (+ unzip) so the CI builder can build the console.
- rpm.yml: build `PF_WITH_WEB=1` (Prep bootstraps bun to stay green pre-image-rebuild); the
publish loop already globs the new noarch rpm into the registry. build-rpm.sh: `--with web`
when PF_WITH_WEB=1.
- bootc/Containerfile: install from the Gitea RPM registry (which carries punktfunk-web)
instead of COPR — `dnf5 install punktfunk punktfunk-web`.
- arch/PKGBUILD: opt-in `punktfunk-web` split member (PF_WITH_WEB=1 appends it + bun) so a
default makepkg still builds host+client with no JS tooling — matching the spec's bcond.
- docs: packaging/README, rpm/README, copr/README (the no-bun caveat), bazzite/README
(Path B rewritten COPR→Gitea registry), arch/README — enable + journal-password steps.
Reviewed across methods by an adversarial multi-agent pass (rpm/ci/arch/bootc/consistency
lenses, each blocking finding 3x-verified); fixed the two it confirmed real — the Arch
bun-mandatory regression (now opt-in) and the stale COPR wording in bazzite Path B.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three changes to make a reproducible Fedora KDE host install:
- ci/fedora-rpm.Dockerfile: parameterize the Fedora base (ARG FEDORA_VERSION, default 43) so the
same builder produces the Bazzite (F43, libavcodec.so.61) and Fedora 44 (libavcodec.so.62) RPMs.
A binary RPM is soname-coupled to its base, so each target Fedora needs its own build/channel.
- spec: install punktfunk-kde-session.service (was in the tree but never packaged) with its
ExecStart repointed from the dev source tree to the installed run-headless-kde.sh. This is the
headless `kwin --virtual` session (KWIN_WAYLAND_NO_PERMISSION_CHECKS=1) the kwin backend needs —
an interactive Plasma session refuses to hand its privileged zkde_screencast protocol to an
external client, so a dedicated session is required. Not enabled by default (kwin hosts opt in).
- ship packaging/kde/host.env as host.env.kde — the ready KWin appliance config (wayland-kde).
Validated live on a Fedora 44 KDE box (RTX 4090): KWin virtual output + zero-copy dmabuf->CUDA->NVENC.
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>
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>
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>
Three workflows: ci.yml (Rust workspace inside the punktfunk-rust-ci
builder image + web/docs-site build+typecheck), docker.yml (build+push
punktfunk-web, punktfunk-docs, punktfunk-rust-ci to git.unom.io — host
and native clients stay un-dockerized by design), apple.yml (host-mode
macos-arm64 runner: Rust core -> PunktfunkCore.xcframework ->
swift build + swift test).
ci/rust-ci.Dockerfile: Ubuntu 26.04 with the workspace's link deps
(FFmpeg 8, PipeWire, Opus, GL/EGL/GBM, xkbcommon, libcuda via the
580-server userspace as a link stub) + pinned rustup + node for the JS
actions. Verified end to end in-container: build, 141/141 tests, C ABI
harness; all three images seeded to the registry manually.
scripts/ci/setup-macos-runner.sh provisions the Mac (rustup + darwin
targets, Node tarball, gitea-runner 1.0.8 host mode, LaunchAgent with
DEVELOPER_DIR auto-detect for sudo-free Xcode selection). Docs in
docs-site/content/docs/ci.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>